bsw/jbe@1309: /**
bsw/jbe@1309:  * @license wysihtml v0.6.0-beta1
bsw/jbe@1309:  * https://github.com/Voog/wysihtml
bsw/jbe@1309:  *
bsw/jbe@1309:  * Author: Christopher Blum (https://github.com/tiff)
bsw/jbe@1309:  * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
bsw/jbe@1309:  *
bsw/jbe@1309:  * Copyright (C) 2012 XING AG
bsw/jbe@1309:  * Licensed under the MIT license (MIT)
bsw/jbe@1309:  *
bsw/jbe@1309:  */
bsw/jbe@1309: var wysihtml = {
bsw/jbe@1309:   version: '0.6.0-beta1',
bsw/jbe@1309: 
bsw/jbe@1309:   // namespaces
bsw/jbe@1309:   commands:   {},
bsw/jbe@1309:   dom:        {},
bsw/jbe@1309:   quirks:     {},
bsw/jbe@1309:   toolbar:    {},
bsw/jbe@1309:   lang:       {},
bsw/jbe@1309:   selection:  {},
bsw/jbe@1309:   views:      {},
bsw/jbe@1309: 
bsw/jbe@1309:   editorExtenders: [],
bsw/jbe@1309:   extendEditor: function(extender) {
bsw/jbe@1309:     this.editorExtenders.push(extender);
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   INVISIBLE_SPACE: '\uFEFF',
bsw/jbe@1309:   INVISIBLE_SPACE_REG_EXP: /\uFEFF/g,
bsw/jbe@1309: 
bsw/jbe@1309:   VOID_ELEMENTS: 'area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr',
bsw/jbe@1309:   PERMITTED_PHRASING_CONTENT_ONLY: 'h1, h2, h3, h4, h5, h6, p, pre',
bsw/jbe@1309: 
bsw/jbe@1309:   EMPTY_FUNCTION: function() {},
bsw/jbe@1309: 
bsw/jbe@1309:   ELEMENT_NODE: 1,
bsw/jbe@1309:   TEXT_NODE:    3,
bsw/jbe@1309: 
bsw/jbe@1309:   BACKSPACE_KEY:  8,
bsw/jbe@1309:   ENTER_KEY:      13,
bsw/jbe@1309:   ESCAPE_KEY:     27,
bsw/jbe@1309:   SPACE_KEY:      32,
bsw/jbe@1309:   TAB_KEY:        9,
bsw/jbe@1309:   DELETE_KEY:     46
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.polyfills = function(win, doc) {
bsw/jbe@1309: 
bsw/jbe@1309:   var methods = {
bsw/jbe@1309: 
bsw/jbe@1309:     // Safary has a bug of not restoring selection after node.normalize correctly.
bsw/jbe@1309:     // Detects the misbegaviour and patches it
bsw/jbe@1309:     normalizeHasCaretError: function() {
bsw/jbe@1309:       if ("createRange" in doc && "getSelection" in win) {
bsw/jbe@1309:         var originalTarget,
bsw/jbe@1309:             scrollTop = window.pageYOffset,
bsw/jbe@1309:             scrollLeft = window.pageXOffset,
bsw/jbe@1309:             e = doc.createElement('div'),
bsw/jbe@1309:             t1 = doc.createTextNode('a'),
bsw/jbe@1309:             t2 = doc.createTextNode('a'),
bsw/jbe@1309:             t3 = doc.createTextNode('a'),
bsw/jbe@1309:             r = doc.createRange(),
bsw/jbe@1309:             s, ret;
bsw/jbe@1309: 
bsw/jbe@1309:         if (document.activeElement) {
bsw/jbe@1309:           if (document.activeElement.nodeType === 1 && ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].indexOf(document.activeElement.nodeName) > -1) {
bsw/jbe@1309:             originalTarget = {
bsw/jbe@1309:               type: 'form',
bsw/jbe@1309:               node: document.activeElement,
bsw/jbe@1309:               start: document.activeElement.selectionStart,
bsw/jbe@1309:               end: document.activeElement.selectionEnd,
bsw/jbe@1309:             };
bsw/jbe@1309:           } else {
bsw/jbe@1309:             s = win.getSelection();
bsw/jbe@1309:             if (s && s.anchorNode) {
bsw/jbe@1309:               originalTarget = {
bsw/jbe@1309:                 type: 'range',
bsw/jbe@1309:                 anchorNode: s.anchorNode,
bsw/jbe@1309:                 anchorOffset: s.anchorOffset,
bsw/jbe@1309:                 focusNode: s.focusNode,
bsw/jbe@1309:                 focusOffset: s.focusOffset
bsw/jbe@1309:               };
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         e.setAttribute('contenteditable', 'true');
bsw/jbe@1309:         e.appendChild(t1);
bsw/jbe@1309:         e.appendChild(t2);
bsw/jbe@1309:         e.appendChild(t3);
bsw/jbe@1309:         doc.body.appendChild(e);
bsw/jbe@1309:         r.setStart(t2, 1);
bsw/jbe@1309:         r.setEnd(t2, 1);
bsw/jbe@1309: 
bsw/jbe@1309:         s = win.getSelection();
bsw/jbe@1309:         s.removeAllRanges();
bsw/jbe@1309:         s.addRange(r);
bsw/jbe@1309:         e.normalize();
bsw/jbe@1309:         s = win.getSelection();
bsw/jbe@1309: 
bsw/jbe@1309:         ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2);
bsw/jbe@1309:         e.parentNode.removeChild(e);
bsw/jbe@1309:         s.removeAllRanges();
bsw/jbe@1309: 
bsw/jbe@1309:         if (originalTarget) {
bsw/jbe@1309:           if (originalTarget.type === 'form') {
bsw/jbe@1309:             // The selection parameters are not present for all form elements
bsw/jbe@1309:             if (typeof originalTarget.start !== 'undefined' && typeof originalTarget.end !== 'undefined') {
bsw/jbe@1309:               originalTarget.node.setSelectionRange(originalTarget.start, originalTarget.end);
bsw/jbe@1309:             }
bsw/jbe@1309:             originalTarget.node.focus();
bsw/jbe@1309:           } else if (originalTarget.type === 'range') {
bsw/jbe@1309:             r = doc.createRange();
bsw/jbe@1309:             r.setStart(originalTarget.anchorNode, originalTarget.anchorOffset);
bsw/jbe@1309:             r.setEnd(originalTarget.focusNode, originalTarget.focusOffset);
bsw/jbe@1309:             s.addRange(r);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (scrollTop !== window.pageYOffset || scrollLeft !== window.pageXOffset) {
bsw/jbe@1309:           win.scrollTo(scrollLeft, scrollTop);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return ret;
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     apply: function() {
bsw/jbe@1309:       // closest, matches, and remove polyfill
bsw/jbe@1309:       // https://github.com/jonathantneal/closest
bsw/jbe@1309:       (function (ELEMENT) {
bsw/jbe@1309:         ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
bsw/jbe@1309:           var
bsw/jbe@1309:           element = this,
bsw/jbe@1309:           elements = (element.document || element.ownerDocument).querySelectorAll(selector),
bsw/jbe@1309:           index = 0;
bsw/jbe@1309: 
bsw/jbe@1309:           while (elements[index] && elements[index] !== element) {
bsw/jbe@1309:             ++index;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           return elements[index] ? true : false;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         ELEMENT.closest = ELEMENT.closest || function closest(selector) {
bsw/jbe@1309:           var element = this;
bsw/jbe@1309: 
bsw/jbe@1309:           while (element) {
bsw/jbe@1309:             if (element.matches(selector)) {
bsw/jbe@1309:               break;
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             element = element.parentElement;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           return element;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         ELEMENT.remove = ELEMENT.remove || function remove() {
bsw/jbe@1309:           if (this.parentNode) {
bsw/jbe@1309:             this.parentNode.removeChild(this);
bsw/jbe@1309:           }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:       }(win.Element.prototype));
bsw/jbe@1309: 
bsw/jbe@1309:       if (!('classList' in doc.documentElement) && win.Object.defineProperty && typeof win.HTMLElement !== 'undefined') {
bsw/jbe@1309:         win.Object.defineProperty(win.HTMLElement.prototype, 'classList', {
bsw/jbe@1309:           get: function() {
bsw/jbe@1309:             var self = this;
bsw/jbe@1309:             function update(fn) {
bsw/jbe@1309:               return function(value) {
bsw/jbe@1309:                 var classes = self.className.split(/\s+/),
bsw/jbe@1309:                     index = classes.indexOf(value);
bsw/jbe@1309: 
bsw/jbe@1309:                 fn(classes, index, value);
bsw/jbe@1309:                 self.className = classes.join(' ');
bsw/jbe@1309:               };
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             var ret = {
bsw/jbe@1309:                 add: update(function(classes, index, value) {
bsw/jbe@1309:                   ~index || classes.push(value);
bsw/jbe@1309:                 }),
bsw/jbe@1309: 
bsw/jbe@1309:                 remove: update(function(classes, index) {
bsw/jbe@1309:                   ~index && classes.splice(index, 1);
bsw/jbe@1309:                 }),
bsw/jbe@1309: 
bsw/jbe@1309:                 toggle: update(function(classes, index, value) {
bsw/jbe@1309:                   ~index ? classes.splice(index, 1) : classes.push(value);
bsw/jbe@1309:                 }),
bsw/jbe@1309: 
bsw/jbe@1309:                 contains: function(value) {
bsw/jbe@1309:                   return !!~self.className.split(/\s+/).indexOf(value);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 item: function(i) {
bsw/jbe@1309:                   return self.className.split(/\s+/)[i] || null;
bsw/jbe@1309:                 }
bsw/jbe@1309:               };
bsw/jbe@1309: 
bsw/jbe@1309:             win.Object.defineProperty(ret, 'length', {
bsw/jbe@1309:               get: function() {
bsw/jbe@1309:                 return self.className.split(/\s+/).length;
bsw/jbe@1309:               }
bsw/jbe@1309:             });
bsw/jbe@1309: 
bsw/jbe@1309:             return ret;
bsw/jbe@1309:           }
bsw/jbe@1309:         });
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var getTextNodes = function(node){
bsw/jbe@1309:         var all = [];
bsw/jbe@1309:         for (node=node.firstChild;node;node=node.nextSibling){
bsw/jbe@1309:           if (node.nodeType == 3) {
bsw/jbe@1309:               all.push(node);
bsw/jbe@1309:           } else {
bsw/jbe@1309:             all = all.concat(getTextNodes(node));
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:         return all;
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       var isInDom = function(node) {
bsw/jbe@1309:         var doc = node.ownerDocument,
bsw/jbe@1309:             n = node;
bsw/jbe@1309: 
bsw/jbe@1309:         do {
bsw/jbe@1309:           if (n === doc) {
bsw/jbe@1309:             return true;
bsw/jbe@1309:           }
bsw/jbe@1309:           n = n.parentNode;
bsw/jbe@1309:         } while(n);
bsw/jbe@1309: 
bsw/jbe@1309:         return false;
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       var normalizeFix = function() {
bsw/jbe@1309:         var f = win.Node.prototype.normalize;
bsw/jbe@1309:         var nf = function() {
bsw/jbe@1309:           var texts = getTextNodes(this),
bsw/jbe@1309:               s = this.ownerDocument.defaultView.getSelection(),
bsw/jbe@1309:               anode = s.anchorNode,
bsw/jbe@1309:               aoffset = s.anchorOffset,
bsw/jbe@1309:               aelement = anode && anode.nodeType === 1 && anode.childNodes.length > 0 ? anode.childNodes[aoffset] : undefined,
bsw/jbe@1309:               fnode = s.focusNode,
bsw/jbe@1309:               foffset = s.focusOffset,
bsw/jbe@1309:               felement = fnode && fnode.nodeType === 1 && foffset > 0 ? fnode.childNodes[foffset -1] : undefined,
bsw/jbe@1309:               r = this.ownerDocument.createRange(),
bsw/jbe@1309:               prevTxt = texts.shift(),
bsw/jbe@1309:               curText = prevTxt ? texts.shift() : null;
bsw/jbe@1309: 
bsw/jbe@1309:           if (felement && felement.nodeType === 3) {
bsw/jbe@1309:             fnode = felement;
bsw/jbe@1309:             foffset = felement.nodeValue.length;
bsw/jbe@1309:             felement = undefined;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if (aelement && aelement.nodeType === 3) {
bsw/jbe@1309:             anode = aelement;
bsw/jbe@1309:             aoffset = 0;
bsw/jbe@1309:             aelement = undefined;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_CONTAINS))) {
bsw/jbe@1309:             fnode = [anode, anode = fnode][0];
bsw/jbe@1309:             foffset = [aoffset, aoffset = foffset][0];
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           while(prevTxt && curText) {
bsw/jbe@1309:             if (curText.previousSibling && curText.previousSibling === prevTxt) {
bsw/jbe@1309:               if (anode === curText) {
bsw/jbe@1309:                 anode = prevTxt;
bsw/jbe@1309:                 aoffset = prevTxt.nodeValue.length +  aoffset;
bsw/jbe@1309:               }
bsw/jbe@1309:               if (fnode === curText) {
bsw/jbe@1309:                 fnode = prevTxt;
bsw/jbe@1309:                 foffset = prevTxt.nodeValue.length +  foffset;
bsw/jbe@1309:               }
bsw/jbe@1309:               prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue;
bsw/jbe@1309:               curText.parentNode.removeChild(curText);
bsw/jbe@1309:               curText = texts.shift();
bsw/jbe@1309:             } else {
bsw/jbe@1309:               prevTxt = curText;
bsw/jbe@1309:               curText = texts.shift();
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if (felement) {
bsw/jbe@1309:             foffset = Array.prototype.indexOf.call(felement.parentNode.childNodes, felement) + 1;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if (aelement) {
bsw/jbe@1309:             aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement);
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) {
bsw/jbe@1309:             r.setStart(anode, aoffset);
bsw/jbe@1309:             r.setEnd(fnode, foffset);
bsw/jbe@1309:             s.removeAllRanges();
bsw/jbe@1309:             s.addRange(r);
bsw/jbe@1309:           }
bsw/jbe@1309:         };
bsw/jbe@1309:         win.Node.prototype.normalize = nf;
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       var F = function() {
bsw/jbe@1309:         win.removeEventListener("load", F);
bsw/jbe@1309:         if ("Node" in win && "normalize" in win.Node.prototype && methods.normalizeHasCaretError()) {
bsw/jbe@1309:           normalizeFix();
bsw/jbe@1309:         }
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       if (doc.readyState !== "complete") {
bsw/jbe@1309:         win.addEventListener("load", F);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         F();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // CustomEvent for ie9 and up
bsw/jbe@1309:       function nativeCustomEventSupported() {
bsw/jbe@1309:         try {
bsw/jbe@1309:           var p = new win.CustomEvent('cat', {detail: {foo: 'bar'}});
bsw/jbe@1309:           return  'cat' === p.type && 'bar' === p.detail.foo;
bsw/jbe@1309:         } catch (e) {}
bsw/jbe@1309:         return false;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Polyfills CustomEvent object for IE9 and up
bsw/jbe@1309:       (function() {
bsw/jbe@1309:         if (!nativeCustomEventSupported() && "CustomEvent" in win) {
bsw/jbe@1309:           function CustomEvent(event, params) {
bsw/jbe@1309:             params = params || {bubbles: false, cancelable: false, detail: undefined};
bsw/jbe@1309:             var evt = doc.createEvent('CustomEvent');
bsw/jbe@1309:             evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
bsw/jbe@1309:             return evt;
bsw/jbe@1309:           }
bsw/jbe@1309:           CustomEvent.prototype = win.Event.prototype;
bsw/jbe@1309:           win.CustomEvent = CustomEvent;
bsw/jbe@1309:         }
bsw/jbe@1309:       })();
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   return methods;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.polyfills(window, document).apply();
bsw/jbe@1309: 
bsw/jbe@1309: /*
bsw/jbe@1309: 	Base.js, version 1.1a
bsw/jbe@1309: 	Copyright 2006-2010, Dean Edwards
bsw/jbe@1309: 	License: http://www.opensource.org/licenses/mit-license.php
bsw/jbe@1309: */
bsw/jbe@1309: 
bsw/jbe@1309: var Base = function() {
bsw/jbe@1309: 	// dummy
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: Base.extend = function(_instance, _static) { // subclass
bsw/jbe@1309: 	var extend = Base.prototype.extend;
bsw/jbe@1309: 	
bsw/jbe@1309: 	// build the prototype
bsw/jbe@1309: 	Base._prototyping = true;
bsw/jbe@1309: 	var proto = new this;
bsw/jbe@1309: 	extend.call(proto, _instance);
bsw/jbe@1309:   proto.base = function() {
bsw/jbe@1309:     // call this method from any other method to invoke that method's ancestor
bsw/jbe@1309:   };
bsw/jbe@1309: 	delete Base._prototyping;
bsw/jbe@1309: 	
bsw/jbe@1309: 	// create the wrapper for the constructor function
bsw/jbe@1309: 	//var constructor = proto.constructor.valueOf(); //-dean
bsw/jbe@1309: 	var constructor = proto.constructor;
bsw/jbe@1309: 	var klass = proto.constructor = function() {
bsw/jbe@1309: 		if (!Base._prototyping) {
bsw/jbe@1309: 			if (this._constructing || this.constructor == klass) { // instantiation
bsw/jbe@1309: 				this._constructing = true;
bsw/jbe@1309: 				constructor.apply(this, arguments);
bsw/jbe@1309: 				delete this._constructing;
bsw/jbe@1309: 			} else if (arguments[0] != null) { // casting
bsw/jbe@1309: 				return (arguments[0].extend || extend).call(arguments[0], proto);
bsw/jbe@1309: 			}
bsw/jbe@1309: 		}
bsw/jbe@1309: 	};
bsw/jbe@1309: 	
bsw/jbe@1309: 	// build the class interface
bsw/jbe@1309: 	klass.ancestor = this;
bsw/jbe@1309: 	klass.extend = this.extend;
bsw/jbe@1309: 	klass.forEach = this.forEach;
bsw/jbe@1309: 	klass.implement = this.implement;
bsw/jbe@1309: 	klass.prototype = proto;
bsw/jbe@1309: 	klass.toString = this.toString;
bsw/jbe@1309: 	klass.valueOf = function(type) {
bsw/jbe@1309: 		//return (type == "object") ? klass : constructor; //-dean
bsw/jbe@1309: 		return (type == "object") ? klass : constructor.valueOf();
bsw/jbe@1309: 	};
bsw/jbe@1309: 	extend.call(klass, _static);
bsw/jbe@1309: 	// class initialisation
bsw/jbe@1309: 	if (typeof klass.init == "function") klass.init();
bsw/jbe@1309: 	return klass;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: Base.prototype = {	
bsw/jbe@1309: 	extend: function(source, value) {
bsw/jbe@1309: 		if (arguments.length > 1) { // extending with a name/value pair
bsw/jbe@1309: 			var ancestor = this[source];
bsw/jbe@1309: 			if (ancestor && (typeof value == "function") && // overriding a method?
bsw/jbe@1309: 				// the valueOf() comparison is to avoid circular references
bsw/jbe@1309: 				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
bsw/jbe@1309: 				/\bbase\b/.test(value)) {
bsw/jbe@1309: 				// get the underlying method
bsw/jbe@1309: 				var method = value.valueOf();
bsw/jbe@1309: 				// override
bsw/jbe@1309: 				value = function() {
bsw/jbe@1309: 					var previous = this.base || Base.prototype.base;
bsw/jbe@1309: 					this.base = ancestor;
bsw/jbe@1309: 					var returnValue = method.apply(this, arguments);
bsw/jbe@1309: 					this.base = previous;
bsw/jbe@1309: 					return returnValue;
bsw/jbe@1309: 				};
bsw/jbe@1309: 				// point to the underlying method
bsw/jbe@1309: 				value.valueOf = function(type) {
bsw/jbe@1309: 					return (type == "object") ? value : method;
bsw/jbe@1309: 				};
bsw/jbe@1309: 				value.toString = Base.toString;
bsw/jbe@1309: 			}
bsw/jbe@1309: 			this[source] = value;
bsw/jbe@1309: 		} else if (source) { // extending with an object literal
bsw/jbe@1309: 			var extend = Base.prototype.extend;
bsw/jbe@1309: 			// if this object has a customised extend method then use it
bsw/jbe@1309: 			if (!Base._prototyping && typeof this != "function") {
bsw/jbe@1309: 				extend = this.extend || extend;
bsw/jbe@1309: 			}
bsw/jbe@1309: 			var proto = {toSource: null};
bsw/jbe@1309: 			// do the "toString" and other methods manually
bsw/jbe@1309: 			var hidden = ["constructor", "toString", "valueOf"];
bsw/jbe@1309: 			// if we are prototyping then include the constructor
bsw/jbe@1309: 			var i = Base._prototyping ? 0 : 1;
bsw/jbe@1309: 			while (key = hidden[i++]) {
bsw/jbe@1309: 				if (source[key] != proto[key]) {
bsw/jbe@1309: 					extend.call(this, key, source[key]);
bsw/jbe@1309: 
bsw/jbe@1309: 				}
bsw/jbe@1309: 			}
bsw/jbe@1309: 			// copy each of the source object's properties to this object
bsw/jbe@1309: 			for (var key in source) {
bsw/jbe@1309: 				if (!proto[key]) extend.call(this, key, source[key]);
bsw/jbe@1309: 			}
bsw/jbe@1309: 		}
bsw/jbe@1309: 		return this;
bsw/jbe@1309: 	}
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: // initialise
bsw/jbe@1309: Base = Base.extend({
bsw/jbe@1309: 	constructor: function() {
bsw/jbe@1309: 		this.extend(arguments[0]);
bsw/jbe@1309: 	}
bsw/jbe@1309: }, {
bsw/jbe@1309: 	ancestor: Object,
bsw/jbe@1309: 	version: "1.1",
bsw/jbe@1309: 	
bsw/jbe@1309: 	forEach: function(object, block, context) {
bsw/jbe@1309: 		for (var key in object) {
bsw/jbe@1309: 			if (this.prototype[key] === undefined) {
bsw/jbe@1309: 				block.call(context, object[key], key, object);
bsw/jbe@1309: 			}
bsw/jbe@1309: 		}
bsw/jbe@1309: 	},
bsw/jbe@1309: 		
bsw/jbe@1309: 	implement: function() {
bsw/jbe@1309: 		for (var i = 0; i < arguments.length; i++) {
bsw/jbe@1309: 			if (typeof arguments[i] == "function") {
bsw/jbe@1309: 				// if it's a function, call it
bsw/jbe@1309: 				arguments[i](this.prototype);
bsw/jbe@1309: 			} else {
bsw/jbe@1309: 				// add the interface using the extend method
bsw/jbe@1309: 				this.prototype.extend(arguments[i]);
bsw/jbe@1309: 			}
bsw/jbe@1309: 		}
bsw/jbe@1309: 		return this;
bsw/jbe@1309: 	},
bsw/jbe@1309: 	
bsw/jbe@1309: 	toString: function() {
bsw/jbe@1309: 		return String(this.valueOf());
bsw/jbe@1309: 	}
bsw/jbe@1309: });
bsw/jbe@1309: /**
bsw/jbe@1309:  * Rangy, a cross-browser JavaScript range and selection library
bsw/jbe@1309:  * https://github.com/timdown/rangy
bsw/jbe@1309:  *
bsw/jbe@1309:  * Copyright 2015, Tim Down
bsw/jbe@1309:  * Licensed under the MIT license.
bsw/jbe@1309:  * Version: 1.3.1-dev
bsw/jbe@1309:  * Build date: 20 May 2015
bsw/jbe@1309:  *
bsw/jbe@1309:  * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
bsw/jbe@1309:  */
bsw/jbe@1309: var rangy;
bsw/jbe@1309: 
bsw/jbe@1309: (function() {
bsw/jbe@1309:     var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
bsw/jbe@1309: 
bsw/jbe@1309:     // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
bsw/jbe@1309:     // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
bsw/jbe@1309:     var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
bsw/jbe@1309:         "commonAncestorContainer"];
bsw/jbe@1309: 
bsw/jbe@1309:     // Minimal set of methods required for DOM Level 2 Range compliance
bsw/jbe@1309:     var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
bsw/jbe@1309:         "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
bsw/jbe@1309:         "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
bsw/jbe@1309: 
bsw/jbe@1309:     var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
bsw/jbe@1309: 
bsw/jbe@1309:     // Subset of TextRange's full set of methods that we're interested in
bsw/jbe@1309:     var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
bsw/jbe@1309:         "setEndPoint", "getBoundingClientRect"];
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // Trio of functions taken from Peter Michaux's article:
bsw/jbe@1309:     // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
bsw/jbe@1309:     function isHostMethod(o, p) {
bsw/jbe@1309:         var t = typeof o[p];
bsw/jbe@1309:         return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function isHostObject(o, p) {
bsw/jbe@1309:         return !!(typeof o[p] == OBJECT && o[p]);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function isHostProperty(o, p) {
bsw/jbe@1309:         return typeof o[p] != UNDEFINED;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Creates a convenience function to save verbose repeated calls to tests functions
bsw/jbe@1309:     function createMultiplePropertyTest(testFunc) {
bsw/jbe@1309:         return function(o, props) {
bsw/jbe@1309:             var i = props.length;
bsw/jbe@1309:             while (i--) {
bsw/jbe@1309:                 if (!testFunc(o, props[i])) {
bsw/jbe@1309:                     return false;
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             return true;
bsw/jbe@1309:         };
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
bsw/jbe@1309:     var areHostMethods = createMultiplePropertyTest(isHostMethod);
bsw/jbe@1309:     var areHostObjects = createMultiplePropertyTest(isHostObject);
bsw/jbe@1309:     var areHostProperties = createMultiplePropertyTest(isHostProperty);
bsw/jbe@1309: 
bsw/jbe@1309:     function isTextRange(range) {
bsw/jbe@1309:         return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function getBody(doc) {
bsw/jbe@1309:         return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var forEach = [].forEach ?
bsw/jbe@1309:         function(arr, func) {
bsw/jbe@1309:             arr.forEach(func);
bsw/jbe@1309:         } :
bsw/jbe@1309:         function(arr, func) {
bsw/jbe@1309:             for (var i = 0, len = arr.length; i < len; ++i) {
bsw/jbe@1309:                 func(arr[i], i);
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:     var modules = {};
bsw/jbe@1309: 
bsw/jbe@1309:     var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
bsw/jbe@1309: 
bsw/jbe@1309:     var util = {
bsw/jbe@1309:         isHostMethod: isHostMethod,
bsw/jbe@1309:         isHostObject: isHostObject,
bsw/jbe@1309:         isHostProperty: isHostProperty,
bsw/jbe@1309:         areHostMethods: areHostMethods,
bsw/jbe@1309:         areHostObjects: areHostObjects,
bsw/jbe@1309:         areHostProperties: areHostProperties,
bsw/jbe@1309:         isTextRange: isTextRange,
bsw/jbe@1309:         getBody: getBody,
bsw/jbe@1309:         forEach: forEach
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     var api = {
bsw/jbe@1309:         version: "1.3.1-dev",
bsw/jbe@1309:         initialized: false,
bsw/jbe@1309:         isBrowser: isBrowser,
bsw/jbe@1309:         supported: true,
bsw/jbe@1309:         util: util,
bsw/jbe@1309:         features: {},
bsw/jbe@1309:         modules: modules,
bsw/jbe@1309:         config: {
bsw/jbe@1309:             alertOnFail: false,
bsw/jbe@1309:             alertOnWarn: false,
bsw/jbe@1309:             preferTextRange: false,
bsw/jbe@1309:             autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
bsw/jbe@1309:         }
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     function consoleLog(msg) {
bsw/jbe@1309:         if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
bsw/jbe@1309:             console.log(msg);
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function alertOrLog(msg, shouldAlert) {
bsw/jbe@1309:         if (isBrowser && shouldAlert) {
bsw/jbe@1309:             alert(msg);
bsw/jbe@1309:         } else  {
bsw/jbe@1309:             consoleLog(msg);
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function fail(reason) {
bsw/jbe@1309:         api.initialized = true;
bsw/jbe@1309:         api.supported = false;
bsw/jbe@1309:         alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     api.fail = fail;
bsw/jbe@1309: 
bsw/jbe@1309:     function warn(msg) {
bsw/jbe@1309:         alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     api.warn = warn;
bsw/jbe@1309: 
bsw/jbe@1309:     // Add utility extend() method
bsw/jbe@1309:     var extend;
bsw/jbe@1309:     if ({}.hasOwnProperty) {
bsw/jbe@1309:         util.extend = extend = function(obj, props, deep) {
bsw/jbe@1309:             var o, p;
bsw/jbe@1309:             for (var i in props) {
bsw/jbe@1309:                 if (props.hasOwnProperty(i)) {
bsw/jbe@1309:                     o = obj[i];
bsw/jbe@1309:                     p = props[i];
bsw/jbe@1309:                     if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
bsw/jbe@1309:                         extend(o, p, true);
bsw/jbe@1309:                     }
bsw/jbe@1309:                     obj[i] = p;
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             // Special case for toString, which does not show up in for...in loops in IE <= 8
bsw/jbe@1309:             if (props.hasOwnProperty("toString")) {
bsw/jbe@1309:                 obj.toString = props.toString;
bsw/jbe@1309:             }
bsw/jbe@1309:             return obj;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         util.createOptions = function(optionsParam, defaults) {
bsw/jbe@1309:             var options = {};
bsw/jbe@1309:             extend(options, defaults);
bsw/jbe@1309:             if (optionsParam) {
bsw/jbe@1309:                 extend(options, optionsParam);
bsw/jbe@1309:             }
bsw/jbe@1309:             return options;
bsw/jbe@1309:         };
bsw/jbe@1309:     } else {
bsw/jbe@1309:         fail("hasOwnProperty not supported");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Test whether we're in a browser and bail out if not
bsw/jbe@1309:     if (!isBrowser) {
bsw/jbe@1309:         fail("Rangy can only run in a browser");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
bsw/jbe@1309:     (function() {
bsw/jbe@1309:         var toArray;
bsw/jbe@1309: 
bsw/jbe@1309:         if (isBrowser) {
bsw/jbe@1309:             var el = document.createElement("div");
bsw/jbe@1309:             el.appendChild(document.createElement("span"));
bsw/jbe@1309:             var slice = [].slice;
bsw/jbe@1309:             try {
bsw/jbe@1309:                 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
bsw/jbe@1309:                     toArray = function(arrayLike) {
bsw/jbe@1309:                         return slice.call(arrayLike, 0);
bsw/jbe@1309:                     };
bsw/jbe@1309:                 }
bsw/jbe@1309:             } catch (e) {}
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (!toArray) {
bsw/jbe@1309:             toArray = function(arrayLike) {
bsw/jbe@1309:                 var arr = [];
bsw/jbe@1309:                 for (var i = 0, len = arrayLike.length; i < len; ++i) {
bsw/jbe@1309:                     arr[i] = arrayLike[i];
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return arr;
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         util.toArray = toArray;
bsw/jbe@1309:     })();
bsw/jbe@1309: 
bsw/jbe@1309:     // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
bsw/jbe@1309:     // normalization of event properties
bsw/jbe@1309:     var addListener;
bsw/jbe@1309:     if (isBrowser) {
bsw/jbe@1309:         if (isHostMethod(document, "addEventListener")) {
bsw/jbe@1309:             addListener = function(obj, eventType, listener) {
bsw/jbe@1309:                 obj.addEventListener(eventType, listener, false);
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (isHostMethod(document, "attachEvent")) {
bsw/jbe@1309:             addListener = function(obj, eventType, listener) {
bsw/jbe@1309:                 obj.attachEvent("on" + eventType, listener);
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             fail("Document does not have required addEventListener or attachEvent method");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         util.addListener = addListener;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var initListeners = [];
bsw/jbe@1309: 
bsw/jbe@1309:     function getErrorDesc(ex) {
bsw/jbe@1309:         return ex.message || ex.description || String(ex);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Initialization
bsw/jbe@1309:     function init() {
bsw/jbe@1309:         if (!isBrowser || api.initialized) {
bsw/jbe@1309:             return;
bsw/jbe@1309:         }
bsw/jbe@1309:         var testRange;
bsw/jbe@1309:         var implementsDomRange = false, implementsTextRange = false;
bsw/jbe@1309: 
bsw/jbe@1309:         // First, perform basic feature tests
bsw/jbe@1309: 
bsw/jbe@1309:         if (isHostMethod(document, "createRange")) {
bsw/jbe@1309:             testRange = document.createRange();
bsw/jbe@1309:             if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
bsw/jbe@1309:                 implementsDomRange = true;
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var body = getBody(document);
bsw/jbe@1309:         if (!body || body.nodeName.toLowerCase() != "body") {
bsw/jbe@1309:             fail("No body element found");
bsw/jbe@1309:             return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (body && isHostMethod(body, "createTextRange")) {
bsw/jbe@1309:             testRange = body.createTextRange();
bsw/jbe@1309:             if (isTextRange(testRange)) {
bsw/jbe@1309:                 implementsTextRange = true;
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (!implementsDomRange && !implementsTextRange) {
bsw/jbe@1309:             fail("Neither Range nor TextRange are available");
bsw/jbe@1309:             return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         api.initialized = true;
bsw/jbe@1309:         api.features = {
bsw/jbe@1309:             implementsDomRange: implementsDomRange,
bsw/jbe@1309:             implementsTextRange: implementsTextRange
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         // Initialize modules
bsw/jbe@1309:         var module, errorMessage;
bsw/jbe@1309:         for (var moduleName in modules) {
bsw/jbe@1309:             if ( (module = modules[moduleName]) instanceof Module ) {
bsw/jbe@1309:                 module.init(module, api);
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Call init listeners
bsw/jbe@1309:         for (var i = 0, len = initListeners.length; i < len; ++i) {
bsw/jbe@1309:             try {
bsw/jbe@1309:                 initListeners[i](api);
bsw/jbe@1309:             } catch (ex) {
bsw/jbe@1309:                 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
bsw/jbe@1309:                 consoleLog(errorMessage);
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function deprecationNotice(deprecated, replacement, module) {
bsw/jbe@1309:         if (module) {
bsw/jbe@1309:             deprecated += " in module " + module.name;
bsw/jbe@1309:         }
bsw/jbe@1309:         api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
bsw/jbe@1309:         replacement + " instead.");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
bsw/jbe@1309:         owner[deprecated] = function() {
bsw/jbe@1309:             deprecationNotice(deprecated, replacement, module);
bsw/jbe@1309:             return owner[replacement].apply(owner, util.toArray(arguments));
bsw/jbe@1309:         };
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     util.deprecationNotice = deprecationNotice;
bsw/jbe@1309:     util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
bsw/jbe@1309: 
bsw/jbe@1309:     // Allow external scripts to initialize this library in case it's loaded after the document has loaded
bsw/jbe@1309:     api.init = init;
bsw/jbe@1309: 
bsw/jbe@1309:     // Execute listener immediately if already initialized
bsw/jbe@1309:     api.addInitListener = function(listener) {
bsw/jbe@1309:         if (api.initialized) {
bsw/jbe@1309:             listener(api);
bsw/jbe@1309:         } else {
bsw/jbe@1309:             initListeners.push(listener);
bsw/jbe@1309:         }
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     var shimListeners = [];
bsw/jbe@1309: 
bsw/jbe@1309:     api.addShimListener = function(listener) {
bsw/jbe@1309:         shimListeners.push(listener);
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     function shim(win) {
bsw/jbe@1309:         win = win || window;
bsw/jbe@1309:         init();
bsw/jbe@1309: 
bsw/jbe@1309:         // Notify listeners
bsw/jbe@1309:         for (var i = 0, len = shimListeners.length; i < len; ++i) {
bsw/jbe@1309:             shimListeners[i](win);
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (isBrowser) {
bsw/jbe@1309:         api.shim = api.createMissingNativeApi = shim;
bsw/jbe@1309:         createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function Module(name, dependencies, initializer) {
bsw/jbe@1309:         this.name = name;
bsw/jbe@1309:         this.dependencies = dependencies;
bsw/jbe@1309:         this.initialized = false;
bsw/jbe@1309:         this.supported = false;
bsw/jbe@1309:         this.initializer = initializer;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     Module.prototype = {
bsw/jbe@1309:         init: function() {
bsw/jbe@1309:             var requiredModuleNames = this.dependencies || [];
bsw/jbe@1309:             for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
bsw/jbe@1309:                 moduleName = requiredModuleNames[i];
bsw/jbe@1309: 
bsw/jbe@1309:                 requiredModule = modules[moduleName];
bsw/jbe@1309:                 if (!requiredModule || !(requiredModule instanceof Module)) {
bsw/jbe@1309:                     throw new Error("required module '" + moduleName + "' not found");
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 requiredModule.init();
bsw/jbe@1309: 
bsw/jbe@1309:                 if (!requiredModule.supported) {
bsw/jbe@1309:                     throw new Error("required module '" + moduleName + "' not supported");
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // Now run initializer
bsw/jbe@1309:             this.initializer(this);
bsw/jbe@1309:         },
bsw/jbe@1309: 
bsw/jbe@1309:         fail: function(reason) {
bsw/jbe@1309:             this.initialized = true;
bsw/jbe@1309:             this.supported = false;
bsw/jbe@1309:             throw new Error(reason);
bsw/jbe@1309:         },
bsw/jbe@1309: 
bsw/jbe@1309:         warn: function(msg) {
bsw/jbe@1309:             api.warn("Module " + this.name + ": " + msg);
bsw/jbe@1309:         },
bsw/jbe@1309: 
bsw/jbe@1309:         deprecationNotice: function(deprecated, replacement) {
bsw/jbe@1309:             api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
bsw/jbe@1309:                 replacement + " instead");
bsw/jbe@1309:         },
bsw/jbe@1309: 
bsw/jbe@1309:         createError: function(msg) {
bsw/jbe@1309:             return new Error("Error in Rangy " + this.name + " module: " + msg);
bsw/jbe@1309:         }
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     function createModule(name, dependencies, initFunc) {
bsw/jbe@1309:         var newModule = new Module(name, dependencies, function(module) {
bsw/jbe@1309:             if (!module.initialized) {
bsw/jbe@1309:                 module.initialized = true;
bsw/jbe@1309:                 try {
bsw/jbe@1309:                     initFunc(api, module);
bsw/jbe@1309:                     module.supported = true;
bsw/jbe@1309:                 } catch (ex) {
bsw/jbe@1309:                     var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
bsw/jbe@1309:                     consoleLog(errorMessage);
bsw/jbe@1309:                     if (ex.stack) {
bsw/jbe@1309:                         consoleLog(ex.stack);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         });
bsw/jbe@1309:         modules[name] = newModule;
bsw/jbe@1309:         return newModule;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     api.createModule = function(name) {
bsw/jbe@1309:         // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
bsw/jbe@1309:         var initFunc, dependencies;
bsw/jbe@1309:         if (arguments.length == 2) {
bsw/jbe@1309:             initFunc = arguments[1];
bsw/jbe@1309:             dependencies = [];
bsw/jbe@1309:         } else {
bsw/jbe@1309:             initFunc = arguments[2];
bsw/jbe@1309:             dependencies = arguments[1];
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var module = createModule(name, dependencies, initFunc);
bsw/jbe@1309: 
bsw/jbe@1309:         // Initialize the module immediately if the core is already initialized
bsw/jbe@1309:         if (api.initialized && api.supported) {
bsw/jbe@1309:             module.init();
bsw/jbe@1309:         }
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     api.createCoreModule = function(name, dependencies, initFunc) {
bsw/jbe@1309:         createModule(name, dependencies, initFunc);
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
bsw/jbe@1309: 
bsw/jbe@1309:     function RangePrototype() {}
bsw/jbe@1309:     api.RangePrototype = RangePrototype;
bsw/jbe@1309:     api.rangePrototype = new RangePrototype();
bsw/jbe@1309: 
bsw/jbe@1309:     function SelectionPrototype() {}
bsw/jbe@1309:     api.selectionPrototype = new SelectionPrototype();
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // DOM utility methods used by Rangy
bsw/jbe@1309:     api.createCoreModule("DomUtil", [], function(api, module) {
bsw/jbe@1309:         var UNDEF = "undefined";
bsw/jbe@1309:         var util = api.util;
bsw/jbe@1309:         var getBody = util.getBody;
bsw/jbe@1309: 
bsw/jbe@1309:         // Perform feature tests
bsw/jbe@1309:         if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
bsw/jbe@1309:             module.fail("document missing a Node creation method");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (!util.isHostMethod(document, "getElementsByTagName")) {
bsw/jbe@1309:             module.fail("document missing getElementsByTagName method");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var el = document.createElement("div");
bsw/jbe@1309:         if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
bsw/jbe@1309:                 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
bsw/jbe@1309:             module.fail("Incomplete Element implementation");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // innerHTML is required for Range's createContextualFragment method
bsw/jbe@1309:         if (!util.isHostProperty(el, "innerHTML")) {
bsw/jbe@1309:             module.fail("Element is missing innerHTML property");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var textNode = document.createTextNode("test");
bsw/jbe@1309:         if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
bsw/jbe@1309:                 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
bsw/jbe@1309:                 !util.areHostProperties(textNode, ["data"]))) {
bsw/jbe@1309:             module.fail("Incomplete Text Node implementation");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
bsw/jbe@1309:         // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
bsw/jbe@1309:         // contains just the document as a single element and the value searched for is the document.
bsw/jbe@1309:         var arrayContains = /*Array.prototype.indexOf ?
bsw/jbe@1309:             function(arr, val) {
bsw/jbe@1309:                 return arr.indexOf(val) > -1;
bsw/jbe@1309:             }:*/
bsw/jbe@1309: 
bsw/jbe@1309:             function(arr, val) {
bsw/jbe@1309:                 var i = arr.length;
bsw/jbe@1309:                 while (i--) {
bsw/jbe@1309:                     if (arr[i] === val) {
bsw/jbe@1309:                         return true;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return false;
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:         // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
bsw/jbe@1309:         function isHtmlNamespace(node) {
bsw/jbe@1309:             var ns;
bsw/jbe@1309:             return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function parentElement(node) {
bsw/jbe@1309:             var parent = node.parentNode;
bsw/jbe@1309:             return (parent.nodeType == 1) ? parent : null;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getNodeIndex(node) {
bsw/jbe@1309:             var i = 0;
bsw/jbe@1309:             while( (node = node.previousSibling) ) {
bsw/jbe@1309:                 ++i;
bsw/jbe@1309:             }
bsw/jbe@1309:             return i;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getNodeLength(node) {
bsw/jbe@1309:             switch (node.nodeType) {
bsw/jbe@1309:                 case 7:
bsw/jbe@1309:                 case 10:
bsw/jbe@1309:                     return 0;
bsw/jbe@1309:                 case 3:
bsw/jbe@1309:                 case 8:
bsw/jbe@1309:                     return node.length;
bsw/jbe@1309:                 default:
bsw/jbe@1309:                     return node.childNodes.length;
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getCommonAncestor(node1, node2) {
bsw/jbe@1309:             var ancestors = [], n;
bsw/jbe@1309:             for (n = node1; n; n = n.parentNode) {
bsw/jbe@1309:                 ancestors.push(n);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             for (n = node2; n; n = n.parentNode) {
bsw/jbe@1309:                 if (arrayContains(ancestors, n)) {
bsw/jbe@1309:                     return n;
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             return null;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function isAncestorOf(ancestor, descendant, selfIsAncestor) {
bsw/jbe@1309:             var n = selfIsAncestor ? descendant : descendant.parentNode;
bsw/jbe@1309:             while (n) {
bsw/jbe@1309:                 if (n === ancestor) {
bsw/jbe@1309:                     return true;
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     n = n.parentNode;
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function isOrIsAncestorOf(ancestor, descendant) {
bsw/jbe@1309:             return isAncestorOf(ancestor, descendant, true);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
bsw/jbe@1309:             var p, n = selfIsAncestor ? node : node.parentNode;
bsw/jbe@1309:             while (n) {
bsw/jbe@1309:                 p = n.parentNode;
bsw/jbe@1309:                 if (p === ancestor) {
bsw/jbe@1309:                     return n;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 n = p;
bsw/jbe@1309:             }
bsw/jbe@1309:             return null;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function isCharacterDataNode(node) {
bsw/jbe@1309:             var t = node.nodeType;
bsw/jbe@1309:             return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function isTextOrCommentNode(node) {
bsw/jbe@1309:             if (!node) {
bsw/jbe@1309:                 return false;
bsw/jbe@1309:             }
bsw/jbe@1309:             var t = node.nodeType;
bsw/jbe@1309:             return t == 3 || t == 8 ; // Text or Comment
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function insertAfter(node, precedingNode) {
bsw/jbe@1309:             var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
bsw/jbe@1309:             if (nextNode) {
bsw/jbe@1309:                 parent.insertBefore(node, nextNode);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 parent.appendChild(node);
bsw/jbe@1309:             }
bsw/jbe@1309:             return node;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Note that we cannot use splitText() because it is bugridden in IE 9.
bsw/jbe@1309:         function splitDataNode(node, index, positionsToPreserve) {
bsw/jbe@1309:             var newNode = node.cloneNode(false);
bsw/jbe@1309:             newNode.deleteData(0, index);
bsw/jbe@1309:             node.deleteData(index, node.length - index);
bsw/jbe@1309:             insertAfter(newNode, node);
bsw/jbe@1309: 
bsw/jbe@1309:             // Preserve positions
bsw/jbe@1309:             if (positionsToPreserve) {
bsw/jbe@1309:                 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
bsw/jbe@1309:                     // Handle case where position was inside the portion of node after the split point
bsw/jbe@1309:                     if (position.node == node && position.offset > index) {
bsw/jbe@1309:                         position.node = newNode;
bsw/jbe@1309:                         position.offset -= index;
bsw/jbe@1309:                     }
bsw/jbe@1309:                     // Handle the case where the position is a node offset within node's parent
bsw/jbe@1309:                     else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
bsw/jbe@1309:                         ++position.offset;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             return newNode;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getDocument(node) {
bsw/jbe@1309:             if (node.nodeType == 9) {
bsw/jbe@1309:                 return node;
bsw/jbe@1309:             } else if (typeof node.ownerDocument != UNDEF) {
bsw/jbe@1309:                 return node.ownerDocument;
bsw/jbe@1309:             } else if (typeof node.document != UNDEF) {
bsw/jbe@1309:                 return node.document;
bsw/jbe@1309:             } else if (node.parentNode) {
bsw/jbe@1309:                 return getDocument(node.parentNode);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 throw module.createError("getDocument: no document found for node");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getWindow(node) {
bsw/jbe@1309:             var doc = getDocument(node);
bsw/jbe@1309:             if (typeof doc.defaultView != UNDEF) {
bsw/jbe@1309:                 return doc.defaultView;
bsw/jbe@1309:             } else if (typeof doc.parentWindow != UNDEF) {
bsw/jbe@1309:                 return doc.parentWindow;
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 throw module.createError("Cannot get a window object for node");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getIframeDocument(iframeEl) {
bsw/jbe@1309:             if (typeof iframeEl.contentDocument != UNDEF) {
bsw/jbe@1309:                 return iframeEl.contentDocument;
bsw/jbe@1309:             } else if (typeof iframeEl.contentWindow != UNDEF) {
bsw/jbe@1309:                 return iframeEl.contentWindow.document;
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 throw module.createError("getIframeDocument: No Document object found for iframe element");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getIframeWindow(iframeEl) {
bsw/jbe@1309:             if (typeof iframeEl.contentWindow != UNDEF) {
bsw/jbe@1309:                 return iframeEl.contentWindow;
bsw/jbe@1309:             } else if (typeof iframeEl.contentDocument != UNDEF) {
bsw/jbe@1309:                 return iframeEl.contentDocument.defaultView;
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 throw module.createError("getIframeWindow: No Window object found for iframe element");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // This looks bad. Is it worth it?
bsw/jbe@1309:         function isWindow(obj) {
bsw/jbe@1309:             return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getContentDocument(obj, module, methodName) {
bsw/jbe@1309:             var doc;
bsw/jbe@1309: 
bsw/jbe@1309:             if (!obj) {
bsw/jbe@1309:                 doc = document;
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // Test if a DOM node has been passed and obtain a document object for it if so
bsw/jbe@1309:             else if (util.isHostProperty(obj, "nodeType")) {
bsw/jbe@1309:                 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
bsw/jbe@1309:                     getIframeDocument(obj) : getDocument(obj);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // Test if the doc parameter appears to be a Window object
bsw/jbe@1309:             else if (isWindow(obj)) {
bsw/jbe@1309:                 doc = obj.document;
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (!doc) {
bsw/jbe@1309:                 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             return doc;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getRootContainer(node) {
bsw/jbe@1309:             var parent;
bsw/jbe@1309:             while ( (parent = node.parentNode) ) {
bsw/jbe@1309:                 node = parent;
bsw/jbe@1309:             }
bsw/jbe@1309:             return node;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function comparePoints(nodeA, offsetA, nodeB, offsetB) {
bsw/jbe@1309:             // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
bsw/jbe@1309:             var nodeC, root, childA, childB, n;
bsw/jbe@1309:             if (nodeA == nodeB) {
bsw/jbe@1309:                 // Case 1: nodes are the same
bsw/jbe@1309:                 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
bsw/jbe@1309:             } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
bsw/jbe@1309:                 // Case 2: node C (container B or an ancestor) is a child node of A
bsw/jbe@1309:                 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
bsw/jbe@1309:             } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
bsw/jbe@1309:                 // Case 3: node C (container A or an ancestor) is a child node of B
bsw/jbe@1309:                 return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 root = getCommonAncestor(nodeA, nodeB);
bsw/jbe@1309:                 if (!root) {
bsw/jbe@1309:                     throw new Error("comparePoints error: nodes have no common ancestor");
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Case 4: containers are siblings or descendants of siblings
bsw/jbe@1309:                 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
bsw/jbe@1309:                 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (childA === childB) {
bsw/jbe@1309:                     // This shouldn't be possible
bsw/jbe@1309:                     throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     n = root.firstChild;
bsw/jbe@1309:                     while (n) {
bsw/jbe@1309:                         if (n === childA) {
bsw/jbe@1309:                             return -1;
bsw/jbe@1309:                         } else if (n === childB) {
bsw/jbe@1309:                             return 1;
bsw/jbe@1309:                         }
bsw/jbe@1309:                         n = n.nextSibling;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
bsw/jbe@1309:         var crashyTextNodes = false;
bsw/jbe@1309: 
bsw/jbe@1309:         function isBrokenNode(node) {
bsw/jbe@1309:             var n;
bsw/jbe@1309:             try {
bsw/jbe@1309:                 n = node.parentNode;
bsw/jbe@1309:                 return false;
bsw/jbe@1309:             } catch (e) {
bsw/jbe@1309:                 return true;
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         (function() {
bsw/jbe@1309:             var el = document.createElement("b");
bsw/jbe@1309:             el.innerHTML = "1";
bsw/jbe@1309:             var textNode = el.firstChild;
bsw/jbe@1309:             el.innerHTML = "
";
bsw/jbe@1309:             crashyTextNodes = isBrokenNode(textNode);
bsw/jbe@1309: 
bsw/jbe@1309:             api.features.crashyTextNodes = crashyTextNodes;
bsw/jbe@1309:         })();
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         function inspectNode(node) {
bsw/jbe@1309:             if (!node) {
bsw/jbe@1309:                 return "[No node]";
bsw/jbe@1309:             }
bsw/jbe@1309:             if (crashyTextNodes && isBrokenNode(node)) {
bsw/jbe@1309:                 return "[Broken node]";
bsw/jbe@1309:             }
bsw/jbe@1309:             if (isCharacterDataNode(node)) {
bsw/jbe@1309:                 return '"' + node.data + '"';
bsw/jbe@1309:             }
bsw/jbe@1309:             if (node.nodeType == 1) {
bsw/jbe@1309:                 var idAttr = node.id ? ' id="' + node.id + '"' : "";
bsw/jbe@1309:                 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
bsw/jbe@1309:             }
bsw/jbe@1309:             return node.nodeName;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function fragmentFromNodeChildren(node) {
bsw/jbe@1309:             var fragment = getDocument(node).createDocumentFragment(), child;
bsw/jbe@1309:             while ( (child = node.firstChild) ) {
bsw/jbe@1309:                 fragment.appendChild(child);
bsw/jbe@1309:             }
bsw/jbe@1309:             return fragment;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var getComputedStyleProperty;
bsw/jbe@1309:         if (typeof window.getComputedStyle != UNDEF) {
bsw/jbe@1309:             getComputedStyleProperty = function(el, propName) {
bsw/jbe@1309:                 return getWindow(el).getComputedStyle(el, null)[propName];
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (typeof document.documentElement.currentStyle != UNDEF) {
bsw/jbe@1309:             getComputedStyleProperty = function(el, propName) {
bsw/jbe@1309:                 return el.currentStyle ? el.currentStyle[propName] : "";
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             module.fail("No means of obtaining computed style properties found");
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function createTestElement(doc, html, contentEditable) {
bsw/jbe@1309:             var body = getBody(doc);
bsw/jbe@1309:             var el = doc.createElement("div");
bsw/jbe@1309:             el.contentEditable = "" + !!contentEditable;
bsw/jbe@1309:             if (html) {
bsw/jbe@1309:                 el.innerHTML = html;
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
bsw/jbe@1309:             var bodyFirstChild = body.firstChild;
bsw/jbe@1309:             if (bodyFirstChild) {
bsw/jbe@1309:                 body.insertBefore(el, bodyFirstChild);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 body.appendChild(el);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             return el;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function removeNode(node) {
bsw/jbe@1309:             return node.parentNode.removeChild(node);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function NodeIterator(root) {
bsw/jbe@1309:             this.root = root;
bsw/jbe@1309:             this._next = root;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         NodeIterator.prototype = {
bsw/jbe@1309:             _current: null,
bsw/jbe@1309: 
bsw/jbe@1309:             hasNext: function() {
bsw/jbe@1309:                 return !!this._next;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             next: function() {
bsw/jbe@1309:                 var n = this._current = this._next;
bsw/jbe@1309:                 var child, next;
bsw/jbe@1309:                 if (this._current) {
bsw/jbe@1309:                     child = n.firstChild;
bsw/jbe@1309:                     if (child) {
bsw/jbe@1309:                         this._next = child;
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         next = null;
bsw/jbe@1309:                         while ((n !== this.root) && !(next = n.nextSibling)) {
bsw/jbe@1309:                             n = n.parentNode;
bsw/jbe@1309:                         }
bsw/jbe@1309:                         this._next = next;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return this._current;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             detach: function() {
bsw/jbe@1309:                 this._current = this._next = this.root = null;
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         function createIterator(root) {
bsw/jbe@1309:             return new NodeIterator(root);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function DomPosition(node, offset) {
bsw/jbe@1309:             this.node = node;
bsw/jbe@1309:             this.offset = offset;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         DomPosition.prototype = {
bsw/jbe@1309:             equals: function(pos) {
bsw/jbe@1309:                 return !!pos && this.node === pos.node && this.offset == pos.offset;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             inspect: function() {
bsw/jbe@1309:                 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             toString: function() {
bsw/jbe@1309:                 return this.inspect();
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         function DOMException(codeName) {
bsw/jbe@1309:             this.code = this[codeName];
bsw/jbe@1309:             this.codeName = codeName;
bsw/jbe@1309:             this.message = "DOMException: " + this.codeName;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         DOMException.prototype = {
bsw/jbe@1309:             INDEX_SIZE_ERR: 1,
bsw/jbe@1309:             HIERARCHY_REQUEST_ERR: 3,
bsw/jbe@1309:             WRONG_DOCUMENT_ERR: 4,
bsw/jbe@1309:             NO_MODIFICATION_ALLOWED_ERR: 7,
bsw/jbe@1309:             NOT_FOUND_ERR: 8,
bsw/jbe@1309:             NOT_SUPPORTED_ERR: 9,
bsw/jbe@1309:             INVALID_STATE_ERR: 11,
bsw/jbe@1309:             INVALID_NODE_TYPE_ERR: 24
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         DOMException.prototype.toString = function() {
bsw/jbe@1309:             return this.message;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         api.dom = {
bsw/jbe@1309:             arrayContains: arrayContains,
bsw/jbe@1309:             isHtmlNamespace: isHtmlNamespace,
bsw/jbe@1309:             parentElement: parentElement,
bsw/jbe@1309:             getNodeIndex: getNodeIndex,
bsw/jbe@1309:             getNodeLength: getNodeLength,
bsw/jbe@1309:             getCommonAncestor: getCommonAncestor,
bsw/jbe@1309:             isAncestorOf: isAncestorOf,
bsw/jbe@1309:             isOrIsAncestorOf: isOrIsAncestorOf,
bsw/jbe@1309:             getClosestAncestorIn: getClosestAncestorIn,
bsw/jbe@1309:             isCharacterDataNode: isCharacterDataNode,
bsw/jbe@1309:             isTextOrCommentNode: isTextOrCommentNode,
bsw/jbe@1309:             insertAfter: insertAfter,
bsw/jbe@1309:             splitDataNode: splitDataNode,
bsw/jbe@1309:             getDocument: getDocument,
bsw/jbe@1309:             getWindow: getWindow,
bsw/jbe@1309:             getIframeWindow: getIframeWindow,
bsw/jbe@1309:             getIframeDocument: getIframeDocument,
bsw/jbe@1309:             getBody: getBody,
bsw/jbe@1309:             isWindow: isWindow,
bsw/jbe@1309:             getContentDocument: getContentDocument,
bsw/jbe@1309:             getRootContainer: getRootContainer,
bsw/jbe@1309:             comparePoints: comparePoints,
bsw/jbe@1309:             isBrokenNode: isBrokenNode,
bsw/jbe@1309:             inspectNode: inspectNode,
bsw/jbe@1309:             getComputedStyleProperty: getComputedStyleProperty,
bsw/jbe@1309:             createTestElement: createTestElement,
bsw/jbe@1309:             removeNode: removeNode,
bsw/jbe@1309:             fragmentFromNodeChildren: fragmentFromNodeChildren,
bsw/jbe@1309:             createIterator: createIterator,
bsw/jbe@1309:             DomPosition: DomPosition
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         api.DOMException = DOMException;
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // Pure JavaScript implementation of DOM Range
bsw/jbe@1309:     api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
bsw/jbe@1309:         var dom = api.dom;
bsw/jbe@1309:         var util = api.util;
bsw/jbe@1309:         var DomPosition = dom.DomPosition;
bsw/jbe@1309:         var DOMException = api.DOMException;
bsw/jbe@1309: 
bsw/jbe@1309:         var isCharacterDataNode = dom.isCharacterDataNode;
bsw/jbe@1309:         var getNodeIndex = dom.getNodeIndex;
bsw/jbe@1309:         var isOrIsAncestorOf = dom.isOrIsAncestorOf;
bsw/jbe@1309:         var getDocument = dom.getDocument;
bsw/jbe@1309:         var comparePoints = dom.comparePoints;
bsw/jbe@1309:         var splitDataNode = dom.splitDataNode;
bsw/jbe@1309:         var getClosestAncestorIn = dom.getClosestAncestorIn;
bsw/jbe@1309:         var getNodeLength = dom.getNodeLength;
bsw/jbe@1309:         var arrayContains = dom.arrayContains;
bsw/jbe@1309:         var getRootContainer = dom.getRootContainer;
bsw/jbe@1309:         var crashyTextNodes = api.features.crashyTextNodes;
bsw/jbe@1309: 
bsw/jbe@1309:         var removeNode = dom.removeNode;
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         // Utility functions
bsw/jbe@1309: 
bsw/jbe@1309:         function isNonTextPartiallySelected(node, range) {
bsw/jbe@1309:             return (node.nodeType != 3) &&
bsw/jbe@1309:                    (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getRangeDocument(range) {
bsw/jbe@1309:             return range.document || getDocument(range.startContainer);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getRangeRoot(range) {
bsw/jbe@1309:             return getRootContainer(range.startContainer);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getBoundaryBeforeNode(node) {
bsw/jbe@1309:             return new DomPosition(node.parentNode, getNodeIndex(node));
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getBoundaryAfterNode(node) {
bsw/jbe@1309:             return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function insertNodeAtPosition(node, n, o) {
bsw/jbe@1309:             var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
bsw/jbe@1309:             if (isCharacterDataNode(n)) {
bsw/jbe@1309:                 if (o == n.length) {
bsw/jbe@1309:                     dom.insertAfter(node, n);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
bsw/jbe@1309:                 }
bsw/jbe@1309:             } else if (o >= n.childNodes.length) {
bsw/jbe@1309:                 n.appendChild(node);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 n.insertBefore(node, n.childNodes[o]);
bsw/jbe@1309:             }
bsw/jbe@1309:             return firstNodeInserted;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
bsw/jbe@1309:             assertRangeValid(rangeA);
bsw/jbe@1309:             assertRangeValid(rangeB);
bsw/jbe@1309: 
bsw/jbe@1309:             if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
bsw/jbe@1309:                 throw new DOMException("WRONG_DOCUMENT_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
bsw/jbe@1309:                 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
bsw/jbe@1309: 
bsw/jbe@1309:             return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function cloneSubtree(iterator) {
bsw/jbe@1309:             var partiallySelected;
bsw/jbe@1309:             for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
bsw/jbe@1309:                 partiallySelected = iterator.isPartiallySelectedSubtree();
bsw/jbe@1309:                 node = node.cloneNode(!partiallySelected);
bsw/jbe@1309:                 if (partiallySelected) {
bsw/jbe@1309:                     subIterator = iterator.getSubtreeIterator();
bsw/jbe@1309:                     node.appendChild(cloneSubtree(subIterator));
bsw/jbe@1309:                     subIterator.detach();
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 if (node.nodeType == 10) { // DocumentType
bsw/jbe@1309:                     throw new DOMException("HIERARCHY_REQUEST_ERR");
bsw/jbe@1309:                 }
bsw/jbe@1309:                 frag.appendChild(node);
bsw/jbe@1309:             }
bsw/jbe@1309:             return frag;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function iterateSubtree(rangeIterator, func, iteratorState) {
bsw/jbe@1309:             var it, n;
bsw/jbe@1309:             iteratorState = iteratorState || { stop: false };
bsw/jbe@1309:             for (var node, subRangeIterator; node = rangeIterator.next(); ) {
bsw/jbe@1309:                 if (rangeIterator.isPartiallySelectedSubtree()) {
bsw/jbe@1309:                     if (func(node) === false) {
bsw/jbe@1309:                         iteratorState.stop = true;
bsw/jbe@1309:                         return;
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
bsw/jbe@1309:                         // the node selected by the Range.
bsw/jbe@1309:                         subRangeIterator = rangeIterator.getSubtreeIterator();
bsw/jbe@1309:                         iterateSubtree(subRangeIterator, func, iteratorState);
bsw/jbe@1309:                         subRangeIterator.detach();
bsw/jbe@1309:                         if (iteratorState.stop) {
bsw/jbe@1309:                             return;
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
bsw/jbe@1309:                     // descendants
bsw/jbe@1309:                     it = dom.createIterator(node);
bsw/jbe@1309:                     while ( (n = it.next()) ) {
bsw/jbe@1309:                         if (func(n) === false) {
bsw/jbe@1309:                             iteratorState.stop = true;
bsw/jbe@1309:                             return;
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function deleteSubtree(iterator) {
bsw/jbe@1309:             var subIterator;
bsw/jbe@1309:             while (iterator.next()) {
bsw/jbe@1309:                 if (iterator.isPartiallySelectedSubtree()) {
bsw/jbe@1309:                     subIterator = iterator.getSubtreeIterator();
bsw/jbe@1309:                     deleteSubtree(subIterator);
bsw/jbe@1309:                     subIterator.detach();
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     iterator.remove();
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function extractSubtree(iterator) {
bsw/jbe@1309:             for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
bsw/jbe@1309: 
bsw/jbe@1309:                 if (iterator.isPartiallySelectedSubtree()) {
bsw/jbe@1309:                     node = node.cloneNode(false);
bsw/jbe@1309:                     subIterator = iterator.getSubtreeIterator();
bsw/jbe@1309:                     node.appendChild(extractSubtree(subIterator));
bsw/jbe@1309:                     subIterator.detach();
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     iterator.remove();
bsw/jbe@1309:                 }
bsw/jbe@1309:                 if (node.nodeType == 10) { // DocumentType
bsw/jbe@1309:                     throw new DOMException("HIERARCHY_REQUEST_ERR");
bsw/jbe@1309:                 }
bsw/jbe@1309:                 frag.appendChild(node);
bsw/jbe@1309:             }
bsw/jbe@1309:             return frag;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getNodesInRange(range, nodeTypes, filter) {
bsw/jbe@1309:             var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
bsw/jbe@1309:             var filterExists = !!filter;
bsw/jbe@1309:             if (filterNodeTypes) {
bsw/jbe@1309:                 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             var nodes = [];
bsw/jbe@1309:             iterateSubtree(new RangeIterator(range, false), function(node) {
bsw/jbe@1309:                 if (filterNodeTypes && !regex.test(node.nodeType)) {
bsw/jbe@1309:                     return;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 if (filterExists && !filter(node)) {
bsw/jbe@1309:                     return;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 // Don't include a boundary container if it is a character data node and the range does not contain any
bsw/jbe@1309:                 // of its character data. See issue 190.
bsw/jbe@1309:                 var sc = range.startContainer;
bsw/jbe@1309:                 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
bsw/jbe@1309:                     return;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 var ec = range.endContainer;
bsw/jbe@1309:                 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
bsw/jbe@1309:                     return;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 nodes.push(node);
bsw/jbe@1309:             });
bsw/jbe@1309:             return nodes;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function inspect(range) {
bsw/jbe@1309:             var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
bsw/jbe@1309:             return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
bsw/jbe@1309:                     dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
bsw/jbe@1309: 
bsw/jbe@1309:         function RangeIterator(range, clonePartiallySelectedTextNodes) {
bsw/jbe@1309:             this.range = range;
bsw/jbe@1309:             this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:             if (!range.collapsed) {
bsw/jbe@1309:                 this.sc = range.startContainer;
bsw/jbe@1309:                 this.so = range.startOffset;
bsw/jbe@1309:                 this.ec = range.endContainer;
bsw/jbe@1309:                 this.eo = range.endOffset;
bsw/jbe@1309:                 var root = range.commonAncestorContainer;
bsw/jbe@1309: 
bsw/jbe@1309:                 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
bsw/jbe@1309:                     this.isSingleCharacterDataNode = true;
bsw/jbe@1309:                     this._first = this._last = this._next = this.sc;
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
bsw/jbe@1309:                         this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
bsw/jbe@1309:                     this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
bsw/jbe@1309:                         this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         RangeIterator.prototype = {
bsw/jbe@1309:             _current: null,
bsw/jbe@1309:             _next: null,
bsw/jbe@1309:             _first: null,
bsw/jbe@1309:             _last: null,
bsw/jbe@1309:             isSingleCharacterDataNode: false,
bsw/jbe@1309: 
bsw/jbe@1309:             reset: function() {
bsw/jbe@1309:                 this._current = null;
bsw/jbe@1309:                 this._next = this._first;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             hasNext: function() {
bsw/jbe@1309:                 return !!this._next;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             next: function() {
bsw/jbe@1309:                 // Move to next node
bsw/jbe@1309:                 var current = this._current = this._next;
bsw/jbe@1309:                 if (current) {
bsw/jbe@1309:                     this._next = (current !== this._last) ? current.nextSibling : null;
bsw/jbe@1309: 
bsw/jbe@1309:                     // Check for partially selected text nodes
bsw/jbe@1309:                     if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
bsw/jbe@1309:                         if (current === this.ec) {
bsw/jbe@1309:                             (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
bsw/jbe@1309:                         }
bsw/jbe@1309:                         if (this._current === this.sc) {
bsw/jbe@1309:                             (current = current.cloneNode(true)).deleteData(0, this.so);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 return current;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             remove: function() {
bsw/jbe@1309:                 var current = this._current, start, end;
bsw/jbe@1309: 
bsw/jbe@1309:                 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
bsw/jbe@1309:                     start = (current === this.sc) ? this.so : 0;
bsw/jbe@1309:                     end = (current === this.ec) ? this.eo : current.length;
bsw/jbe@1309:                     if (start != end) {
bsw/jbe@1309:                         current.deleteData(start, end - start);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     if (current.parentNode) {
bsw/jbe@1309:                         removeNode(current);
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             // Checks if the current node is partially selected
bsw/jbe@1309:             isPartiallySelectedSubtree: function() {
bsw/jbe@1309:                 var current = this._current;
bsw/jbe@1309:                 return isNonTextPartiallySelected(current, this.range);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             getSubtreeIterator: function() {
bsw/jbe@1309:                 var subRange;
bsw/jbe@1309:                 if (this.isSingleCharacterDataNode) {
bsw/jbe@1309:                     subRange = this.range.cloneRange();
bsw/jbe@1309:                     subRange.collapse(false);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     subRange = new Range(getRangeDocument(this.range));
bsw/jbe@1309:                     var current = this._current;
bsw/jbe@1309:                     var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
bsw/jbe@1309: 
bsw/jbe@1309:                     if (isOrIsAncestorOf(current, this.sc)) {
bsw/jbe@1309:                         startContainer = this.sc;
bsw/jbe@1309:                         startOffset = this.so;
bsw/jbe@1309:                     }
bsw/jbe@1309:                     if (isOrIsAncestorOf(current, this.ec)) {
bsw/jbe@1309:                         endContainer = this.ec;
bsw/jbe@1309:                         endOffset = this.eo;
bsw/jbe@1309:                     }
bsw/jbe@1309: 
bsw/jbe@1309:                     updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             detach: function() {
bsw/jbe@1309:                 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
bsw/jbe@1309:         var rootContainerNodeTypes = [2, 9, 11];
bsw/jbe@1309:         var readonlyNodeTypes = [5, 6, 10, 12];
bsw/jbe@1309:         var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
bsw/jbe@1309:         var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
bsw/jbe@1309: 
bsw/jbe@1309:         function createAncestorFinder(nodeTypes) {
bsw/jbe@1309:             return function(node, selfIsAncestor) {
bsw/jbe@1309:                 var t, n = selfIsAncestor ? node : node.parentNode;
bsw/jbe@1309:                 while (n) {
bsw/jbe@1309:                     t = n.nodeType;
bsw/jbe@1309:                     if (arrayContains(nodeTypes, t)) {
bsw/jbe@1309:                         return n;
bsw/jbe@1309:                     }
bsw/jbe@1309:                     n = n.parentNode;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return null;
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
bsw/jbe@1309:         var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
bsw/jbe@1309:         var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
bsw/jbe@1309: 
bsw/jbe@1309:         function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
bsw/jbe@1309:             if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
bsw/jbe@1309:                 throw new DOMException("INVALID_NODE_TYPE_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function assertValidNodeType(node, invalidTypes) {
bsw/jbe@1309:             if (!arrayContains(invalidTypes, node.nodeType)) {
bsw/jbe@1309:                 throw new DOMException("INVALID_NODE_TYPE_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function assertValidOffset(node, offset) {
bsw/jbe@1309:             if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
bsw/jbe@1309:                 throw new DOMException("INDEX_SIZE_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function assertSameDocumentOrFragment(node1, node2) {
bsw/jbe@1309:             if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
bsw/jbe@1309:                 throw new DOMException("WRONG_DOCUMENT_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function assertNodeNotReadOnly(node) {
bsw/jbe@1309:             if (getReadonlyAncestor(node, true)) {
bsw/jbe@1309:                 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function assertNode(node, codeName) {
bsw/jbe@1309:             if (!node) {
bsw/jbe@1309:                 throw new DOMException(codeName);
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function isValidOffset(node, offset) {
bsw/jbe@1309:             return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function isRangeValid(range) {
bsw/jbe@1309:             return (!!range.startContainer && !!range.endContainer &&
bsw/jbe@1309:                     !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
bsw/jbe@1309:                     getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
bsw/jbe@1309:                     isValidOffset(range.startContainer, range.startOffset) &&
bsw/jbe@1309:                     isValidOffset(range.endContainer, range.endOffset));
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function assertRangeValid(range) {
bsw/jbe@1309:             if (!isRangeValid(range)) {
bsw/jbe@1309:                 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         // Test the browser's innerHTML support to decide how to implement createContextualFragment
bsw/jbe@1309:         var styleEl = document.createElement("style");
bsw/jbe@1309:         var htmlParsingConforms = false;
bsw/jbe@1309:         try {
bsw/jbe@1309:             styleEl.innerHTML = "x";
bsw/jbe@1309:             htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
bsw/jbe@1309:         } catch (e) {
bsw/jbe@1309:             // IE 6 and 7 throw
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         api.features.htmlParsingConforms = htmlParsingConforms;
bsw/jbe@1309: 
bsw/jbe@1309:         var createContextualFragment = htmlParsingConforms ?
bsw/jbe@1309: 
bsw/jbe@1309:             // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
bsw/jbe@1309:             // discussion and base code for this implementation at issue 67.
bsw/jbe@1309:             // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
bsw/jbe@1309:             // Thanks to Aleks Williams.
bsw/jbe@1309:             function(fragmentStr) {
bsw/jbe@1309:                 // "Let node the context object's start's node."
bsw/jbe@1309:                 var node = this.startContainer;
bsw/jbe@1309:                 var doc = getDocument(node);
bsw/jbe@1309: 
bsw/jbe@1309:                 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
bsw/jbe@1309:                 // exception and abort these steps."
bsw/jbe@1309:                 if (!node) {
bsw/jbe@1309:                     throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // "Let element be as follows, depending on node's interface:"
bsw/jbe@1309:                 // Document, Document Fragment: null
bsw/jbe@1309:                 var el = null;
bsw/jbe@1309: 
bsw/jbe@1309:                 // "Element: node"
bsw/jbe@1309:                 if (node.nodeType == 1) {
bsw/jbe@1309:                     el = node;
bsw/jbe@1309: 
bsw/jbe@1309:                 // "Text, Comment: node's parentElement"
bsw/jbe@1309:                 } else if (isCharacterDataNode(node)) {
bsw/jbe@1309:                     el = dom.parentElement(node);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // "If either element is null or element's ownerDocument is an HTML document
bsw/jbe@1309:                 // and element's local name is "html" and element's namespace is the HTML
bsw/jbe@1309:                 // namespace"
bsw/jbe@1309:                 if (el === null || (
bsw/jbe@1309:                     el.nodeName == "HTML" &&
bsw/jbe@1309:                     dom.isHtmlNamespace(getDocument(el).documentElement) &&
bsw/jbe@1309:                     dom.isHtmlNamespace(el)
bsw/jbe@1309:                 )) {
bsw/jbe@1309: 
bsw/jbe@1309:                 // "let element be a new Element with "body" as its local name and the HTML
bsw/jbe@1309:                 // namespace as its namespace.""
bsw/jbe@1309:                     el = doc.createElement("body");
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     el = el.cloneNode(false);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
bsw/jbe@1309:                 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
bsw/jbe@1309:                 // "In either case, the algorithm must be invoked with fragment as the input
bsw/jbe@1309:                 // and element as the context element."
bsw/jbe@1309:                 el.innerHTML = fragmentStr;
bsw/jbe@1309: 
bsw/jbe@1309:                 // "If this raises an exception, then abort these steps. Otherwise, let new
bsw/jbe@1309:                 // children be the nodes returned."
bsw/jbe@1309: 
bsw/jbe@1309:                 // "Let fragment be a new DocumentFragment."
bsw/jbe@1309:                 // "Append all new children to fragment."
bsw/jbe@1309:                 // "Return fragment."
bsw/jbe@1309:                 return dom.fragmentFromNodeChildren(el);
bsw/jbe@1309:             } :
bsw/jbe@1309: 
bsw/jbe@1309:             // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
bsw/jbe@1309:             // previous versions of Rangy used (with the exception of using a body element rather than a div)
bsw/jbe@1309:             function(fragmentStr) {
bsw/jbe@1309:                 var doc = getRangeDocument(this);
bsw/jbe@1309:                 var el = doc.createElement("body");
bsw/jbe@1309:                 el.innerHTML = fragmentStr;
bsw/jbe@1309: 
bsw/jbe@1309:                 return dom.fragmentFromNodeChildren(el);
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:         function splitRangeBoundaries(range, positionsToPreserve) {
bsw/jbe@1309:             assertRangeValid(range);
bsw/jbe@1309: 
bsw/jbe@1309:             var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
bsw/jbe@1309:             var startEndSame = (sc === ec);
bsw/jbe@1309: 
bsw/jbe@1309:             if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
bsw/jbe@1309:                 splitDataNode(ec, eo, positionsToPreserve);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
bsw/jbe@1309:                 sc = splitDataNode(sc, so, positionsToPreserve);
bsw/jbe@1309:                 if (startEndSame) {
bsw/jbe@1309:                     eo -= so;
bsw/jbe@1309:                     ec = sc;
bsw/jbe@1309:                 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
bsw/jbe@1309:                     eo++;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 so = 0;
bsw/jbe@1309:             }
bsw/jbe@1309:             range.setStartAndEnd(sc, so, ec, eo);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function rangeToHtml(range) {
bsw/jbe@1309:             assertRangeValid(range);
bsw/jbe@1309:             var container = range.commonAncestorContainer.parentNode.cloneNode(false);
bsw/jbe@1309:             container.appendChild( range.cloneContents() );
bsw/jbe@1309:             return container.innerHTML;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
bsw/jbe@1309:             "commonAncestorContainer"];
bsw/jbe@1309: 
bsw/jbe@1309:         var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
bsw/jbe@1309:         var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
bsw/jbe@1309: 
bsw/jbe@1309:         util.extend(api.rangePrototype, {
bsw/jbe@1309:             compareBoundaryPoints: function(how, range) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
bsw/jbe@1309: 
bsw/jbe@1309:                 var nodeA, offsetA, nodeB, offsetB;
bsw/jbe@1309:                 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
bsw/jbe@1309:                 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
bsw/jbe@1309:                 nodeA = this[prefixA + "Container"];
bsw/jbe@1309:                 offsetA = this[prefixA + "Offset"];
bsw/jbe@1309:                 nodeB = range[prefixB + "Container"];
bsw/jbe@1309:                 offsetB = range[prefixB + "Offset"];
bsw/jbe@1309:                 return comparePoints(nodeA, offsetA, nodeB, offsetB);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             insertNode: function(node) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 assertValidNodeType(node, insertableNodeTypes);
bsw/jbe@1309:                 assertNodeNotReadOnly(this.startContainer);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (isOrIsAncestorOf(node, this.startContainer)) {
bsw/jbe@1309:                     throw new DOMException("HIERARCHY_REQUEST_ERR");
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // No check for whether the container of the start of the Range is of a type that does not allow
bsw/jbe@1309:                 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
bsw/jbe@1309:                 // to add the node
bsw/jbe@1309: 
bsw/jbe@1309:                 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
bsw/jbe@1309:                 this.setStartBefore(firstNodeInserted);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             cloneContents: function() {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309: 
bsw/jbe@1309:                 var clone, frag;
bsw/jbe@1309:                 if (this.collapsed) {
bsw/jbe@1309:                     return getRangeDocument(this).createDocumentFragment();
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
bsw/jbe@1309:                         clone = this.startContainer.cloneNode(true);
bsw/jbe@1309:                         clone.data = clone.data.slice(this.startOffset, this.endOffset);
bsw/jbe@1309:                         frag = getRangeDocument(this).createDocumentFragment();
bsw/jbe@1309:                         frag.appendChild(clone);
bsw/jbe@1309:                         return frag;
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         var iterator = new RangeIterator(this, true);
bsw/jbe@1309:                         clone = cloneSubtree(iterator);
bsw/jbe@1309:                         iterator.detach();
bsw/jbe@1309:                     }
bsw/jbe@1309:                     return clone;
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             canSurroundContents: function() {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 assertNodeNotReadOnly(this.startContainer);
bsw/jbe@1309:                 assertNodeNotReadOnly(this.endContainer);
bsw/jbe@1309: 
bsw/jbe@1309:                 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
bsw/jbe@1309:                 // no non-text nodes.
bsw/jbe@1309:                 var iterator = new RangeIterator(this, true);
bsw/jbe@1309:                 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
bsw/jbe@1309:                         (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
bsw/jbe@1309:                 iterator.detach();
bsw/jbe@1309:                 return !boundariesInvalid;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             surroundContents: function(node) {
bsw/jbe@1309:                 assertValidNodeType(node, surroundNodeTypes);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (!this.canSurroundContents()) {
bsw/jbe@1309:                     throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Extract the contents
bsw/jbe@1309:                 var content = this.extractContents();
bsw/jbe@1309: 
bsw/jbe@1309:                 // Clear the children of the node
bsw/jbe@1309:                 if (node.hasChildNodes()) {
bsw/jbe@1309:                     while (node.lastChild) {
bsw/jbe@1309:                         node.removeChild(node.lastChild);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Insert the new node and add the extracted contents
bsw/jbe@1309:                 insertNodeAtPosition(node, this.startContainer, this.startOffset);
bsw/jbe@1309:                 node.appendChild(content);
bsw/jbe@1309: 
bsw/jbe@1309:                 this.selectNode(node);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             cloneRange: function() {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 var range = new Range(getRangeDocument(this));
bsw/jbe@1309:                 var i = rangeProperties.length, prop;
bsw/jbe@1309:                 while (i--) {
bsw/jbe@1309:                     prop = rangeProperties[i];
bsw/jbe@1309:                     range[prop] = this[prop];
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return range;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             toString: function() {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 var sc = this.startContainer;
bsw/jbe@1309:                 if (sc === this.endContainer && isCharacterDataNode(sc)) {
bsw/jbe@1309:                     return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     var textParts = [], iterator = new RangeIterator(this, true);
bsw/jbe@1309:                     iterateSubtree(iterator, function(node) {
bsw/jbe@1309:                         // Accept only text or CDATA nodes, not comments
bsw/jbe@1309:                         if (node.nodeType == 3 || node.nodeType == 4) {
bsw/jbe@1309:                             textParts.push(node.data);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     });
bsw/jbe@1309:                     iterator.detach();
bsw/jbe@1309:                     return textParts.join("");
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
bsw/jbe@1309:             // been removed from Mozilla.
bsw/jbe@1309: 
bsw/jbe@1309:             compareNode: function(node) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309: 
bsw/jbe@1309:                 var parent = node.parentNode;
bsw/jbe@1309:                 var nodeIndex = getNodeIndex(node);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (!parent) {
bsw/jbe@1309:                     throw new DOMException("NOT_FOUND_ERR");
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 var startComparison = this.comparePoint(parent, nodeIndex),
bsw/jbe@1309:                     endComparison = this.comparePoint(parent, nodeIndex + 1);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (startComparison < 0) { // Node starts before
bsw/jbe@1309:                     return (endComparison > 0) ? n_b_a : n_b;
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     return (endComparison > 0) ? n_a : n_i;
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             comparePoint: function(node, offset) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 assertNode(node, "HIERARCHY_REQUEST_ERR");
bsw/jbe@1309:                 assertSameDocumentOrFragment(node, this.startContainer);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
bsw/jbe@1309:                     return -1;
bsw/jbe@1309:                 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
bsw/jbe@1309:                     return 1;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return 0;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             createContextualFragment: createContextualFragment,
bsw/jbe@1309: 
bsw/jbe@1309:             toHtml: function() {
bsw/jbe@1309:                 return rangeToHtml(this);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
bsw/jbe@1309:             // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
bsw/jbe@1309:             intersectsNode: function(node, touchingIsIntersecting) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 if (getRootContainer(node) != getRangeRoot(this)) {
bsw/jbe@1309:                     return false;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 var parent = node.parentNode, offset = getNodeIndex(node);
bsw/jbe@1309:                 if (!parent) {
bsw/jbe@1309:                     return true;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
bsw/jbe@1309:                     endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
bsw/jbe@1309: 
bsw/jbe@1309:                 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             isPointInRange: function(node, offset) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 assertNode(node, "HIERARCHY_REQUEST_ERR");
bsw/jbe@1309:                 assertSameDocumentOrFragment(node, this.startContainer);
bsw/jbe@1309: 
bsw/jbe@1309:                 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
bsw/jbe@1309:                        (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             // The methods below are non-standard and invented by me.
bsw/jbe@1309: 
bsw/jbe@1309:             // Sharing a boundary start-to-end or end-to-start does not count as intersection.
bsw/jbe@1309:             intersectsRange: function(range) {
bsw/jbe@1309:                 return rangesIntersect(this, range, false);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             // Sharing a boundary start-to-end or end-to-start does count as intersection.
bsw/jbe@1309:             intersectsOrTouchesRange: function(range) {
bsw/jbe@1309:                 return rangesIntersect(this, range, true);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             intersection: function(range) {
bsw/jbe@1309:                 if (this.intersectsRange(range)) {
bsw/jbe@1309:                     var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
bsw/jbe@1309:                         endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
bsw/jbe@1309: 
bsw/jbe@1309:                     var intersectionRange = this.cloneRange();
bsw/jbe@1309:                     if (startComparison == -1) {
bsw/jbe@1309:                         intersectionRange.setStart(range.startContainer, range.startOffset);
bsw/jbe@1309:                     }
bsw/jbe@1309:                     if (endComparison == 1) {
bsw/jbe@1309:                         intersectionRange.setEnd(range.endContainer, range.endOffset);
bsw/jbe@1309:                     }
bsw/jbe@1309:                     return intersectionRange;
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return null;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             union: function(range) {
bsw/jbe@1309:                 if (this.intersectsOrTouchesRange(range)) {
bsw/jbe@1309:                     var unionRange = this.cloneRange();
bsw/jbe@1309:                     if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
bsw/jbe@1309:                         unionRange.setStart(range.startContainer, range.startOffset);
bsw/jbe@1309:                     }
bsw/jbe@1309:                     if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
bsw/jbe@1309:                         unionRange.setEnd(range.endContainer, range.endOffset);
bsw/jbe@1309:                     }
bsw/jbe@1309:                     return unionRange;
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     throw new DOMException("Ranges do not intersect");
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             containsNode: function(node, allowPartial) {
bsw/jbe@1309:                 if (allowPartial) {
bsw/jbe@1309:                     return this.intersectsNode(node, false);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     return this.compareNode(node) == n_i;
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             containsNodeContents: function(node) {
bsw/jbe@1309:                 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             containsRange: function(range) {
bsw/jbe@1309:                 var intersection = this.intersection(range);
bsw/jbe@1309:                 return intersection !== null && range.equals(intersection);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             containsNodeText: function(node) {
bsw/jbe@1309:                 var nodeRange = this.cloneRange();
bsw/jbe@1309:                 nodeRange.selectNode(node);
bsw/jbe@1309:                 var textNodes = nodeRange.getNodes([3]);
bsw/jbe@1309:                 if (textNodes.length > 0) {
bsw/jbe@1309:                     nodeRange.setStart(textNodes[0], 0);
bsw/jbe@1309:                     var lastTextNode = textNodes.pop();
bsw/jbe@1309:                     nodeRange.setEnd(lastTextNode, lastTextNode.length);
bsw/jbe@1309:                     return this.containsRange(nodeRange);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     return this.containsNodeContents(node);
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             getNodes: function(nodeTypes, filter) {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309:                 return getNodesInRange(this, nodeTypes, filter);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             getDocument: function() {
bsw/jbe@1309:                 return getRangeDocument(this);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             collapseBefore: function(node) {
bsw/jbe@1309:                 this.setEndBefore(node);
bsw/jbe@1309:                 this.collapse(false);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             collapseAfter: function(node) {
bsw/jbe@1309:                 this.setStartAfter(node);
bsw/jbe@1309:                 this.collapse(true);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             getBookmark: function(containerNode) {
bsw/jbe@1309:                 var doc = getRangeDocument(this);
bsw/jbe@1309:                 var preSelectionRange = api.createRange(doc);
bsw/jbe@1309:                 containerNode = containerNode || dom.getBody(doc);
bsw/jbe@1309:                 preSelectionRange.selectNodeContents(containerNode);
bsw/jbe@1309:                 var range = this.intersection(preSelectionRange);
bsw/jbe@1309:                 var start = 0, end = 0;
bsw/jbe@1309:                 if (range) {
bsw/jbe@1309:                     preSelectionRange.setEnd(range.startContainer, range.startOffset);
bsw/jbe@1309:                     start = preSelectionRange.toString().length;
bsw/jbe@1309:                     end = start + range.toString().length;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 return {
bsw/jbe@1309:                     start: start,
bsw/jbe@1309:                     end: end,
bsw/jbe@1309:                     containerNode: containerNode
bsw/jbe@1309:                 };
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             moveToBookmark: function(bookmark) {
bsw/jbe@1309:                 var containerNode = bookmark.containerNode;
bsw/jbe@1309:                 var charIndex = 0;
bsw/jbe@1309:                 this.setStart(containerNode, 0);
bsw/jbe@1309:                 this.collapse(true);
bsw/jbe@1309:                 var nodeStack = [containerNode], node, foundStart = false, stop = false;
bsw/jbe@1309:                 var nextCharIndex, i, childNodes;
bsw/jbe@1309: 
bsw/jbe@1309:                 while (!stop && (node = nodeStack.pop())) {
bsw/jbe@1309:                     if (node.nodeType == 3) {
bsw/jbe@1309:                         nextCharIndex = charIndex + node.length;
bsw/jbe@1309:                         if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
bsw/jbe@1309:                             this.setStart(node, bookmark.start - charIndex);
bsw/jbe@1309:                             foundStart = true;
bsw/jbe@1309:                         }
bsw/jbe@1309:                         if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
bsw/jbe@1309:                             this.setEnd(node, bookmark.end - charIndex);
bsw/jbe@1309:                             stop = true;
bsw/jbe@1309:                         }
bsw/jbe@1309:                         charIndex = nextCharIndex;
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         childNodes = node.childNodes;
bsw/jbe@1309:                         i = childNodes.length;
bsw/jbe@1309:                         while (i--) {
bsw/jbe@1309:                             nodeStack.push(childNodes[i]);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             getName: function() {
bsw/jbe@1309:                 return "DomRange";
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             equals: function(range) {
bsw/jbe@1309:                 return Range.rangesEqual(this, range);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             isValid: function() {
bsw/jbe@1309:                 return isRangeValid(this);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             inspect: function() {
bsw/jbe@1309:                 return inspect(this);
bsw/jbe@1309:             },
bsw/jbe@1309: 
bsw/jbe@1309:             detach: function() {
bsw/jbe@1309:                 // In DOM4, detach() is now a no-op.
bsw/jbe@1309:             }
bsw/jbe@1309:         });
bsw/jbe@1309: 
bsw/jbe@1309:         function copyComparisonConstantsToObject(obj) {
bsw/jbe@1309:             obj.START_TO_START = s2s;
bsw/jbe@1309:             obj.START_TO_END = s2e;
bsw/jbe@1309:             obj.END_TO_END = e2e;
bsw/jbe@1309:             obj.END_TO_START = e2s;
bsw/jbe@1309: 
bsw/jbe@1309:             obj.NODE_BEFORE = n_b;
bsw/jbe@1309:             obj.NODE_AFTER = n_a;
bsw/jbe@1309:             obj.NODE_BEFORE_AND_AFTER = n_b_a;
bsw/jbe@1309:             obj.NODE_INSIDE = n_i;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function copyComparisonConstants(constructor) {
bsw/jbe@1309:             copyComparisonConstantsToObject(constructor);
bsw/jbe@1309:             copyComparisonConstantsToObject(constructor.prototype);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function createRangeContentRemover(remover, boundaryUpdater) {
bsw/jbe@1309:             return function() {
bsw/jbe@1309:                 assertRangeValid(this);
bsw/jbe@1309: 
bsw/jbe@1309:                 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
bsw/jbe@1309: 
bsw/jbe@1309:                 var iterator = new RangeIterator(this, true);
bsw/jbe@1309: 
bsw/jbe@1309:                 // Work out where to position the range after content removal
bsw/jbe@1309:                 var node, boundary;
bsw/jbe@1309:                 if (sc !== root) {
bsw/jbe@1309:                     node = getClosestAncestorIn(sc, root, true);
bsw/jbe@1309:                     boundary = getBoundaryAfterNode(node);
bsw/jbe@1309:                     sc = boundary.node;
bsw/jbe@1309:                     so = boundary.offset;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Check none of the range is read-only
bsw/jbe@1309:                 iterateSubtree(iterator, assertNodeNotReadOnly);
bsw/jbe@1309: 
bsw/jbe@1309:                 iterator.reset();
bsw/jbe@1309: 
bsw/jbe@1309:                 // Remove the content
bsw/jbe@1309:                 var returnValue = remover(iterator);
bsw/jbe@1309:                 iterator.detach();
bsw/jbe@1309: 
bsw/jbe@1309:                 // Move to the new position
bsw/jbe@1309:                 boundaryUpdater(this, sc, so, sc, so);
bsw/jbe@1309: 
bsw/jbe@1309:                 return returnValue;
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function createPrototypeRange(constructor, boundaryUpdater) {
bsw/jbe@1309:             function createBeforeAfterNodeSetter(isBefore, isStart) {
bsw/jbe@1309:                 return function(node) {
bsw/jbe@1309:                     assertValidNodeType(node, beforeAfterNodeTypes);
bsw/jbe@1309:                     assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
bsw/jbe@1309: 
bsw/jbe@1309:                     var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
bsw/jbe@1309:                     (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
bsw/jbe@1309:                 };
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             function setRangeStart(range, node, offset) {
bsw/jbe@1309:                 var ec = range.endContainer, eo = range.endOffset;
bsw/jbe@1309:                 if (node !== range.startContainer || offset !== range.startOffset) {
bsw/jbe@1309:                     // Check the root containers of the range and the new boundary, and also check whether the new boundary
bsw/jbe@1309:                     // is after the current end. In either case, collapse the range to the new position
bsw/jbe@1309:                     if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
bsw/jbe@1309:                         ec = node;
bsw/jbe@1309:                         eo = offset;
bsw/jbe@1309:                     }
bsw/jbe@1309:                     boundaryUpdater(range, node, offset, ec, eo);
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             function setRangeEnd(range, node, offset) {
bsw/jbe@1309:                 var sc = range.startContainer, so = range.startOffset;
bsw/jbe@1309:                 if (node !== range.endContainer || offset !== range.endOffset) {
bsw/jbe@1309:                     // Check the root containers of the range and the new boundary, and also check whether the new boundary
bsw/jbe@1309:                     // is after the current end. In either case, collapse the range to the new position
bsw/jbe@1309:                     if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
bsw/jbe@1309:                         sc = node;
bsw/jbe@1309:                         so = offset;
bsw/jbe@1309:                     }
bsw/jbe@1309:                     boundaryUpdater(range, sc, so, node, offset);
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // Set up inheritance
bsw/jbe@1309:             var F = function() {};
bsw/jbe@1309:             F.prototype = api.rangePrototype;
bsw/jbe@1309:             constructor.prototype = new F();
bsw/jbe@1309: 
bsw/jbe@1309:             util.extend(constructor.prototype, {
bsw/jbe@1309:                 setStart: function(node, offset) {
bsw/jbe@1309:                     assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309:                     assertValidOffset(node, offset);
bsw/jbe@1309: 
bsw/jbe@1309:                     setRangeStart(this, node, offset);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 setEnd: function(node, offset) {
bsw/jbe@1309:                     assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309:                     assertValidOffset(node, offset);
bsw/jbe@1309: 
bsw/jbe@1309:                     setRangeEnd(this, node, offset);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 /**
bsw/jbe@1309:                  * Convenience method to set a range's start and end boundaries. Overloaded as follows:
bsw/jbe@1309:                  * - Two parameters (node, offset) creates a collapsed range at that position
bsw/jbe@1309:                  * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
bsw/jbe@1309:                  *   startOffset and ending at endOffset
bsw/jbe@1309:                  * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
bsw/jbe@1309:                  *   startNode and ending at endOffset in endNode
bsw/jbe@1309:                  */
bsw/jbe@1309:                 setStartAndEnd: function() {
bsw/jbe@1309:                     var args = arguments;
bsw/jbe@1309:                     var sc = args[0], so = args[1], ec = sc, eo = so;
bsw/jbe@1309: 
bsw/jbe@1309:                     switch (args.length) {
bsw/jbe@1309:                         case 3:
bsw/jbe@1309:                             eo = args[2];
bsw/jbe@1309:                             break;
bsw/jbe@1309:                         case 4:
bsw/jbe@1309:                             ec = args[2];
bsw/jbe@1309:                             eo = args[3];
bsw/jbe@1309:                             break;
bsw/jbe@1309:                     }
bsw/jbe@1309: 
bsw/jbe@1309:                     boundaryUpdater(this, sc, so, ec, eo);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 setBoundary: function(node, offset, isStart) {
bsw/jbe@1309:                     this["set" + (isStart ? "Start" : "End")](node, offset);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 setStartBefore: createBeforeAfterNodeSetter(true, true),
bsw/jbe@1309:                 setStartAfter: createBeforeAfterNodeSetter(false, true),
bsw/jbe@1309:                 setEndBefore: createBeforeAfterNodeSetter(true, false),
bsw/jbe@1309:                 setEndAfter: createBeforeAfterNodeSetter(false, false),
bsw/jbe@1309: 
bsw/jbe@1309:                 collapse: function(isStart) {
bsw/jbe@1309:                     assertRangeValid(this);
bsw/jbe@1309:                     if (isStart) {
bsw/jbe@1309:                         boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 selectNodeContents: function(node) {
bsw/jbe@1309:                     assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309: 
bsw/jbe@1309:                     boundaryUpdater(this, node, 0, node, getNodeLength(node));
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 selectNode: function(node) {
bsw/jbe@1309:                     assertNoDocTypeNotationEntityAncestor(node, false);
bsw/jbe@1309:                     assertValidNodeType(node, beforeAfterNodeTypes);
bsw/jbe@1309: 
bsw/jbe@1309:                     var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
bsw/jbe@1309:                     boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
bsw/jbe@1309: 
bsw/jbe@1309:                 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
bsw/jbe@1309: 
bsw/jbe@1309:                 canSurroundContents: function() {
bsw/jbe@1309:                     assertRangeValid(this);
bsw/jbe@1309:                     assertNodeNotReadOnly(this.startContainer);
bsw/jbe@1309:                     assertNodeNotReadOnly(this.endContainer);
bsw/jbe@1309: 
bsw/jbe@1309:                     // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
bsw/jbe@1309:                     // no non-text nodes.
bsw/jbe@1309:                     var iterator = new RangeIterator(this, true);
bsw/jbe@1309:                     var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
bsw/jbe@1309:                             (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
bsw/jbe@1309:                     iterator.detach();
bsw/jbe@1309:                     return !boundariesInvalid;
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 splitBoundaries: function() {
bsw/jbe@1309:                     splitRangeBoundaries(this);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 splitBoundariesPreservingPositions: function(positionsToPreserve) {
bsw/jbe@1309:                     splitRangeBoundaries(this, positionsToPreserve);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 normalizeBoundaries: function() {
bsw/jbe@1309:                     assertRangeValid(this);
bsw/jbe@1309: 
bsw/jbe@1309:                     var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
bsw/jbe@1309: 
bsw/jbe@1309:                     var mergeForward = function(node) {
bsw/jbe@1309:                         var sibling = node.nextSibling;
bsw/jbe@1309:                         if (sibling && sibling.nodeType == node.nodeType) {
bsw/jbe@1309:                             ec = node;
bsw/jbe@1309:                             eo = node.length;
bsw/jbe@1309:                             node.appendData(sibling.data);
bsw/jbe@1309:                             removeNode(sibling);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     var mergeBackward = function(node) {
bsw/jbe@1309:                         var sibling = node.previousSibling;
bsw/jbe@1309:                         if (sibling && sibling.nodeType == node.nodeType) {
bsw/jbe@1309:                             sc = node;
bsw/jbe@1309:                             var nodeLength = node.length;
bsw/jbe@1309:                             so = sibling.length;
bsw/jbe@1309:                             node.insertData(0, sibling.data);
bsw/jbe@1309:                             removeNode(sibling);
bsw/jbe@1309:                             if (sc == ec) {
bsw/jbe@1309:                                 eo += so;
bsw/jbe@1309:                                 ec = sc;
bsw/jbe@1309:                             } else if (ec == node.parentNode) {
bsw/jbe@1309:                                 var nodeIndex = getNodeIndex(node);
bsw/jbe@1309:                                 if (eo == nodeIndex) {
bsw/jbe@1309:                                     ec = node;
bsw/jbe@1309:                                     eo = nodeLength;
bsw/jbe@1309:                                 } else if (eo > nodeIndex) {
bsw/jbe@1309:                                     eo--;
bsw/jbe@1309:                                 }
bsw/jbe@1309:                             }
bsw/jbe@1309:                         }
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     var normalizeStart = true;
bsw/jbe@1309:                     var sibling;
bsw/jbe@1309: 
bsw/jbe@1309:                     if (isCharacterDataNode(ec)) {
bsw/jbe@1309:                         if (eo == ec.length) {
bsw/jbe@1309:                             mergeForward(ec);
bsw/jbe@1309:                         } else if (eo == 0) {
bsw/jbe@1309:                             sibling = ec.previousSibling;
bsw/jbe@1309:                             if (sibling && sibling.nodeType == ec.nodeType) {
bsw/jbe@1309:                                 eo = sibling.length;
bsw/jbe@1309:                                 if (sc == ec) {
bsw/jbe@1309:                                     normalizeStart = false;
bsw/jbe@1309:                                 }
bsw/jbe@1309:                                 sibling.appendData(ec.data);
bsw/jbe@1309:                                 removeNode(ec);
bsw/jbe@1309:                                 ec = sibling;
bsw/jbe@1309:                             }
bsw/jbe@1309:                         }
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         if (eo > 0) {
bsw/jbe@1309:                             var endNode = ec.childNodes[eo - 1];
bsw/jbe@1309:                             if (endNode && isCharacterDataNode(endNode)) {
bsw/jbe@1309:                                 mergeForward(endNode);
bsw/jbe@1309:                             }
bsw/jbe@1309:                         }
bsw/jbe@1309:                         normalizeStart = !this.collapsed;
bsw/jbe@1309:                     }
bsw/jbe@1309: 
bsw/jbe@1309:                     if (normalizeStart) {
bsw/jbe@1309:                         if (isCharacterDataNode(sc)) {
bsw/jbe@1309:                             if (so == 0) {
bsw/jbe@1309:                                 mergeBackward(sc);
bsw/jbe@1309:                             } else if (so == sc.length) {
bsw/jbe@1309:                                 sibling = sc.nextSibling;
bsw/jbe@1309:                                 if (sibling && sibling.nodeType == sc.nodeType) {
bsw/jbe@1309:                                     if (ec == sibling) {
bsw/jbe@1309:                                         ec = sc;
bsw/jbe@1309:                                         eo += sc.length;
bsw/jbe@1309:                                     }
bsw/jbe@1309:                                     sc.appendData(sibling.data);
bsw/jbe@1309:                                     removeNode(sibling);
bsw/jbe@1309:                                 }
bsw/jbe@1309:                             }
bsw/jbe@1309:                         } else {
bsw/jbe@1309:                             if (so < sc.childNodes.length) {
bsw/jbe@1309:                                 var startNode = sc.childNodes[so];
bsw/jbe@1309:                                 if (startNode && isCharacterDataNode(startNode)) {
bsw/jbe@1309:                                     mergeBackward(startNode);
bsw/jbe@1309:                                 }
bsw/jbe@1309:                             }
bsw/jbe@1309:                         }
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         sc = ec;
bsw/jbe@1309:                         so = eo;
bsw/jbe@1309:                     }
bsw/jbe@1309: 
bsw/jbe@1309:                     boundaryUpdater(this, sc, so, ec, eo);
bsw/jbe@1309:                 },
bsw/jbe@1309: 
bsw/jbe@1309:                 collapseToPoint: function(node, offset) {
bsw/jbe@1309:                     assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309:                     assertValidOffset(node, offset);
bsw/jbe@1309:                     this.setStartAndEnd(node, offset);
bsw/jbe@1309:                 }
bsw/jbe@1309:             });
bsw/jbe@1309: 
bsw/jbe@1309:             copyComparisonConstants(constructor);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         // Updates commonAncestorContainer and collapsed after boundary change
bsw/jbe@1309:         function updateCollapsedAndCommonAncestor(range) {
bsw/jbe@1309:             range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
bsw/jbe@1309:             range.commonAncestorContainer = range.collapsed ?
bsw/jbe@1309:                 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
bsw/jbe@1309:             range.startContainer = startContainer;
bsw/jbe@1309:             range.startOffset = startOffset;
bsw/jbe@1309:             range.endContainer = endContainer;
bsw/jbe@1309:             range.endOffset = endOffset;
bsw/jbe@1309:             range.document = dom.getDocument(startContainer);
bsw/jbe@1309: 
bsw/jbe@1309:             updateCollapsedAndCommonAncestor(range);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function Range(doc) {
bsw/jbe@1309:             this.startContainer = doc;
bsw/jbe@1309:             this.startOffset = 0;
bsw/jbe@1309:             this.endContainer = doc;
bsw/jbe@1309:             this.endOffset = 0;
bsw/jbe@1309:             this.document = doc;
bsw/jbe@1309:             updateCollapsedAndCommonAncestor(this);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         createPrototypeRange(Range, updateBoundaries);
bsw/jbe@1309: 
bsw/jbe@1309:         util.extend(Range, {
bsw/jbe@1309:             rangeProperties: rangeProperties,
bsw/jbe@1309:             RangeIterator: RangeIterator,
bsw/jbe@1309:             copyComparisonConstants: copyComparisonConstants,
bsw/jbe@1309:             createPrototypeRange: createPrototypeRange,
bsw/jbe@1309:             inspect: inspect,
bsw/jbe@1309:             toHtml: rangeToHtml,
bsw/jbe@1309:             getRangeDocument: getRangeDocument,
bsw/jbe@1309:             rangesEqual: function(r1, r2) {
bsw/jbe@1309:                 return r1.startContainer === r2.startContainer &&
bsw/jbe@1309:                     r1.startOffset === r2.startOffset &&
bsw/jbe@1309:                     r1.endContainer === r2.endContainer &&
bsw/jbe@1309:                     r1.endOffset === r2.endOffset;
bsw/jbe@1309:             }
bsw/jbe@1309:         });
bsw/jbe@1309: 
bsw/jbe@1309:         api.DomRange = Range;
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // Wrappers for the browser's native DOM Range and/or TextRange implementation
bsw/jbe@1309:     api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
bsw/jbe@1309:         var WrappedRange, WrappedTextRange;
bsw/jbe@1309:         var dom = api.dom;
bsw/jbe@1309:         var util = api.util;
bsw/jbe@1309:         var DomPosition = dom.DomPosition;
bsw/jbe@1309:         var DomRange = api.DomRange;
bsw/jbe@1309:         var getBody = dom.getBody;
bsw/jbe@1309:         var getContentDocument = dom.getContentDocument;
bsw/jbe@1309:         var isCharacterDataNode = dom.isCharacterDataNode;
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:         /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:         if (api.features.implementsDomRange) {
bsw/jbe@1309:             // This is a wrapper around the browser's native DOM Range. It has two aims:
bsw/jbe@1309:             // - Provide workarounds for specific browser bugs
bsw/jbe@1309:             // - provide convenient extensions, which are inherited from Rangy's DomRange
bsw/jbe@1309: 
bsw/jbe@1309:             (function() {
bsw/jbe@1309:                 var rangeProto;
bsw/jbe@1309:                 var rangeProperties = DomRange.rangeProperties;
bsw/jbe@1309: 
bsw/jbe@1309:                 function updateRangeProperties(range) {
bsw/jbe@1309:                     var i = rangeProperties.length, prop;
bsw/jbe@1309:                     while (i--) {
bsw/jbe@1309:                         prop = rangeProperties[i];
bsw/jbe@1309:                         range[prop] = range.nativeRange[prop];
bsw/jbe@1309:                     }
bsw/jbe@1309:                     // Fix for broken collapsed property in IE 9.
bsw/jbe@1309:                     range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
bsw/jbe@1309:                     var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
bsw/jbe@1309:                     var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
bsw/jbe@1309:                     var nativeRangeDifferent = !range.equals(range.nativeRange);
bsw/jbe@1309: 
bsw/jbe@1309:                     // Always set both boundaries for the benefit of IE9 (see issue 35)
bsw/jbe@1309:                     if (startMoved || endMoved || nativeRangeDifferent) {
bsw/jbe@1309:                         range.setEnd(endContainer, endOffset);
bsw/jbe@1309:                         range.setStart(startContainer, startOffset);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 var createBeforeAfterNodeSetter;
bsw/jbe@1309: 
bsw/jbe@1309:                 WrappedRange = function(range) {
bsw/jbe@1309:                     if (!range) {
bsw/jbe@1309:                         throw module.createError("WrappedRange: Range must be specified");
bsw/jbe@1309:                     }
bsw/jbe@1309:                     this.nativeRange = range;
bsw/jbe@1309:                     updateRangeProperties(this);
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto = WrappedRange.prototype;
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.selectNode = function(node) {
bsw/jbe@1309:                     this.nativeRange.selectNode(node);
bsw/jbe@1309:                     updateRangeProperties(this);
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.cloneContents = function() {
bsw/jbe@1309:                     return this.nativeRange.cloneContents();
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
bsw/jbe@1309:                 // insertNode() is never delegated to the native range.
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.surroundContents = function(node) {
bsw/jbe@1309:                     this.nativeRange.surroundContents(node);
bsw/jbe@1309:                     updateRangeProperties(this);
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.collapse = function(isStart) {
bsw/jbe@1309:                     this.nativeRange.collapse(isStart);
bsw/jbe@1309:                     updateRangeProperties(this);
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.cloneRange = function() {
bsw/jbe@1309:                     return new WrappedRange(this.nativeRange.cloneRange());
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.refresh = function() {
bsw/jbe@1309:                     updateRangeProperties(this);
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.toString = function() {
bsw/jbe@1309:                     return this.nativeRange.toString();
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 // Create test range and node for feature detection
bsw/jbe@1309: 
bsw/jbe@1309:                 var testTextNode = document.createTextNode("test");
bsw/jbe@1309:                 getBody(document).appendChild(testTextNode);
bsw/jbe@1309:                 var range = document.createRange();
bsw/jbe@1309: 
bsw/jbe@1309:                 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:                 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
bsw/jbe@1309:                 // correct for it
bsw/jbe@1309: 
bsw/jbe@1309:                 range.setStart(testTextNode, 0);
bsw/jbe@1309:                 range.setEnd(testTextNode, 0);
bsw/jbe@1309: 
bsw/jbe@1309:                 try {
bsw/jbe@1309:                     range.setStart(testTextNode, 1);
bsw/jbe@1309: 
bsw/jbe@1309:                     rangeProto.setStart = function(node, offset) {
bsw/jbe@1309:                         this.nativeRange.setStart(node, offset);
bsw/jbe@1309:                         updateRangeProperties(this);
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     rangeProto.setEnd = function(node, offset) {
bsw/jbe@1309:                         this.nativeRange.setEnd(node, offset);
bsw/jbe@1309:                         updateRangeProperties(this);
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     createBeforeAfterNodeSetter = function(name) {
bsw/jbe@1309:                         return function(node) {
bsw/jbe@1309:                             this.nativeRange[name](node);
bsw/jbe@1309:                             updateRangeProperties(this);
bsw/jbe@1309:                         };
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                 } catch(ex) {
bsw/jbe@1309: 
bsw/jbe@1309:                     rangeProto.setStart = function(node, offset) {
bsw/jbe@1309:                         try {
bsw/jbe@1309:                             this.nativeRange.setStart(node, offset);
bsw/jbe@1309:                         } catch (ex) {
bsw/jbe@1309:                             this.nativeRange.setEnd(node, offset);
bsw/jbe@1309:                             this.nativeRange.setStart(node, offset);
bsw/jbe@1309:                         }
bsw/jbe@1309:                         updateRangeProperties(this);
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     rangeProto.setEnd = function(node, offset) {
bsw/jbe@1309:                         try {
bsw/jbe@1309:                             this.nativeRange.setEnd(node, offset);
bsw/jbe@1309:                         } catch (ex) {
bsw/jbe@1309:                             this.nativeRange.setStart(node, offset);
bsw/jbe@1309:                             this.nativeRange.setEnd(node, offset);
bsw/jbe@1309:                         }
bsw/jbe@1309:                         updateRangeProperties(this);
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     createBeforeAfterNodeSetter = function(name, oppositeName) {
bsw/jbe@1309:                         return function(node) {
bsw/jbe@1309:                             try {
bsw/jbe@1309:                                 this.nativeRange[name](node);
bsw/jbe@1309:                             } catch (ex) {
bsw/jbe@1309:                                 this.nativeRange[oppositeName](node);
bsw/jbe@1309:                                 this.nativeRange[name](node);
bsw/jbe@1309:                             }
bsw/jbe@1309:                             updateRangeProperties(this);
bsw/jbe@1309:                         };
bsw/jbe@1309:                     };
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
bsw/jbe@1309:                 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
bsw/jbe@1309:                 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
bsw/jbe@1309:                 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
bsw/jbe@1309: 
bsw/jbe@1309:                 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:                 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
bsw/jbe@1309:                 // whether the native implementation can be trusted
bsw/jbe@1309:                 rangeProto.selectNodeContents = function(node) {
bsw/jbe@1309:                     this.setStartAndEnd(node, 0, dom.getNodeLength(node));
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:                 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
bsw/jbe@1309:                 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
bsw/jbe@1309: 
bsw/jbe@1309:                 range.selectNodeContents(testTextNode);
bsw/jbe@1309:                 range.setEnd(testTextNode, 3);
bsw/jbe@1309: 
bsw/jbe@1309:                 var range2 = document.createRange();
bsw/jbe@1309:                 range2.selectNodeContents(testTextNode);
bsw/jbe@1309:                 range2.setEnd(testTextNode, 4);
bsw/jbe@1309:                 range2.setStart(testTextNode, 2);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
bsw/jbe@1309:                         range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
bsw/jbe@1309:                     // This is the wrong way round, so correct for it
bsw/jbe@1309: 
bsw/jbe@1309:                     rangeProto.compareBoundaryPoints = function(type, range) {
bsw/jbe@1309:                         range = range.nativeRange || range;
bsw/jbe@1309:                         if (type == range.START_TO_END) {
bsw/jbe@1309:                             type = range.END_TO_START;
bsw/jbe@1309:                         } else if (type == range.END_TO_START) {
bsw/jbe@1309:                             type = range.START_TO_END;
bsw/jbe@1309:                         }
bsw/jbe@1309:                         return this.nativeRange.compareBoundaryPoints(type, range);
bsw/jbe@1309:                     };
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     rangeProto.compareBoundaryPoints = function(type, range) {
bsw/jbe@1309:                         return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
bsw/jbe@1309:                     };
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:                 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
bsw/jbe@1309: 
bsw/jbe@1309:                 var el = document.createElement("div");
bsw/jbe@1309:                 el.innerHTML = "123";
bsw/jbe@1309:                 var textNode = el.firstChild;
bsw/jbe@1309:                 var body = getBody(document);
bsw/jbe@1309:                 body.appendChild(el);
bsw/jbe@1309: 
bsw/jbe@1309:                 range.setStart(textNode, 1);
bsw/jbe@1309:                 range.setEnd(textNode, 2);
bsw/jbe@1309:                 range.deleteContents();
bsw/jbe@1309: 
bsw/jbe@1309:                 if (textNode.data == "13") {
bsw/jbe@1309:                     // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
bsw/jbe@1309:                     // extractContents()
bsw/jbe@1309:                     rangeProto.deleteContents = function() {
bsw/jbe@1309:                         this.nativeRange.deleteContents();
bsw/jbe@1309:                         updateRangeProperties(this);
bsw/jbe@1309:                     };
bsw/jbe@1309: 
bsw/jbe@1309:                     rangeProto.extractContents = function() {
bsw/jbe@1309:                         var frag = this.nativeRange.extractContents();
bsw/jbe@1309:                         updateRangeProperties(this);
bsw/jbe@1309:                         return frag;
bsw/jbe@1309:                     };
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 body.removeChild(el);
bsw/jbe@1309:                 body = null;
bsw/jbe@1309: 
bsw/jbe@1309:                 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:                 // Test for existence of createContextualFragment and delegate to it if it exists
bsw/jbe@1309:                 if (util.isHostMethod(range, "createContextualFragment")) {
bsw/jbe@1309:                     rangeProto.createContextualFragment = function(fragmentStr) {
bsw/jbe@1309:                         return this.nativeRange.createContextualFragment(fragmentStr);
bsw/jbe@1309:                     };
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:                 // Clean up
bsw/jbe@1309:                 getBody(document).removeChild(testTextNode);
bsw/jbe@1309: 
bsw/jbe@1309:                 rangeProto.getName = function() {
bsw/jbe@1309:                     return "WrappedRange";
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 api.WrappedRange = WrappedRange;
bsw/jbe@1309: 
bsw/jbe@1309:                 api.createNativeRange = function(doc) {
bsw/jbe@1309:                     doc = getContentDocument(doc, module, "createNativeRange");
bsw/jbe@1309:                     return doc.createRange();
bsw/jbe@1309:                 };
bsw/jbe@1309:             })();
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (api.features.implementsTextRange) {
bsw/jbe@1309:             /*
bsw/jbe@1309:             This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
bsw/jbe@1309:             method. For example, in the following (where pipes denote the selection boundaries):
bsw/jbe@1309: 
bsw/jbe@1309:             
 element, for example), we need a slightly complicated approach to get the boundary's offset in
bsw/jbe@1309:                         IE. The facts:
bsw/jbe@1309: 
bsw/jbe@1309:                         - Each line break is represented as \r in the text node's data/nodeValue properties
bsw/jbe@1309:                         - Each line break is represented as \r\n in the TextRange's 'text' property
bsw/jbe@1309:                         - The 'text' property of the TextRange does not contain trailing line breaks
bsw/jbe@1309: 
bsw/jbe@1309:                         To get round the problem presented by the final fact above, we can use the fact that TextRange's
bsw/jbe@1309:                         moveStart() and moveEnd() methods return the actual number of characters moved, which is not
bsw/jbe@1309:                         necessarily the same as the number of characters it was instructed to move. The simplest approach is
bsw/jbe@1309:                         to use this to store the characters moved when moving both the start and end of the range to the
bsw/jbe@1309:                         start of the document body and subtracting the start offset from the end offset (the
bsw/jbe@1309:                         "move-negative-gazillion" method). However, this is extremely slow when the document is large and
bsw/jbe@1309:                         the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
bsw/jbe@1309:                         the end of the document) has the same problem.
bsw/jbe@1309: 
bsw/jbe@1309:                         Another approach that works is to use moveStart() to move the start boundary of the range up to the
bsw/jbe@1309:                         end boundary one character at a time and incrementing a counter with the value returned by the
bsw/jbe@1309:                         moveStart() call. However, the check for whether the start boundary has reached the end boundary is
bsw/jbe@1309:                         expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
bsw/jbe@1309:                         by the location of the range within the document).
bsw/jbe@1309: 
bsw/jbe@1309:                         The approach used below is a hybrid of the two methods above. It uses the fact that a string
bsw/jbe@1309:                         containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
bsw/jbe@1309:                         be longer than the text of the TextRange, so the start of the range is moved that length initially
bsw/jbe@1309:                         and then a character at a time to make up for any trailing line breaks not contained in the 'text'
bsw/jbe@1309:                         property. This has good performance in most situations compared to the previous two methods.
bsw/jbe@1309:                         */
bsw/jbe@1309:                         var tempRange = workingRange.duplicate();
bsw/jbe@1309:                         var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
bsw/jbe@1309: 
bsw/jbe@1309:                         offset = tempRange.moveStart("character", rangeLength);
bsw/jbe@1309:                         while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
bsw/jbe@1309:                             offset++;
bsw/jbe@1309:                             tempRange.moveStart("character", 1);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         offset = workingRange.text.length;
bsw/jbe@1309:                     }
bsw/jbe@1309:                     boundaryPosition = new DomPosition(boundaryNode, offset);
bsw/jbe@1309:                 } else {
bsw/jbe@1309: 
bsw/jbe@1309:                     // If the boundary immediately follows a character data node and this is the end boundary, we should favour
bsw/jbe@1309:                     // a position within that, and likewise for a start boundary preceding a character data node
bsw/jbe@1309:                     previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
bsw/jbe@1309:                     nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
bsw/jbe@1309:                     if (nextNode && isCharacterDataNode(nextNode)) {
bsw/jbe@1309:                         boundaryPosition = new DomPosition(nextNode, 0);
bsw/jbe@1309:                     } else if (previousNode && isCharacterDataNode(previousNode)) {
bsw/jbe@1309:                         boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Clean up
bsw/jbe@1309:                 dom.removeNode(workingNode);
bsw/jbe@1309: 
bsw/jbe@1309:                 return {
bsw/jbe@1309:                     boundaryPosition: boundaryPosition,
bsw/jbe@1309:                     nodeInfo: {
bsw/jbe@1309:                         nodeIndex: nodeIndex,
bsw/jbe@1309:                         containerElement: containerElement
bsw/jbe@1309:                     }
bsw/jbe@1309:                 };
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
bsw/jbe@1309:             // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
bsw/jbe@1309:             // (http://code.google.com/p/ierange/)
bsw/jbe@1309:             var createBoundaryTextRange = function(boundaryPosition, isStart) {
bsw/jbe@1309:                 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
bsw/jbe@1309:                 var doc = dom.getDocument(boundaryPosition.node);
bsw/jbe@1309:                 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
bsw/jbe@1309:                 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (nodeIsDataNode) {
bsw/jbe@1309:                     boundaryNode = boundaryPosition.node;
bsw/jbe@1309:                     boundaryParent = boundaryNode.parentNode;
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     childNodes = boundaryPosition.node.childNodes;
bsw/jbe@1309:                     boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
bsw/jbe@1309:                     boundaryParent = boundaryPosition.node;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Position the range immediately before the node containing the boundary
bsw/jbe@1309:                 workingNode = doc.createElement("span");
bsw/jbe@1309: 
bsw/jbe@1309:                 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
bsw/jbe@1309:                 // the element rather than immediately before or after it
bsw/jbe@1309:                 workingNode.innerHTML = "feff;";
bsw/jbe@1309: 
bsw/jbe@1309:                 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
bsw/jbe@1309:                 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
bsw/jbe@1309:                 if (boundaryNode) {
bsw/jbe@1309:                     boundaryParent.insertBefore(workingNode, boundaryNode);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     boundaryParent.appendChild(workingNode);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 workingRange.moveToElementText(workingNode);
bsw/jbe@1309:                 workingRange.collapse(!isStart);
bsw/jbe@1309: 
bsw/jbe@1309:                 // Clean up
bsw/jbe@1309:                 boundaryParent.removeChild(workingNode);
bsw/jbe@1309: 
bsw/jbe@1309:                 // Move the working range to the text offset, if required
bsw/jbe@1309:                 if (nodeIsDataNode) {
bsw/jbe@1309:                     workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 return workingRange;
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             /*------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:             // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
bsw/jbe@1309:             // prototype
bsw/jbe@1309: 
bsw/jbe@1309:             WrappedTextRange = function(textRange) {
bsw/jbe@1309:                 this.textRange = textRange;
bsw/jbe@1309:                 this.refresh();
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             WrappedTextRange.prototype = new DomRange(document);
bsw/jbe@1309: 
bsw/jbe@1309:             WrappedTextRange.prototype.refresh = function() {
bsw/jbe@1309:                 var start, end, startBoundary;
bsw/jbe@1309: 
bsw/jbe@1309:                 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
bsw/jbe@1309:                 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
bsw/jbe@1309: 
bsw/jbe@1309:                 if (textRangeIsCollapsed(this.textRange)) {
bsw/jbe@1309:                     end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
bsw/jbe@1309:                         true).boundaryPosition;
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
bsw/jbe@1309:                     start = startBoundary.boundaryPosition;
bsw/jbe@1309: 
bsw/jbe@1309:                     // An optimization used here is that if the start and end boundaries have the same parent element, the
bsw/jbe@1309:                     // search scope for the end boundary can be limited to exclude the portion of the element that precedes
bsw/jbe@1309:                     // the start boundary
bsw/jbe@1309:                     end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
bsw/jbe@1309:                         startBoundary.nodeInfo).boundaryPosition;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 this.setStart(start.node, start.offset);
bsw/jbe@1309:                 this.setEnd(end.node, end.offset);
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             WrappedTextRange.prototype.getName = function() {
bsw/jbe@1309:                 return "WrappedTextRange";
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             DomRange.copyComparisonConstants(WrappedTextRange);
bsw/jbe@1309: 
bsw/jbe@1309:             var rangeToTextRange = function(range) {
bsw/jbe@1309:                 if (range.collapsed) {
bsw/jbe@1309:                     return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
bsw/jbe@1309:                     var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
bsw/jbe@1309:                     var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
bsw/jbe@1309:                     textRange.setEndPoint("StartToStart", startRange);
bsw/jbe@1309:                     textRange.setEndPoint("EndToEnd", endRange);
bsw/jbe@1309:                     return textRange;
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             WrappedTextRange.rangeToTextRange = rangeToTextRange;
bsw/jbe@1309: 
bsw/jbe@1309:             WrappedTextRange.prototype.toTextRange = function() {
bsw/jbe@1309:                 return rangeToTextRange(this);
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             api.WrappedTextRange = WrappedTextRange;
bsw/jbe@1309: 
bsw/jbe@1309:             // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
bsw/jbe@1309:             // implementation to use by default.
bsw/jbe@1309:             if (!api.features.implementsDomRange || api.config.preferTextRange) {
bsw/jbe@1309:                 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
bsw/jbe@1309:                 var globalObj = (function(f) { return f("return this;")(); })(Function);
bsw/jbe@1309:                 if (typeof globalObj.Range == "undefined") {
bsw/jbe@1309:                     globalObj.Range = WrappedTextRange;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 api.createNativeRange = function(doc) {
bsw/jbe@1309:                     doc = getContentDocument(doc, module, "createNativeRange");
bsw/jbe@1309:                     return getBody(doc).createTextRange();
bsw/jbe@1309:                 };
bsw/jbe@1309: 
bsw/jbe@1309:                 api.WrappedRange = WrappedTextRange;
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         api.createRange = function(doc) {
bsw/jbe@1309:             doc = getContentDocument(doc, module, "createRange");
bsw/jbe@1309:             return new api.WrappedRange(api.createNativeRange(doc));
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         api.createRangyRange = function(doc) {
bsw/jbe@1309:             doc = getContentDocument(doc, module, "createRangyRange");
bsw/jbe@1309:             return new DomRange(doc);
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
bsw/jbe@1309:         util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
bsw/jbe@1309: 
bsw/jbe@1309:         api.addShimListener(function(win) {
bsw/jbe@1309:             var doc = win.document;
bsw/jbe@1309:             if (typeof doc.createRange == "undefined") {
bsw/jbe@1309:                 doc.createRange = function() {
bsw/jbe@1309:                     return api.createRange(doc);
bsw/jbe@1309:                 };
bsw/jbe@1309:             }
bsw/jbe@1309:             doc = win = null;
bsw/jbe@1309:         });
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
bsw/jbe@1309:     // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
bsw/jbe@1309:     api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
bsw/jbe@1309:         api.config.checkSelectionRanges = true;
bsw/jbe@1309: 
bsw/jbe@1309:         var BOOLEAN = "boolean";
bsw/jbe@1309:         var NUMBER = "number";
bsw/jbe@1309:         var dom = api.dom;
bsw/jbe@1309:         var util = api.util;
bsw/jbe@1309:         var isHostMethod = util.isHostMethod;
bsw/jbe@1309:         var DomRange = api.DomRange;
bsw/jbe@1309:         var WrappedRange = api.WrappedRange;
bsw/jbe@1309:         var DOMException = api.DOMException;
bsw/jbe@1309:         var DomPosition = dom.DomPosition;
bsw/jbe@1309:         var getNativeSelection;
bsw/jbe@1309:         var selectionIsCollapsed;
bsw/jbe@1309:         var features = api.features;
bsw/jbe@1309:         var CONTROL = "Control";
bsw/jbe@1309:         var getDocument = dom.getDocument;
bsw/jbe@1309:         var getBody = dom.getBody;
bsw/jbe@1309:         var rangesEqual = DomRange.rangesEqual;
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:         // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
bsw/jbe@1309:         // "forward" or "forwards") or a Boolean (true for backwards).
bsw/jbe@1309:         function isDirectionBackward(dir) {
bsw/jbe@1309:             return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getWindow(win, methodName) {
bsw/jbe@1309:             if (!win) {
bsw/jbe@1309:                 return window;
bsw/jbe@1309:             } else if (dom.isWindow(win)) {
bsw/jbe@1309:                 return win;
bsw/jbe@1309:             } else if (win instanceof WrappedSelection) {
bsw/jbe@1309:                 return win.win;
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 var doc = dom.getContentDocument(win, module, methodName);
bsw/jbe@1309:                 return dom.getWindow(doc);
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getWinSelection(winParam) {
bsw/jbe@1309:             return getWindow(winParam, "getWinSelection").getSelection();
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getDocSelection(winParam) {
bsw/jbe@1309:             return getWindow(winParam, "getDocSelection").document.selection;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function winSelectionIsBackward(sel) {
bsw/jbe@1309:             var backward = false;
bsw/jbe@1309:             if (sel.anchorNode) {
bsw/jbe@1309:                 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
bsw/jbe@1309:             }
bsw/jbe@1309:             return backward;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Test for the Range/TextRange and Selection features required
bsw/jbe@1309:         // Test for ability to retrieve selection
bsw/jbe@1309:         var implementsWinGetSelection = isHostMethod(window, "getSelection"),
bsw/jbe@1309:             implementsDocSelection = util.isHostObject(document, "selection");
bsw/jbe@1309: 
bsw/jbe@1309:         features.implementsWinGetSelection = implementsWinGetSelection;
bsw/jbe@1309:         features.implementsDocSelection = implementsDocSelection;
bsw/jbe@1309: 
bsw/jbe@1309:         var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
bsw/jbe@1309: 
bsw/jbe@1309:         if (useDocumentSelection) {
bsw/jbe@1309:             getNativeSelection = getDocSelection;
bsw/jbe@1309:             api.isSelectionValid = function(winParam) {
bsw/jbe@1309:                 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
bsw/jbe@1309: 
bsw/jbe@1309:                 // Check whether the selection TextRange is actually contained within the correct document
bsw/jbe@1309:                 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (implementsWinGetSelection) {
bsw/jbe@1309:             getNativeSelection = getWinSelection;
bsw/jbe@1309:             api.isSelectionValid = function() {
bsw/jbe@1309:                 return true;
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             module.fail("Neither document.selection or window.getSelection() detected.");
bsw/jbe@1309:             return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         api.getNativeSelection = getNativeSelection;
bsw/jbe@1309: 
bsw/jbe@1309:         var testSelection = getNativeSelection();
bsw/jbe@1309: 
bsw/jbe@1309:         // In Firefox, the selection is null in an iframe with display: none. See issue #138.
bsw/jbe@1309:         if (!testSelection) {
bsw/jbe@1309:             module.fail("Native selection was null (possibly issue 138?)");
bsw/jbe@1309:             return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var testRange = api.createNativeRange(document);
bsw/jbe@1309:         var body = getBody(document);
bsw/jbe@1309: 
bsw/jbe@1309:         // Obtaining a range from a selection
bsw/jbe@1309:         var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
bsw/jbe@1309:             ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
bsw/jbe@1309: 
bsw/jbe@1309:         features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
bsw/jbe@1309: 
bsw/jbe@1309:         // Test for existence of native selection extend() method
bsw/jbe@1309:         var selectionHasExtend = isHostMethod(testSelection, "extend");
bsw/jbe@1309:         features.selectionHasExtend = selectionHasExtend;
bsw/jbe@1309: 
bsw/jbe@1309:         // Test if rangeCount exists
bsw/jbe@1309:         var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
bsw/jbe@1309:         features.selectionHasRangeCount = selectionHasRangeCount;
bsw/jbe@1309: 
bsw/jbe@1309:         var selectionSupportsMultipleRanges = false;
bsw/jbe@1309:         var collapsedNonEditableSelectionsSupported = true;
bsw/jbe@1309: 
bsw/jbe@1309:         var addRangeBackwardToNative = selectionHasExtend ?
bsw/jbe@1309:             function(nativeSelection, range) {
bsw/jbe@1309:                 var doc = DomRange.getRangeDocument(range);
bsw/jbe@1309:                 var endRange = api.createRange(doc);
bsw/jbe@1309:                 endRange.collapseToPoint(range.endContainer, range.endOffset);
bsw/jbe@1309:                 nativeSelection.addRange(getNativeRange(endRange));
bsw/jbe@1309:                 nativeSelection.extend(range.startContainer, range.startOffset);
bsw/jbe@1309:             } : null;
bsw/jbe@1309: 
bsw/jbe@1309:         if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
bsw/jbe@1309:                 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
bsw/jbe@1309: 
bsw/jbe@1309:             (function() {
bsw/jbe@1309:                 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
bsw/jbe@1309:                 // performed on the current document's selection. See issue 109.
bsw/jbe@1309: 
bsw/jbe@1309:                 // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
bsw/jbe@1309:                 // will result in the selection direction begin reversed if the original selection was backwards and the
bsw/jbe@1309:                 // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
bsw/jbe@1309:                 var sel = window.getSelection();
bsw/jbe@1309:                 if (sel) {
bsw/jbe@1309:                     // Store the current selection
bsw/jbe@1309:                     var originalSelectionRangeCount = sel.rangeCount;
bsw/jbe@1309:                     var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
bsw/jbe@1309:                     var originalSelectionRanges = [];
bsw/jbe@1309:                     var originalSelectionBackward = winSelectionIsBackward(sel);
bsw/jbe@1309:                     for (var i = 0; i < originalSelectionRangeCount; ++i) {
bsw/jbe@1309:                         originalSelectionRanges[i] = sel.getRangeAt(i);
bsw/jbe@1309:                     }
bsw/jbe@1309: 
bsw/jbe@1309:                     // Create some test elements
bsw/jbe@1309:                     var testEl = dom.createTestElement(document, "", false);
bsw/jbe@1309:                     var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
bsw/jbe@1309: 
bsw/jbe@1309:                     // Test whether the native selection will allow a collapsed selection within a non-editable element
bsw/jbe@1309:                     var r1 = document.createRange();
bsw/jbe@1309: 
bsw/jbe@1309:                     r1.setStart(textNode, 1);
bsw/jbe@1309:                     r1.collapse(true);
bsw/jbe@1309:                     sel.removeAllRanges();
bsw/jbe@1309:                     sel.addRange(r1);
bsw/jbe@1309:                     collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
bsw/jbe@1309:                     sel.removeAllRanges();
bsw/jbe@1309: 
bsw/jbe@1309:                     // Test whether the native selection is capable of supporting multiple ranges.
bsw/jbe@1309:                     if (!selectionHasMultipleRanges) {
bsw/jbe@1309:                         // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
bsw/jbe@1309:                         // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
bsw/jbe@1309:                         // nothing we can do about this while retaining the feature test so we have to resort to a browser
bsw/jbe@1309:                         // sniff. I'm not happy about it. See
bsw/jbe@1309:                         // https://code.google.com/p/chromium/issues/detail?id=399791
bsw/jbe@1309:                         var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
bsw/jbe@1309:                         if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
bsw/jbe@1309:                             selectionSupportsMultipleRanges = false;
bsw/jbe@1309:                         } else {
bsw/jbe@1309:                             var r2 = r1.cloneRange();
bsw/jbe@1309:                             r1.setStart(textNode, 0);
bsw/jbe@1309:                             r2.setEnd(textNode, 3);
bsw/jbe@1309:                             r2.setStart(textNode, 2);
bsw/jbe@1309:                             sel.addRange(r1);
bsw/jbe@1309:                             sel.addRange(r2);
bsw/jbe@1309:                             selectionSupportsMultipleRanges = (sel.rangeCount == 2);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309: 
bsw/jbe@1309:                     // Clean up
bsw/jbe@1309:                     dom.removeNode(testEl);
bsw/jbe@1309:                     sel.removeAllRanges();
bsw/jbe@1309: 
bsw/jbe@1309:                     for (i = 0; i < originalSelectionRangeCount; ++i) {
bsw/jbe@1309:                         if (i == 0 && originalSelectionBackward) {
bsw/jbe@1309:                             if (addRangeBackwardToNative) {
bsw/jbe@1309:                                 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
bsw/jbe@1309:                             } else {
bsw/jbe@1309:                                 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
bsw/jbe@1309:                                 sel.addRange(originalSelectionRanges[i]);
bsw/jbe@1309:                             }
bsw/jbe@1309:                         } else {
bsw/jbe@1309:                             sel.addRange(originalSelectionRanges[i]);
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             })();
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
bsw/jbe@1309:         features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
bsw/jbe@1309: 
bsw/jbe@1309:         // ControlRanges
bsw/jbe@1309:         var implementsControlRange = false, testControlRange;
bsw/jbe@1309: 
bsw/jbe@1309:         if (body && isHostMethod(body, "createControlRange")) {
bsw/jbe@1309:             testControlRange = body.createControlRange();
bsw/jbe@1309:             if (util.areHostProperties(testControlRange, ["item", "add"])) {
bsw/jbe@1309:                 implementsControlRange = true;
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309:         features.implementsControlRange = implementsControlRange;
bsw/jbe@1309: 
bsw/jbe@1309:         // Selection collapsedness
bsw/jbe@1309:         if (selectionHasAnchorAndFocus) {
bsw/jbe@1309:             selectionIsCollapsed = function(sel) {
bsw/jbe@1309:                 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             selectionIsCollapsed = function(sel) {
bsw/jbe@1309:                 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function updateAnchorAndFocusFromRange(sel, range, backward) {
bsw/jbe@1309:             var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
bsw/jbe@1309:             sel.anchorNode = range[anchorPrefix + "Container"];
bsw/jbe@1309:             sel.anchorOffset = range[anchorPrefix + "Offset"];
bsw/jbe@1309:             sel.focusNode = range[focusPrefix + "Container"];
bsw/jbe@1309:             sel.focusOffset = range[focusPrefix + "Offset"];
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function updateAnchorAndFocusFromNativeSelection(sel) {
bsw/jbe@1309:             var nativeSel = sel.nativeSelection;
bsw/jbe@1309:             sel.anchorNode = nativeSel.anchorNode;
bsw/jbe@1309:             sel.anchorOffset = nativeSel.anchorOffset;
bsw/jbe@1309:             sel.focusNode = nativeSel.focusNode;
bsw/jbe@1309:             sel.focusOffset = nativeSel.focusOffset;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function updateEmptySelection(sel) {
bsw/jbe@1309:             sel.anchorNode = sel.focusNode = null;
bsw/jbe@1309:             sel.anchorOffset = sel.focusOffset = 0;
bsw/jbe@1309:             sel.rangeCount = 0;
bsw/jbe@1309:             sel.isCollapsed = true;
bsw/jbe@1309:             sel._ranges.length = 0;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getNativeRange(range) {
bsw/jbe@1309:             var nativeRange;
bsw/jbe@1309:             if (range instanceof DomRange) {
bsw/jbe@1309:                 nativeRange = api.createNativeRange(range.getDocument());
bsw/jbe@1309:                 nativeRange.setEnd(range.endContainer, range.endOffset);
bsw/jbe@1309:                 nativeRange.setStart(range.startContainer, range.startOffset);
bsw/jbe@1309:             } else if (range instanceof WrappedRange) {
bsw/jbe@1309:                 nativeRange = range.nativeRange;
bsw/jbe@1309:             } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
bsw/jbe@1309:                 nativeRange = range;
bsw/jbe@1309:             }
bsw/jbe@1309:             return nativeRange;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function rangeContainsSingleElement(rangeNodes) {
bsw/jbe@1309:             if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
bsw/jbe@1309:                 return false;
bsw/jbe@1309:             }
bsw/jbe@1309:             for (var i = 1, len = rangeNodes.length; i < len; ++i) {
bsw/jbe@1309:                 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
bsw/jbe@1309:                     return false;
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             return true;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function getSingleElementFromRange(range) {
bsw/jbe@1309:             var nodes = range.getNodes();
bsw/jbe@1309:             if (!rangeContainsSingleElement(nodes)) {
bsw/jbe@1309:                 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
bsw/jbe@1309:             }
bsw/jbe@1309:             return nodes[0];
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
bsw/jbe@1309:         function isTextRange(range) {
bsw/jbe@1309:             return !!range && typeof range.text != "undefined";
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function updateFromTextRange(sel, range) {
bsw/jbe@1309:             // Create a Range from the selected TextRange
bsw/jbe@1309:             var wrappedRange = new WrappedRange(range);
bsw/jbe@1309:             sel._ranges = [wrappedRange];
bsw/jbe@1309: 
bsw/jbe@1309:             updateAnchorAndFocusFromRange(sel, wrappedRange, false);
bsw/jbe@1309:             sel.rangeCount = 1;
bsw/jbe@1309:             sel.isCollapsed = wrappedRange.collapsed;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function updateControlSelection(sel) {
bsw/jbe@1309:             // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309:             sel._ranges.length = 0;
bsw/jbe@1309:             if (sel.docSelection.type == "None") {
bsw/jbe@1309:                 updateEmptySelection(sel);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 var controlRange = sel.docSelection.createRange();
bsw/jbe@1309:                 if (isTextRange(controlRange)) {
bsw/jbe@1309:                     // This case (where the selection type is "Control" and calling createRange() on the selection returns
bsw/jbe@1309:                     // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
bsw/jbe@1309:                     // ControlRange have been removed from the ControlRange and removed from the document.
bsw/jbe@1309:                     updateFromTextRange(sel, controlRange);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     sel.rangeCount = controlRange.length;
bsw/jbe@1309:                     var range, doc = getDocument(controlRange.item(0));
bsw/jbe@1309:                     for (var i = 0; i < sel.rangeCount; ++i) {
bsw/jbe@1309:                         range = api.createRange(doc);
bsw/jbe@1309:                         range.selectNode(controlRange.item(i));
bsw/jbe@1309:                         sel._ranges.push(range);
bsw/jbe@1309:                     }
bsw/jbe@1309:                     sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
bsw/jbe@1309:                     updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function addRangeToControlSelection(sel, range) {
bsw/jbe@1309:             var controlRange = sel.docSelection.createRange();
bsw/jbe@1309:             var rangeElement = getSingleElementFromRange(range);
bsw/jbe@1309: 
bsw/jbe@1309:             // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
bsw/jbe@1309:             // contained by the supplied range
bsw/jbe@1309:             var doc = getDocument(controlRange.item(0));
bsw/jbe@1309:             var newControlRange = getBody(doc).createControlRange();
bsw/jbe@1309:             for (var i = 0, len = controlRange.length; i < len; ++i) {
bsw/jbe@1309:                 newControlRange.add(controlRange.item(i));
bsw/jbe@1309:             }
bsw/jbe@1309:             try {
bsw/jbe@1309:                 newControlRange.add(rangeElement);
bsw/jbe@1309:             } catch (ex) {
bsw/jbe@1309:                 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
bsw/jbe@1309:             }
bsw/jbe@1309:             newControlRange.select();
bsw/jbe@1309: 
bsw/jbe@1309:             // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309:             updateControlSelection(sel);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var getSelectionRangeAt;
bsw/jbe@1309: 
bsw/jbe@1309:         if (isHostMethod(testSelection, "getRangeAt")) {
bsw/jbe@1309:             // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
bsw/jbe@1309:             // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
bsw/jbe@1309:             // lesson to us all, especially me.
bsw/jbe@1309:             getSelectionRangeAt = function(sel, index) {
bsw/jbe@1309:                 try {
bsw/jbe@1309:                     return sel.getRangeAt(index);
bsw/jbe@1309:                 } catch (ex) {
bsw/jbe@1309:                     return null;
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (selectionHasAnchorAndFocus) {
bsw/jbe@1309:             getSelectionRangeAt = function(sel) {
bsw/jbe@1309:                 var doc = getDocument(sel.anchorNode);
bsw/jbe@1309:                 var range = api.createRange(doc);
bsw/jbe@1309:                 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
bsw/jbe@1309: 
bsw/jbe@1309:                 // Handle the case when the selection was selected backwards (from the end to the start in the
bsw/jbe@1309:                 // document)
bsw/jbe@1309:                 if (range.collapsed !== this.isCollapsed) {
bsw/jbe@1309:                     range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 return range;
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function WrappedSelection(selection, docSelection, win) {
bsw/jbe@1309:             this.nativeSelection = selection;
bsw/jbe@1309:             this.docSelection = docSelection;
bsw/jbe@1309:             this._ranges = [];
bsw/jbe@1309:             this.win = win;
bsw/jbe@1309:             this.refresh();
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         WrappedSelection.prototype = api.selectionPrototype;
bsw/jbe@1309: 
bsw/jbe@1309:         function deleteProperties(sel) {
bsw/jbe@1309:             sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
bsw/jbe@1309:             sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
bsw/jbe@1309:             sel.detached = true;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var cachedRangySelections = [];
bsw/jbe@1309: 
bsw/jbe@1309:         function actOnCachedSelection(win, action) {
bsw/jbe@1309:             var i = cachedRangySelections.length, cached, sel;
bsw/jbe@1309:             while (i--) {
bsw/jbe@1309:                 cached = cachedRangySelections[i];
bsw/jbe@1309:                 sel = cached.selection;
bsw/jbe@1309:                 if (action == "deleteAll") {
bsw/jbe@1309:                     deleteProperties(sel);
bsw/jbe@1309:                 } else if (cached.win == win) {
bsw/jbe@1309:                     if (action == "delete") {
bsw/jbe@1309:                         cachedRangySelections.splice(i, 1);
bsw/jbe@1309:                         return true;
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         return sel;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             if (action == "deleteAll") {
bsw/jbe@1309:                 cachedRangySelections.length = 0;
bsw/jbe@1309:             }
bsw/jbe@1309:             return null;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var getSelection = function(win) {
bsw/jbe@1309:             // Check if the parameter is a Rangy Selection object
bsw/jbe@1309:             if (win && win instanceof WrappedSelection) {
bsw/jbe@1309:                 win.refresh();
bsw/jbe@1309:                 return win;
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             win = getWindow(win, "getNativeSelection");
bsw/jbe@1309: 
bsw/jbe@1309:             var sel = actOnCachedSelection(win);
bsw/jbe@1309:             var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
bsw/jbe@1309:             if (sel) {
bsw/jbe@1309:                 sel.nativeSelection = nativeSel;
bsw/jbe@1309:                 sel.docSelection = docSel;
bsw/jbe@1309:                 sel.refresh();
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 sel = new WrappedSelection(nativeSel, docSel, win);
bsw/jbe@1309:                 cachedRangySelections.push( { win: win, selection: sel } );
bsw/jbe@1309:             }
bsw/jbe@1309:             return sel;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         api.getSelection = getSelection;
bsw/jbe@1309: 
bsw/jbe@1309:         util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
bsw/jbe@1309: 
bsw/jbe@1309:         var selProto = WrappedSelection.prototype;
bsw/jbe@1309: 
bsw/jbe@1309:         function createControlSelection(sel, ranges) {
bsw/jbe@1309:             // Ensure that the selection becomes of type "Control"
bsw/jbe@1309:             var doc = getDocument(ranges[0].startContainer);
bsw/jbe@1309:             var controlRange = getBody(doc).createControlRange();
bsw/jbe@1309:             for (var i = 0, el, len = ranges.length; i < len; ++i) {
bsw/jbe@1309:                 el = getSingleElementFromRange(ranges[i]);
bsw/jbe@1309:                 try {
bsw/jbe@1309:                     controlRange.add(el);
bsw/jbe@1309:                 } catch (ex) {
bsw/jbe@1309:                     throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             controlRange.select();
bsw/jbe@1309: 
bsw/jbe@1309:             // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309:             updateControlSelection(sel);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Selecting a range
bsw/jbe@1309:         if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
bsw/jbe@1309:             selProto.removeAllRanges = function() {
bsw/jbe@1309:                 this.nativeSelection.removeAllRanges();
bsw/jbe@1309:                 updateEmptySelection(this);
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             var addRangeBackward = function(sel, range) {
bsw/jbe@1309:                 addRangeBackwardToNative(sel.nativeSelection, range);
bsw/jbe@1309:                 sel.refresh();
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             if (selectionHasRangeCount) {
bsw/jbe@1309:                 selProto.addRange = function(range, direction) {
bsw/jbe@1309:                     if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
bsw/jbe@1309:                         addRangeToControlSelection(this, range);
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         if (isDirectionBackward(direction) && selectionHasExtend) {
bsw/jbe@1309:                             addRangeBackward(this, range);
bsw/jbe@1309:                         } else {
bsw/jbe@1309:                             var previousRangeCount;
bsw/jbe@1309:                             if (selectionSupportsMultipleRanges) {
bsw/jbe@1309:                                 previousRangeCount = this.rangeCount;
bsw/jbe@1309:                             } else {
bsw/jbe@1309:                                 this.removeAllRanges();
bsw/jbe@1309:                                 previousRangeCount = 0;
bsw/jbe@1309:                             }
bsw/jbe@1309:                             // Clone the native range so that changing the selected range does not affect the selection.
bsw/jbe@1309:                             // This is contrary to the spec but is the only way to achieve consistency between browsers. See
bsw/jbe@1309:                             // issue 80.
bsw/jbe@1309:                             var clonedNativeRange = getNativeRange(range).cloneRange();
bsw/jbe@1309:                             try {
bsw/jbe@1309:                                 this.nativeSelection.addRange(clonedNativeRange);
bsw/jbe@1309:                             } catch (ex) {
bsw/jbe@1309:                             }
bsw/jbe@1309: 
bsw/jbe@1309:                             // Check whether adding the range was successful
bsw/jbe@1309:                             this.rangeCount = this.nativeSelection.rangeCount;
bsw/jbe@1309: 
bsw/jbe@1309:                             if (this.rangeCount == previousRangeCount + 1) {
bsw/jbe@1309:                                 // The range was added successfully
bsw/jbe@1309: 
bsw/jbe@1309:                                 // Check whether the range that we added to the selection is reflected in the last range extracted from
bsw/jbe@1309:                                 // the selection
bsw/jbe@1309:                                 if (api.config.checkSelectionRanges) {
bsw/jbe@1309:                                     var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
bsw/jbe@1309:                                     if (nativeRange && !rangesEqual(nativeRange, range)) {
bsw/jbe@1309:                                         // Happens in WebKit with, for example, a selection placed at the start of a text node
bsw/jbe@1309:                                         range = new WrappedRange(nativeRange);
bsw/jbe@1309:                                     }
bsw/jbe@1309:                                 }
bsw/jbe@1309:                                 this._ranges[this.rangeCount - 1] = range;
bsw/jbe@1309:                                 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
bsw/jbe@1309:                                 this.isCollapsed = selectionIsCollapsed(this);
bsw/jbe@1309:                             } else {
bsw/jbe@1309:                                 // The range was not added successfully. The simplest thing is to refresh
bsw/jbe@1309:                                 this.refresh();
bsw/jbe@1309:                             }
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 };
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 selProto.addRange = function(range, direction) {
bsw/jbe@1309:                     if (isDirectionBackward(direction) && selectionHasExtend) {
bsw/jbe@1309:                         addRangeBackward(this, range);
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         this.nativeSelection.addRange(getNativeRange(range));
bsw/jbe@1309:                         this.refresh();
bsw/jbe@1309:                     }
bsw/jbe@1309:                 };
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             selProto.setRanges = function(ranges) {
bsw/jbe@1309:                 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
bsw/jbe@1309:                     createControlSelection(this, ranges);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     this.removeAllRanges();
bsw/jbe@1309:                     for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309:                         this.addRange(ranges[i]);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
bsw/jbe@1309:                    implementsControlRange && useDocumentSelection) {
bsw/jbe@1309: 
bsw/jbe@1309:             selProto.removeAllRanges = function() {
bsw/jbe@1309:                 // Added try/catch as fix for issue #21
bsw/jbe@1309:                 try {
bsw/jbe@1309:                     this.docSelection.empty();
bsw/jbe@1309: 
bsw/jbe@1309:                     // Check for empty() not working (issue #24)
bsw/jbe@1309:                     if (this.docSelection.type != "None") {
bsw/jbe@1309:                         // Work around failure to empty a control selection by instead selecting a TextRange and then
bsw/jbe@1309:                         // calling empty()
bsw/jbe@1309:                         var doc;
bsw/jbe@1309:                         if (this.anchorNode) {
bsw/jbe@1309:                             doc = getDocument(this.anchorNode);
bsw/jbe@1309:                         } else if (this.docSelection.type == CONTROL) {
bsw/jbe@1309:                             var controlRange = this.docSelection.createRange();
bsw/jbe@1309:                             if (controlRange.length) {
bsw/jbe@1309:                                 doc = getDocument( controlRange.item(0) );
bsw/jbe@1309:                             }
bsw/jbe@1309:                         }
bsw/jbe@1309:                         if (doc) {
bsw/jbe@1309:                             var textRange = getBody(doc).createTextRange();
bsw/jbe@1309:                             textRange.select();
bsw/jbe@1309:                             this.docSelection.empty();
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                 } catch(ex) {}
bsw/jbe@1309:                 updateEmptySelection(this);
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             selProto.addRange = function(range) {
bsw/jbe@1309:                 if (this.docSelection.type == CONTROL) {
bsw/jbe@1309:                     addRangeToControlSelection(this, range);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     api.WrappedTextRange.rangeToTextRange(range).select();
bsw/jbe@1309:                     this._ranges[0] = range;
bsw/jbe@1309:                     this.rangeCount = 1;
bsw/jbe@1309:                     this.isCollapsed = this._ranges[0].collapsed;
bsw/jbe@1309:                     updateAnchorAndFocusFromRange(this, range, false);
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309: 
bsw/jbe@1309:             selProto.setRanges = function(ranges) {
bsw/jbe@1309:                 this.removeAllRanges();
bsw/jbe@1309:                 var rangeCount = ranges.length;
bsw/jbe@1309:                 if (rangeCount > 1) {
bsw/jbe@1309:                     createControlSelection(this, ranges);
bsw/jbe@1309:                 } else if (rangeCount) {
bsw/jbe@1309:                     this.addRange(ranges[0]);
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             module.fail("No means of selecting a Range or TextRange was found");
bsw/jbe@1309:             return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.getRangeAt = function(index) {
bsw/jbe@1309:             if (index < 0 || index >= this.rangeCount) {
bsw/jbe@1309:                 throw new DOMException("INDEX_SIZE_ERR");
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 // Clone the range to preserve selection-range independence. See issue 80.
bsw/jbe@1309:                 return this._ranges[index].cloneRange();
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         var refreshSelection;
bsw/jbe@1309: 
bsw/jbe@1309:         if (useDocumentSelection) {
bsw/jbe@1309:             refreshSelection = function(sel) {
bsw/jbe@1309:                 var range;
bsw/jbe@1309:                 if (api.isSelectionValid(sel.win)) {
bsw/jbe@1309:                     range = sel.docSelection.createRange();
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     range = getBody(sel.win.document).createTextRange();
bsw/jbe@1309:                     range.collapse(true);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 if (sel.docSelection.type == CONTROL) {
bsw/jbe@1309:                     updateControlSelection(sel);
bsw/jbe@1309:                 } else if (isTextRange(range)) {
bsw/jbe@1309:                     updateFromTextRange(sel, range);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     updateEmptySelection(sel);
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
bsw/jbe@1309:             refreshSelection = function(sel) {
bsw/jbe@1309:                 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
bsw/jbe@1309:                     updateControlSelection(sel);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
bsw/jbe@1309:                     if (sel.rangeCount) {
bsw/jbe@1309:                         for (var i = 0, len = sel.rangeCount; i < len; ++i) {
bsw/jbe@1309:                             sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
bsw/jbe@1309:                         }
bsw/jbe@1309:                         updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
bsw/jbe@1309:                         sel.isCollapsed = selectionIsCollapsed(sel);
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         updateEmptySelection(sel);
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
bsw/jbe@1309:             refreshSelection = function(sel) {
bsw/jbe@1309:                 var range, nativeSel = sel.nativeSelection;
bsw/jbe@1309:                 if (nativeSel.anchorNode) {
bsw/jbe@1309:                     range = getSelectionRangeAt(nativeSel, 0);
bsw/jbe@1309:                     sel._ranges = [range];
bsw/jbe@1309:                     sel.rangeCount = 1;
bsw/jbe@1309:                     updateAnchorAndFocusFromNativeSelection(sel);
bsw/jbe@1309:                     sel.isCollapsed = selectionIsCollapsed(sel);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     updateEmptySelection(sel);
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
bsw/jbe@1309:             return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.refresh = function(checkForChanges) {
bsw/jbe@1309:             var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
bsw/jbe@1309:             var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
bsw/jbe@1309: 
bsw/jbe@1309:             refreshSelection(this);
bsw/jbe@1309:             if (checkForChanges) {
bsw/jbe@1309:                 // Check the range count first
bsw/jbe@1309:                 var i = oldRanges.length;
bsw/jbe@1309:                 if (i != this._ranges.length) {
bsw/jbe@1309:                     return true;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
bsw/jbe@1309:                 // ranges after this
bsw/jbe@1309:                 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
bsw/jbe@1309:                     return true;
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 // Finally, compare each range in turn
bsw/jbe@1309:                 while (i--) {
bsw/jbe@1309:                     if (!rangesEqual(oldRanges[i], this._ranges[i])) {
bsw/jbe@1309:                         return true;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:                 return false;
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         // Removal of a single range
bsw/jbe@1309:         var removeRangeManually = function(sel, range) {
bsw/jbe@1309:             var ranges = sel.getAllRanges();
bsw/jbe@1309:             sel.removeAllRanges();
bsw/jbe@1309:             for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309:                 if (!rangesEqual(range, ranges[i])) {
bsw/jbe@1309:                     sel.addRange(ranges[i]);
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             if (!sel.rangeCount) {
bsw/jbe@1309:                 updateEmptySelection(sel);
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         if (implementsControlRange && implementsDocSelection) {
bsw/jbe@1309:             selProto.removeRange = function(range) {
bsw/jbe@1309:                 if (this.docSelection.type == CONTROL) {
bsw/jbe@1309:                     var controlRange = this.docSelection.createRange();
bsw/jbe@1309:                     var rangeElement = getSingleElementFromRange(range);
bsw/jbe@1309: 
bsw/jbe@1309:                     // Create a new ControlRange containing all the elements in the selected ControlRange minus the
bsw/jbe@1309:                     // element contained by the supplied range
bsw/jbe@1309:                     var doc = getDocument(controlRange.item(0));
bsw/jbe@1309:                     var newControlRange = getBody(doc).createControlRange();
bsw/jbe@1309:                     var el, removed = false;
bsw/jbe@1309:                     for (var i = 0, len = controlRange.length; i < len; ++i) {
bsw/jbe@1309:                         el = controlRange.item(i);
bsw/jbe@1309:                         if (el !== rangeElement || removed) {
bsw/jbe@1309:                             newControlRange.add(controlRange.item(i));
bsw/jbe@1309:                         } else {
bsw/jbe@1309:                             removed = true;
bsw/jbe@1309:                         }
bsw/jbe@1309:                     }
bsw/jbe@1309:                     newControlRange.select();
bsw/jbe@1309: 
bsw/jbe@1309:                     // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309:                     updateControlSelection(this);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     removeRangeManually(this, range);
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             selProto.removeRange = function(range) {
bsw/jbe@1309:                 removeRangeManually(this, range);
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Detecting if a selection is backward
bsw/jbe@1309:         var selectionIsBackward;
bsw/jbe@1309:         if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
bsw/jbe@1309:             selectionIsBackward = winSelectionIsBackward;
bsw/jbe@1309: 
bsw/jbe@1309:             selProto.isBackward = function() {
bsw/jbe@1309:                 return selectionIsBackward(this);
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             selectionIsBackward = selProto.isBackward = function() {
bsw/jbe@1309:                 return false;
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
bsw/jbe@1309:         selProto.isBackwards = selProto.isBackward;
bsw/jbe@1309: 
bsw/jbe@1309:         // Selection stringifier
bsw/jbe@1309:         // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
bsw/jbe@1309:         // The current spec does not yet define this method.
bsw/jbe@1309:         selProto.toString = function() {
bsw/jbe@1309:             var rangeTexts = [];
bsw/jbe@1309:             for (var i = 0, len = this.rangeCount; i < len; ++i) {
bsw/jbe@1309:                 rangeTexts[i] = "" + this._ranges[i];
bsw/jbe@1309:             }
bsw/jbe@1309:             return rangeTexts.join("");
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         function assertNodeInSameDocument(sel, node) {
bsw/jbe@1309:             if (sel.win.document != getDocument(node)) {
bsw/jbe@1309:                 throw new DOMException("WRONG_DOCUMENT_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
bsw/jbe@1309:         selProto.collapse = function(node, offset) {
bsw/jbe@1309:             assertNodeInSameDocument(this, node);
bsw/jbe@1309:             var range = api.createRange(node);
bsw/jbe@1309:             range.collapseToPoint(node, offset);
bsw/jbe@1309:             this.setSingleRange(range);
bsw/jbe@1309:             this.isCollapsed = true;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.collapseToStart = function() {
bsw/jbe@1309:             if (this.rangeCount) {
bsw/jbe@1309:                 var range = this._ranges[0];
bsw/jbe@1309:                 this.collapse(range.startContainer, range.startOffset);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.collapseToEnd = function() {
bsw/jbe@1309:             if (this.rangeCount) {
bsw/jbe@1309:                 var range = this._ranges[this.rangeCount - 1];
bsw/jbe@1309:                 this.collapse(range.endContainer, range.endOffset);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
bsw/jbe@1309:         // specified so the native implementation is never used by Rangy.
bsw/jbe@1309:         selProto.selectAllChildren = function(node) {
bsw/jbe@1309:             assertNodeInSameDocument(this, node);
bsw/jbe@1309:             var range = api.createRange(node);
bsw/jbe@1309:             range.selectNodeContents(node);
bsw/jbe@1309:             this.setSingleRange(range);
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.deleteFromDocument = function() {
bsw/jbe@1309:             // Sepcial behaviour required for IE's control selections
bsw/jbe@1309:             if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
bsw/jbe@1309:                 var controlRange = this.docSelection.createRange();
bsw/jbe@1309:                 var element;
bsw/jbe@1309:                 while (controlRange.length) {
bsw/jbe@1309:                     element = controlRange.item(0);
bsw/jbe@1309:                     controlRange.remove(element);
bsw/jbe@1309:                     dom.removeNode(element);
bsw/jbe@1309:                 }
bsw/jbe@1309:                 this.refresh();
bsw/jbe@1309:             } else if (this.rangeCount) {
bsw/jbe@1309:                 var ranges = this.getAllRanges();
bsw/jbe@1309:                 if (ranges.length) {
bsw/jbe@1309:                     this.removeAllRanges();
bsw/jbe@1309:                     for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309:                         ranges[i].deleteContents();
bsw/jbe@1309:                     }
bsw/jbe@1309:                     // The spec says nothing about what the selection should contain after calling deleteContents on each
bsw/jbe@1309:                     // range. Firefox moves the selection to where the final selected range was, so we emulate that
bsw/jbe@1309:                     this.addRange(ranges[len - 1]);
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         // The following are non-standard extensions
bsw/jbe@1309:         selProto.eachRange = function(func, returnValue) {
bsw/jbe@1309:             for (var i = 0, len = this._ranges.length; i < len; ++i) {
bsw/jbe@1309:                 if ( func( this.getRangeAt(i) ) ) {
bsw/jbe@1309:                     return returnValue;
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.getAllRanges = function() {
bsw/jbe@1309:             var ranges = [];
bsw/jbe@1309:             this.eachRange(function(range) {
bsw/jbe@1309:                 ranges.push(range);
bsw/jbe@1309:             });
bsw/jbe@1309:             return ranges;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.setSingleRange = function(range, direction) {
bsw/jbe@1309:             this.removeAllRanges();
bsw/jbe@1309:             this.addRange(range, direction);
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.callMethodOnEachRange = function(methodName, params) {
bsw/jbe@1309:             var results = [];
bsw/jbe@1309:             this.eachRange( function(range) {
bsw/jbe@1309:                 results.push( range[methodName].apply(range, params || []) );
bsw/jbe@1309:             } );
bsw/jbe@1309:             return results;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         function createStartOrEndSetter(isStart) {
bsw/jbe@1309:             return function(node, offset) {
bsw/jbe@1309:                 var range;
bsw/jbe@1309:                 if (this.rangeCount) {
bsw/jbe@1309:                     range = this.getRangeAt(0);
bsw/jbe@1309:                     range["set" + (isStart ? "Start" : "End")](node, offset);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     range = api.createRange(this.win.document);
bsw/jbe@1309:                     range.setStartAndEnd(node, offset);
bsw/jbe@1309:                 }
bsw/jbe@1309:                 this.setSingleRange(range, this.isBackward());
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.setStart = createStartOrEndSetter(true);
bsw/jbe@1309:         selProto.setEnd = createStartOrEndSetter(false);
bsw/jbe@1309: 
bsw/jbe@1309:         // Add select() method to Range prototype. Any existing selection will be removed.
bsw/jbe@1309:         api.rangePrototype.select = function(direction) {
bsw/jbe@1309:             getSelection( this.getDocument() ).setSingleRange(this, direction);
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.changeEachRange = function(func) {
bsw/jbe@1309:             var ranges = [];
bsw/jbe@1309:             var backward = this.isBackward();
bsw/jbe@1309: 
bsw/jbe@1309:             this.eachRange(function(range) {
bsw/jbe@1309:                 func(range);
bsw/jbe@1309:                 ranges.push(range);
bsw/jbe@1309:             });
bsw/jbe@1309: 
bsw/jbe@1309:             this.removeAllRanges();
bsw/jbe@1309:             if (backward && ranges.length == 1) {
bsw/jbe@1309:                 this.addRange(ranges[0], "backward");
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 this.setRanges(ranges);
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.containsNode = function(node, allowPartial) {
bsw/jbe@1309:             return this.eachRange( function(range) {
bsw/jbe@1309:                 return range.containsNode(node, allowPartial);
bsw/jbe@1309:             }, true ) || false;
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.getBookmark = function(containerNode) {
bsw/jbe@1309:             return {
bsw/jbe@1309:                 backward: this.isBackward(),
bsw/jbe@1309:                 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
bsw/jbe@1309:             };
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.moveToBookmark = function(bookmark) {
bsw/jbe@1309:             var selRanges = [];
bsw/jbe@1309:             for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
bsw/jbe@1309:                 range = api.createRange(this.win);
bsw/jbe@1309:                 range.moveToBookmark(rangeBookmark);
bsw/jbe@1309:                 selRanges.push(range);
bsw/jbe@1309:             }
bsw/jbe@1309:             if (bookmark.backward) {
bsw/jbe@1309:                 this.setSingleRange(selRanges[0], "backward");
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 this.setRanges(selRanges);
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.saveRanges = function() {
bsw/jbe@1309:             return {
bsw/jbe@1309:                 backward: this.isBackward(),
bsw/jbe@1309:                 ranges: this.callMethodOnEachRange("cloneRange")
bsw/jbe@1309:             };
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.restoreRanges = function(selRanges) {
bsw/jbe@1309:             this.removeAllRanges();
bsw/jbe@1309:             for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
bsw/jbe@1309:                 this.addRange(range, (selRanges.backward && i == 0));
bsw/jbe@1309:             }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.toHtml = function() {
bsw/jbe@1309:             var rangeHtmls = [];
bsw/jbe@1309:             this.eachRange(function(range) {
bsw/jbe@1309:                 rangeHtmls.push( DomRange.toHtml(range) );
bsw/jbe@1309:             });
bsw/jbe@1309:             return rangeHtmls.join("");
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         if (features.implementsTextRange) {
bsw/jbe@1309:             selProto.getNativeTextRange = function() {
bsw/jbe@1309:                 var sel, textRange;
bsw/jbe@1309:                 if ( (sel = this.docSelection) ) {
bsw/jbe@1309:                     var range = sel.createRange();
bsw/jbe@1309:                     if (isTextRange(range)) {
bsw/jbe@1309:                         return range;
bsw/jbe@1309:                     } else {
bsw/jbe@1309:                         throw module.createError("getNativeTextRange: selection is a control selection");
bsw/jbe@1309:                     }
bsw/jbe@1309:                 } else if (this.rangeCount > 0) {
bsw/jbe@1309:                     return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     throw module.createError("getNativeTextRange: selection contains no range");
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         function inspect(sel) {
bsw/jbe@1309:             var rangeInspects = [];
bsw/jbe@1309:             var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
bsw/jbe@1309:             var focus = new DomPosition(sel.focusNode, sel.focusOffset);
bsw/jbe@1309:             var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
bsw/jbe@1309: 
bsw/jbe@1309:             if (typeof sel.rangeCount != "undefined") {
bsw/jbe@1309:                 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
bsw/jbe@1309:                     rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:             return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
bsw/jbe@1309:                     ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.getName = function() {
bsw/jbe@1309:             return "WrappedSelection";
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.inspect = function() {
bsw/jbe@1309:             return inspect(this);
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         selProto.detach = function() {
bsw/jbe@1309:             actOnCachedSelection(this.win, "delete");
bsw/jbe@1309:             deleteProperties(this);
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         WrappedSelection.detachAll = function() {
bsw/jbe@1309:             actOnCachedSelection(null, "deleteAll");
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:         WrappedSelection.inspect = inspect;
bsw/jbe@1309:         WrappedSelection.isDirectionBackward = isDirectionBackward;
bsw/jbe@1309: 
bsw/jbe@1309:         api.Selection = WrappedSelection;
bsw/jbe@1309: 
bsw/jbe@1309:         api.selectionPrototype = selProto;
bsw/jbe@1309: 
bsw/jbe@1309:         api.addShimListener(function(win) {
bsw/jbe@1309:             if (typeof win.getSelection == "undefined") {
bsw/jbe@1309:                 win.getSelection = function() {
bsw/jbe@1309:                     return getSelection(win);
bsw/jbe@1309:                 };
bsw/jbe@1309:             }
bsw/jbe@1309:             win = null;
bsw/jbe@1309:         });
bsw/jbe@1309:     });
bsw/jbe@1309:     
bsw/jbe@1309: 
bsw/jbe@1309:     /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309: 
bsw/jbe@1309:     // Wait for document to load before initializing
bsw/jbe@1309:     var docReady = false;
bsw/jbe@1309: 
bsw/jbe@1309:     var loadHandler = function(e) {
bsw/jbe@1309:         if (!docReady) {
bsw/jbe@1309:             docReady = true;
bsw/jbe@1309:             if (!api.initialized && api.config.autoInitialize) {
bsw/jbe@1309:                 init();
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     if (isBrowser) {
bsw/jbe@1309:         // Test whether the document has already been loaded and initialize immediately if so
bsw/jbe@1309:         if (document.readyState == "complete") {
bsw/jbe@1309:             loadHandler();
bsw/jbe@1309:         } else {
bsw/jbe@1309:             if (isHostMethod(document, "addEventListener")) {
bsw/jbe@1309:                 document.addEventListener("DOMContentLoaded", loadHandler, false);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // Add a fallback in case the DOMContentLoaded event isn't supported
bsw/jbe@1309:             addListener(window, "load", loadHandler);
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     rangy = api;
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Selection save and restore module for Rangy.
bsw/jbe@1309:  * Saves and restores user selections using marker invisible elements in the DOM.
bsw/jbe@1309:  *
bsw/jbe@1309:  * Part of Rangy, a cross-browser JavaScript range and selection library
bsw/jbe@1309:  * https://github.com/timdown/rangy
bsw/jbe@1309:  *
bsw/jbe@1309:  * Depends on Rangy core.
bsw/jbe@1309:  *
bsw/jbe@1309:  * Copyright 2015, Tim Down
bsw/jbe@1309:  * Licensed under the MIT license.
bsw/jbe@1309:  * Version: 1.3.1-dev
bsw/jbe@1309:  * Build date: 20 May 2015
bsw/jbe@1309:  *
bsw/jbe@1309: * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
bsw/jbe@1309: */
bsw/jbe@1309: rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
bsw/jbe@1309:     var dom = api.dom;
bsw/jbe@1309:     var removeNode = dom.removeNode;
bsw/jbe@1309:     var isDirectionBackward = api.Selection.isDirectionBackward;
bsw/jbe@1309:     var markerTextChar = "\ufeff";
bsw/jbe@1309: 
bsw/jbe@1309:     function gEBI(id, doc) {
bsw/jbe@1309:         return (doc || document).getElementById(id);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function insertRangeBoundaryMarker(range, atStart) {
bsw/jbe@1309:         var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
bsw/jbe@1309:         var markerEl;
bsw/jbe@1309:         var doc = dom.getDocument(range.startContainer);
bsw/jbe@1309: 
bsw/jbe@1309:         // Clone the Range and collapse to the appropriate boundary point
bsw/jbe@1309:         var boundaryRange = range.cloneRange();
bsw/jbe@1309:         boundaryRange.collapse(atStart);
bsw/jbe@1309: 
bsw/jbe@1309:         // Create the marker element containing a single invisible character using DOM methods and insert it
bsw/jbe@1309:         markerEl = doc.createElement("span");
bsw/jbe@1309:         markerEl.id = markerId;
bsw/jbe@1309:         markerEl.style.lineHeight = "0";
bsw/jbe@1309:         markerEl.style.display = "none";
bsw/jbe@1309:         markerEl.className = "rangySelectionBoundary";
bsw/jbe@1309:         markerEl.appendChild(doc.createTextNode(markerTextChar));
bsw/jbe@1309: 
bsw/jbe@1309:         boundaryRange.insertNode(markerEl);
bsw/jbe@1309:         return markerEl;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function setRangeBoundary(doc, range, markerId, atStart) {
bsw/jbe@1309:         var markerEl = gEBI(markerId, doc);
bsw/jbe@1309:         if (markerEl) {
bsw/jbe@1309:             range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
bsw/jbe@1309:             removeNode(markerEl);
bsw/jbe@1309:         } else {
bsw/jbe@1309:             module.warn("Marker element has been removed. Cannot restore selection.");
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function compareRanges(r1, r2) {
bsw/jbe@1309:         return r2.compareBoundaryPoints(r1.START_TO_START, r1);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function saveRange(range, direction) {
bsw/jbe@1309:         var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
bsw/jbe@1309:         var backward = isDirectionBackward(direction);
bsw/jbe@1309: 
bsw/jbe@1309:         if (range.collapsed) {
bsw/jbe@1309:             endEl = insertRangeBoundaryMarker(range, false);
bsw/jbe@1309:             return {
bsw/jbe@1309:                 document: doc,
bsw/jbe@1309:                 markerId: endEl.id,
bsw/jbe@1309:                 collapsed: true
bsw/jbe@1309:             };
bsw/jbe@1309:         } else {
bsw/jbe@1309:             endEl = insertRangeBoundaryMarker(range, false);
bsw/jbe@1309:             startEl = insertRangeBoundaryMarker(range, true);
bsw/jbe@1309: 
bsw/jbe@1309:             return {
bsw/jbe@1309:                 document: doc,
bsw/jbe@1309:                 startMarkerId: startEl.id,
bsw/jbe@1309:                 endMarkerId: endEl.id,
bsw/jbe@1309:                 collapsed: false,
bsw/jbe@1309:                 backward: backward,
bsw/jbe@1309:                 toString: function() {
bsw/jbe@1309:                     return "original text: '" + text + "', new text: '" + range.toString() + "'";
bsw/jbe@1309:                 }
bsw/jbe@1309:             };
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function restoreRange(rangeInfo, normalize) {
bsw/jbe@1309:         var doc = rangeInfo.document;
bsw/jbe@1309:         if (typeof normalize == "undefined") {
bsw/jbe@1309:             normalize = true;
bsw/jbe@1309:         }
bsw/jbe@1309:         var range = api.createRange(doc);
bsw/jbe@1309:         if (rangeInfo.collapsed) {
bsw/jbe@1309:             var markerEl = gEBI(rangeInfo.markerId, doc);
bsw/jbe@1309:             if (markerEl) {
bsw/jbe@1309:                 markerEl.style.display = "inline";
bsw/jbe@1309:                 var previousNode = markerEl.previousSibling;
bsw/jbe@1309: 
bsw/jbe@1309:                 // Workaround for issue 17
bsw/jbe@1309:                 if (previousNode && previousNode.nodeType == 3) {
bsw/jbe@1309:                     removeNode(markerEl);
bsw/jbe@1309:                     range.collapseToPoint(previousNode, previousNode.length);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                     range.collapseBefore(markerEl);
bsw/jbe@1309:                     removeNode(markerEl);
bsw/jbe@1309:                 }
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 module.warn("Marker element has been removed. Cannot restore selection.");
bsw/jbe@1309:             }
bsw/jbe@1309:         } else {
bsw/jbe@1309:             setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
bsw/jbe@1309:             setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (normalize) {
bsw/jbe@1309:             range.normalizeBoundaries();
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return range;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function saveRanges(ranges, direction) {
bsw/jbe@1309:         var rangeInfos = [], range, doc;
bsw/jbe@1309:         var backward = isDirectionBackward(direction);
bsw/jbe@1309: 
bsw/jbe@1309:         // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
bsw/jbe@1309:         ranges = ranges.slice(0);
bsw/jbe@1309:         ranges.sort(compareRanges);
bsw/jbe@1309: 
bsw/jbe@1309:         for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309:             rangeInfos[i] = saveRange(ranges[i], backward);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
bsw/jbe@1309:         // between its markers
bsw/jbe@1309:         for (i = len - 1; i >= 0; --i) {
bsw/jbe@1309:             range = ranges[i];
bsw/jbe@1309:             doc = api.DomRange.getRangeDocument(range);
bsw/jbe@1309:             if (range.collapsed) {
bsw/jbe@1309:                 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
bsw/jbe@1309:                 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return rangeInfos;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function saveSelection(win) {
bsw/jbe@1309:         if (!api.isSelectionValid(win)) {
bsw/jbe@1309:             module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
bsw/jbe@1309:             return null;
bsw/jbe@1309:         }
bsw/jbe@1309:         var sel = api.getSelection(win);
bsw/jbe@1309:         var ranges = sel.getAllRanges();
bsw/jbe@1309:         var backward = (ranges.length == 1 && sel.isBackward());
bsw/jbe@1309: 
bsw/jbe@1309:         var rangeInfos = saveRanges(ranges, backward);
bsw/jbe@1309: 
bsw/jbe@1309:         // Ensure current selection is unaffected
bsw/jbe@1309:         if (backward) {
bsw/jbe@1309:             sel.setSingleRange(ranges[0], backward);
bsw/jbe@1309:         } else {
bsw/jbe@1309:             sel.setRanges(ranges);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return {
bsw/jbe@1309:             win: win,
bsw/jbe@1309:             rangeInfos: rangeInfos,
bsw/jbe@1309:             restored: false
bsw/jbe@1309:         };
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function restoreRanges(rangeInfos) {
bsw/jbe@1309:         var ranges = [];
bsw/jbe@1309: 
bsw/jbe@1309:         // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
bsw/jbe@1309:         // normalization affecting previously restored ranges.
bsw/jbe@1309:         var rangeCount = rangeInfos.length;
bsw/jbe@1309: 
bsw/jbe@1309:         for (var i = rangeCount - 1; i >= 0; i--) {
bsw/jbe@1309:             ranges[i] = restoreRange(rangeInfos[i], true);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return ranges;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function restoreSelection(savedSelection, preserveDirection) {
bsw/jbe@1309:         if (!savedSelection.restored) {
bsw/jbe@1309:             var rangeInfos = savedSelection.rangeInfos;
bsw/jbe@1309:             var sel = api.getSelection(savedSelection.win);
bsw/jbe@1309:             var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
bsw/jbe@1309: 
bsw/jbe@1309:             if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
bsw/jbe@1309:                 sel.removeAllRanges();
bsw/jbe@1309:                 sel.addRange(ranges[0], true);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 sel.setRanges(ranges);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             savedSelection.restored = true;
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function removeMarkerElement(doc, markerId) {
bsw/jbe@1309:         var markerEl = gEBI(markerId, doc);
bsw/jbe@1309:         if (markerEl) {
bsw/jbe@1309:             removeNode(markerEl);
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     function removeMarkers(savedSelection) {
bsw/jbe@1309:         var rangeInfos = savedSelection.rangeInfos;
bsw/jbe@1309:         for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
bsw/jbe@1309:             rangeInfo = rangeInfos[i];
bsw/jbe@1309:             if (rangeInfo.collapsed) {
bsw/jbe@1309:                 removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
bsw/jbe@1309:             } else {
bsw/jbe@1309:                 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
bsw/jbe@1309:                 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     api.util.extend(api, {
bsw/jbe@1309:         saveRange: saveRange,
bsw/jbe@1309:         restoreRange: restoreRange,
bsw/jbe@1309:         saveRanges: saveRanges,
bsw/jbe@1309:         restoreRanges: restoreRanges,
bsw/jbe@1309:         saveSelection: saveSelection,
bsw/jbe@1309:         restoreSelection: restoreSelection,
bsw/jbe@1309:         removeMarkerElement: removeMarkerElement,
bsw/jbe@1309:         removeMarkers: removeMarkers
bsw/jbe@1309:     });
bsw/jbe@1309: });
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Text range module for Rangy.
bsw/jbe@1309:  * Text-based manipulation and searching of ranges and selections.
bsw/jbe@1309:  *
bsw/jbe@1309:  * Features
bsw/jbe@1309:  *
bsw/jbe@1309:  * - Ability to move range boundaries by character or word offsets
bsw/jbe@1309:  * - Customizable word tokenizer
bsw/jbe@1309:  * - Ignores text nodes inside 
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var /**
bsw/jbe@1309:        * Don't auto-link urls that are contained in the following elements:
bsw/jbe@1309:        */
bsw/jbe@1309:       IGNORE_URLS_IN        = wysihtml.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
bsw/jbe@1309:       /**
bsw/jbe@1309:        * revision 1:
bsw/jbe@1309:        *    /(\S+\.{1}[^\s\,\.\!]+)/g
bsw/jbe@1309:        *
bsw/jbe@1309:        * revision 2:
bsw/jbe@1309:        *    /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
bsw/jbe@1309:        *
bsw/jbe@1309:        * put this in the beginning if you don't wan't to match within a word
bsw/jbe@1309:        *    (^|[\>\(\{\[\s\>])
bsw/jbe@1309:        */
bsw/jbe@1309:       URL_REG_EXP           = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
bsw/jbe@1309:       TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
bsw/jbe@1309:       MAX_DISPLAY_LENGTH    = 100,
bsw/jbe@1309:       BRACKETS              = { ")": "(", "]": "[", "}": "{" };
bsw/jbe@1309: 
bsw/jbe@1309:   function autoLink(element, ignoreInClasses) {
bsw/jbe@1309:     if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
bsw/jbe@1309:       return element;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (element === element.ownerDocument.documentElement) {
bsw/jbe@1309:       element = element.ownerDocument.body;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return _parseNode(element, ignoreInClasses);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * This is basically a rebuild of
bsw/jbe@1309:    * the rails auto_link_urls text helper
bsw/jbe@1309:    */
bsw/jbe@1309:   function _convertUrlsToLinks(str) {
bsw/jbe@1309:     return str.replace(URL_REG_EXP, function(match, url) {
bsw/jbe@1309:       var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
bsw/jbe@1309:           opening     = BRACKETS[punctuation];
bsw/jbe@1309:       url = url.replace(TRAILING_CHAR_REG_EXP, "");
bsw/jbe@1309: 
bsw/jbe@1309:       if (url.split(opening).length > url.split(punctuation).length) {
bsw/jbe@1309:         url = url + punctuation;
bsw/jbe@1309:         punctuation = "";
bsw/jbe@1309:       }
bsw/jbe@1309:       var realUrl    = url,
bsw/jbe@1309:           displayUrl = url;
bsw/jbe@1309:       if (url.length > MAX_DISPLAY_LENGTH) {
bsw/jbe@1309:         displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
bsw/jbe@1309:       }
bsw/jbe@1309:       // Add http prefix if necessary
bsw/jbe@1309:       if (realUrl.substr(0, 4) === "www.") {
bsw/jbe@1309:         realUrl = "http://" + realUrl;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return '' + displayUrl + '' + punctuation;
bsw/jbe@1309:     });
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Creates or (if already cached) returns a temp element
bsw/jbe@1309:    * for the given document object
bsw/jbe@1309:    */
bsw/jbe@1309:   function _getTempElement(context) {
bsw/jbe@1309:     var tempElement = context._wysihtml_tempElement;
bsw/jbe@1309:     if (!tempElement) {
bsw/jbe@1309:       tempElement = context._wysihtml_tempElement = context.createElement("div");
bsw/jbe@1309:     }
bsw/jbe@1309:     return tempElement;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Replaces the original text nodes with the newly auto-linked dom tree
bsw/jbe@1309:    */
bsw/jbe@1309:   function _wrapMatchesInNode(textNode) {
bsw/jbe@1309:     var parentNode  = textNode.parentNode,
bsw/jbe@1309:         nodeValue   = wysihtml.lang.string(textNode.data).escapeHTML(),
bsw/jbe@1309:         tempElement = _getTempElement(parentNode.ownerDocument);
bsw/jbe@1309: 
bsw/jbe@1309:     // We need to insert an empty/temporary  to fix IE quirks
bsw/jbe@1309:     // Elsewise IE would strip white space in the beginning
bsw/jbe@1309:     tempElement.innerHTML = "" + _convertUrlsToLinks(nodeValue);
bsw/jbe@1309:     tempElement.removeChild(tempElement.firstChild);
bsw/jbe@1309: 
bsw/jbe@1309:     while (tempElement.firstChild) {
bsw/jbe@1309:       // inserts tempElement.firstChild before textNode
bsw/jbe@1309:       parentNode.insertBefore(tempElement.firstChild, textNode);
bsw/jbe@1309:     }
bsw/jbe@1309:     parentNode.removeChild(textNode);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
bsw/jbe@1309:     var nodeName;
bsw/jbe@1309:     while (node.parentNode) {
bsw/jbe@1309:       node = node.parentNode;
bsw/jbe@1309:       nodeName = node.nodeName;
bsw/jbe@1309:       if (node.className && wysihtml.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
bsw/jbe@1309:         return true;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (IGNORE_URLS_IN.contains(nodeName)) {
bsw/jbe@1309:         return true;
bsw/jbe@1309:       } else if (nodeName === "body") {
bsw/jbe@1309:         return false;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _parseNode(element, ignoreInClasses) {
bsw/jbe@1309:     if (IGNORE_URLS_IN.contains(element.nodeName)) {
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (element.className && wysihtml.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (element.nodeType === wysihtml.TEXT_NODE && element.data.match(URL_REG_EXP)) {
bsw/jbe@1309:       _wrapMatchesInNode(element);
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var childNodes        = wysihtml.lang.array(element.childNodes).get(),
bsw/jbe@1309:         childNodesLength  = childNodes.length,
bsw/jbe@1309:         i                 = 0;
bsw/jbe@1309: 
bsw/jbe@1309:     for (; i 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.compareDocumentPosition = (function() {
bsw/jbe@1309:   var documentElement = document.documentElement;
bsw/jbe@1309:   if (documentElement.compareDocumentPosition) {
bsw/jbe@1309:     return function(container, element) {
bsw/jbe@1309:       return container.compareDocumentPosition(element);
bsw/jbe@1309:     };
bsw/jbe@1309:   } else {
bsw/jbe@1309:     return function( container, element ) {
bsw/jbe@1309:       // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license
bsw/jbe@1309:       var thisOwner, otherOwner;
bsw/jbe@1309: 
bsw/jbe@1309:       if( container.nodeType === 9) // Node.DOCUMENT_NODE
bsw/jbe@1309:         thisOwner = container;
bsw/jbe@1309:       else
bsw/jbe@1309:         thisOwner = container.ownerDocument;
bsw/jbe@1309: 
bsw/jbe@1309:       if( element.nodeType === 9) // Node.DOCUMENT_NODE
bsw/jbe@1309:         otherOwner = element;
bsw/jbe@1309:       else
bsw/jbe@1309:         otherOwner = element.ownerDocument;
bsw/jbe@1309: 
bsw/jbe@1309:       if( container === element ) return 0;
bsw/jbe@1309:       if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
bsw/jbe@1309:       if( container.ownerDocument === element ) return 2 + 8;  //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
bsw/jbe@1309:       if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED;
bsw/jbe@1309: 
bsw/jbe@1309:       // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child.
bsw/jbe@1309:       if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml.lang.array(container.childNodes).indexOf( element ) !== -1)
bsw/jbe@1309:         return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
bsw/jbe@1309: 
bsw/jbe@1309:       if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml.lang.array(element.childNodes).indexOf( container ) !== -1)
bsw/jbe@1309:         return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
bsw/jbe@1309: 
bsw/jbe@1309:       var point = container;
bsw/jbe@1309:       var parents = [ ];
bsw/jbe@1309:       var previous = null;
bsw/jbe@1309:       while( point ) {
bsw/jbe@1309:         if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
bsw/jbe@1309:         parents.push( point );
bsw/jbe@1309:         point = point.parentNode;
bsw/jbe@1309:       }
bsw/jbe@1309:       point = element;
bsw/jbe@1309:       previous = null;
bsw/jbe@1309:       while( point ) {
bsw/jbe@1309:         if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
bsw/jbe@1309:         var location_index = wysihtml.lang.array(parents).indexOf( point );
bsw/jbe@1309:         if( location_index !== -1) {
bsw/jbe@1309:          var smallest_common_ancestor = parents[ location_index ];
bsw/jbe@1309:          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] );
bsw/jbe@1309:          var other_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous );
bsw/jbe@1309:          if( this_index > other_index ) {
bsw/jbe@1309:                return 2; //Node.DOCUMENT_POSITION_PRECEDING;
bsw/jbe@1309:          }
bsw/jbe@1309:          else {
bsw/jbe@1309:            return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
bsw/jbe@1309:          }
bsw/jbe@1309:         }
bsw/jbe@1309:         previous = point;
bsw/jbe@1309:         point = point.parentNode;
bsw/jbe@1309:       }
bsw/jbe@1309:       return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.contains = (function() {
bsw/jbe@1309:   var documentElement = document.documentElement;
bsw/jbe@1309:   if (documentElement.contains) {
bsw/jbe@1309:     return function(container, element) {
bsw/jbe@1309:       if (element.nodeType !== wysihtml.ELEMENT_NODE) {
bsw/jbe@1309:         if (element.parentNode === container) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:         element = element.parentNode;
bsw/jbe@1309:       }
bsw/jbe@1309:       return container !== element && container.contains(element);
bsw/jbe@1309:     };
bsw/jbe@1309:   } else if (documentElement.compareDocumentPosition) {
bsw/jbe@1309:     return function(container, element) {
bsw/jbe@1309:       // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
bsw/jbe@1309:       return !!(container.compareDocumentPosition(element) & 16);
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var doc = document;
bsw/jbe@1309:   wysihtml.dom.ContentEditableArea = Base.extend({
bsw/jbe@1309:       getContentEditable: function() {
bsw/jbe@1309:         return this.element;
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       getWindow: function() {
bsw/jbe@1309:         return this.element.ownerDocument.defaultView || this.element.ownerDocument.parentWindow;
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       getDocument: function() {
bsw/jbe@1309:         return this.element.ownerDocument;
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       constructor: function(readyCallback, config, contentEditable) {
bsw/jbe@1309:         this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
bsw/jbe@1309:         this.config   = wysihtml.lang.object({}).merge(config).get();
bsw/jbe@1309:         if (!this.config.className) {
bsw/jbe@1309:           this.config.className = "wysihtml-sandbox";
bsw/jbe@1309:         }
bsw/jbe@1309:         if (contentEditable) {
bsw/jbe@1309:             this.element = this._bindElement(contentEditable);
bsw/jbe@1309:         } else {
bsw/jbe@1309:             this.element = this._createElement();
bsw/jbe@1309:         }
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       destroy: function() {
bsw/jbe@1309: 
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       // creates a new contenteditable and initiates it
bsw/jbe@1309:       _createElement: function() {
bsw/jbe@1309:         var element = doc.createElement("div");
bsw/jbe@1309:         element.className = this.config.className;
bsw/jbe@1309:         this._loadElement(element);
bsw/jbe@1309:         return element;
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       // initiates an allready existent contenteditable
bsw/jbe@1309:       _bindElement: function(contentEditable) {
bsw/jbe@1309:         contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml-sandbox" : "wysihtml-sandbox";
bsw/jbe@1309:         this._loadElement(contentEditable, true);
bsw/jbe@1309:         return contentEditable;
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       _loadElement: function(element, contentExists) {
bsw/jbe@1309:         var that = this;
bsw/jbe@1309: 
bsw/jbe@1309:         if (!contentExists) {
bsw/jbe@1309:             var innerHtml = this._getHtml();
bsw/jbe@1309:             element.innerHTML = innerHtml;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         this.loaded = true;
bsw/jbe@1309:         // Trigger the callback
bsw/jbe@1309:         setTimeout(function() { that.callback(that); }, 0);
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       _getHtml: function(templateVars) {
bsw/jbe@1309:         return '';
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:   });
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Converts an HTML fragment/element into a unordered/ordered list
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} element The element which should be turned into a list
bsw/jbe@1309:  * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
bsw/jbe@1309:  * @return {Element} The created list
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    
bsw/jbe@1309:  *    
bsw/jbe@1309:  *      eminem
bsw/jbe@1309:  *      dr. dre
bsw/jbe@1309:  *      50 Cent
bsw/jbe@1309:  *    
bsw/jbe@1309:  *
bsw/jbe@1309:  *    
bsw/jbe@1309:  *
bsw/jbe@1309:  *    
bsw/jbe@1309:  *    
bsw/jbe@1309:  *      - eminembsw/jbe@1309:  *
- dr. drebsw/jbe@1309:  *
- 50 Centbsw/jbe@1309:  *
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.convertToList = (function() {
bsw/jbe@1309:   function _createListItem(doc, list) {
bsw/jbe@1309:     var listItem = doc.createElement("li");
bsw/jbe@1309:     list.appendChild(listItem);
bsw/jbe@1309:     return listItem;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _createList(doc, type) {
bsw/jbe@1309:     return doc.createElement(type);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function convertToList(element, listType, uneditableClass) {
bsw/jbe@1309:     if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
bsw/jbe@1309:       // Already a list
bsw/jbe@1309:       return element;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var doc               = element.ownerDocument,
bsw/jbe@1309:         list              = _createList(doc, listType),
bsw/jbe@1309:         lineBreaks        = element.querySelectorAll("br"),
bsw/jbe@1309:         lineBreaksLength  = lineBreaks.length,
bsw/jbe@1309:         childNodes,
bsw/jbe@1309:         childNodesLength,
bsw/jbe@1309:         childNode,
bsw/jbe@1309:         lineBreak,
bsw/jbe@1309:         parentNode,
bsw/jbe@1309:         isBlockElement,
bsw/jbe@1309:         isLineBreak,
bsw/jbe@1309:         currentListItem,
bsw/jbe@1309:         i;
bsw/jbe@1309: 
bsw/jbe@1309:     // First find
 at the end of inline elements and move them behind them
bsw/jbe@1309:     for (i=0; i if empty, otherwise create a new one
bsw/jbe@1309:         currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
bsw/jbe@1309:         currentListItem.appendChild(childNode);
bsw/jbe@1309:         currentListItem = null;
bsw/jbe@1309:         continue;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (isLineBreak) {
bsw/jbe@1309:         // Only create a new list item in the next iteration when the current one has already content
bsw/jbe@1309:         currentListItem = currentListItem.firstChild ? null : currentListItem;
bsw/jbe@1309:         continue;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       currentListItem.appendChild(childNode);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (childNodes.length === 0) {
bsw/jbe@1309:       _createListItem(doc, list);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     element.parentNode.replaceChild(list, element);
bsw/jbe@1309:     return list;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   return convertToList;
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Copy a set of attributes from one element to another
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Array} attributesToCopy List of attributes which should be copied
bsw/jbe@1309:  * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
bsw/jbe@1309:  *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
bsw/jbe@1309:  *    with the element where to copy the attributes to (see example)
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    var textarea    = document.querySelector("textarea"),
bsw/jbe@1309:  *        div         = document.querySelector("div[contenteditable=true]"),
bsw/jbe@1309:  *        anotherDiv  = document.querySelector("div.preview");
bsw/jbe@1309:  *    wysihtml.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
bsw/jbe@1309:  *
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.copyAttributes = function(attributesToCopy) {
bsw/jbe@1309:   return {
bsw/jbe@1309:     from: function(elementToCopyFrom) {
bsw/jbe@1309:       return {
bsw/jbe@1309:         to: function pasteElementAttributesTo(elementToCopyTo) {
bsw/jbe@1309:           var attribute,
bsw/jbe@1309:               i         = 0,
bsw/jbe@1309:               length    = attributesToCopy.length;
bsw/jbe@1309:           for (; i 0) {
bsw/jbe@1309:           var hasOneStyle = false,
bsw/jbe@1309:               styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty];
bsw/jbe@1309:           for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) {
bsw/jbe@1309:             // Some old IE-s have different property name for cssFloat
bsw/jbe@1309:             prop = wysihtml.browser.fixStyleKey(styles[j]);
bsw/jbe@1309:             if (node.style[prop]) {
bsw/jbe@1309:               if (properties.styleValue) {
bsw/jbe@1309:                 // Style value as additional parameter
bsw/jbe@1309:                 if (properties.styleValue instanceof RegExp) {
bsw/jbe@1309:                   // style value as Regexp
bsw/jbe@1309:                   if (node.style[prop].trim().match(properties.styleValue).length > 0) {
bsw/jbe@1309:                     hasOneStyle = true;
bsw/jbe@1309:                     break;
bsw/jbe@1309:                   }
bsw/jbe@1309:                 } else if (Array.isArray(properties.styleValue)) {
bsw/jbe@1309:                   // style value as array
bsw/jbe@1309:                   if (properties.styleValue.indexOf(node.style[prop].trim())) {
bsw/jbe@1309:                     hasOneStyle = true;
bsw/jbe@1309:                     break;
bsw/jbe@1309:                   }
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                   // style value as string
bsw/jbe@1309:                   if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) {
bsw/jbe@1309:                     hasOneStyle = true;
bsw/jbe@1309:                     break;
bsw/jbe@1309:                   }
bsw/jbe@1309:                 }
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 hasOneStyle = true;
bsw/jbe@1309:                 break;
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:             if (!hasOneStyle) {
bsw/jbe@1309:               return false;
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (properties.attribute) {
bsw/jbe@1309:           var attr = wysihtml.dom.getAttributes(node),
bsw/jbe@1309:               attrList = [],
bsw/jbe@1309:               hasOneAttribute = false;
bsw/jbe@1309: 
bsw/jbe@1309:           if (Array.isArray(properties.attribute)) {
bsw/jbe@1309:             attrList = properties.attribute;
bsw/jbe@1309:           } else {
bsw/jbe@1309:             attrList[properties.attribute] = properties.attributeValue;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           for (var a in attrList) {
bsw/jbe@1309:             if (attrList.hasOwnProperty(a)) {
bsw/jbe@1309:               if (typeof attrList[a] === "undefined") {
bsw/jbe@1309:                 if (typeof attr[a] !== "undefined") {
bsw/jbe@1309:                   hasOneAttribute = true;
bsw/jbe@1309:                   break;
bsw/jbe@1309:                 }
bsw/jbe@1309:               } else if (attr[a] === attrList[a]) {
bsw/jbe@1309:                 hasOneAttribute = true;
bsw/jbe@1309:                 break;
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if (!hasOneAttribute) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return true;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:     };
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Returns the given html wrapped in a div element
bsw/jbe@1309:  *
bsw/jbe@1309:  * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
bsw/jbe@1309:  * when inserted via innerHTML
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {String} html The html which should be wrapped in a dom element
bsw/jbe@1309:  * @param {Obejct} [context] Document object of the context the html belongs to
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.getAsDom("foo ");
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.getAsDom = (function() {
bsw/jbe@1309: 
bsw/jbe@1309:   var _innerHTMLShiv = function(html, context) {
bsw/jbe@1309:     var tempElement = context.createElement("div");
bsw/jbe@1309:     tempElement.style.display = "none";
bsw/jbe@1309:     context.body.appendChild(tempElement);
bsw/jbe@1309:     // IE throws an exception when trying to insert  via innerHTML
bsw/jbe@1309:     try { tempElement.innerHTML = html; } catch(e) {}
bsw/jbe@1309:     context.body.removeChild(tempElement);
bsw/jbe@1309:     return tempElement;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
bsw/jbe@1309:    */
bsw/jbe@1309:   var _ensureHTML5Compatibility = function(context) {
bsw/jbe@1309:     if (context._wysihtml_supportsHTML5Tags) {
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309:     for (var i=0, length=HTML5_ELEMENTS.length; i "1" in IE
bsw/jbe@1309:  *
bsw/jbe@1309:  * Therefore we have to check the element's outerHTML for the attribute
bsw/jbe@1309: */
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.getAttribute = function(node, attributeName) {
bsw/jbe@1309:   var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly();
bsw/jbe@1309:   attributeName = attributeName.toLowerCase();
bsw/jbe@1309:   var nodeName = node.nodeName;
bsw/jbe@1309:   if (nodeName == "IMG" && attributeName == "src" && wysihtml.dom.isLoadedImage(node) === true) {
bsw/jbe@1309:     // Get 'src' attribute value via object property since this will always contain the
bsw/jbe@1309:     // full absolute url (http://...)
bsw/jbe@1309:     // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
bsw/jbe@1309:     // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
bsw/jbe@1309:     return node.src;
bsw/jbe@1309:   } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
bsw/jbe@1309:     // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
bsw/jbe@1309:     var outerHTML      = node.outerHTML.toLowerCase(),
bsw/jbe@1309:         // TODO: This might not work for attributes without value: 
bsw/jbe@1309:         hasAttribute   = outerHTML.indexOf(" " + attributeName +  "=") != -1;
bsw/jbe@1309: 
bsw/jbe@1309:     return hasAttribute ? node.getAttribute(attributeName) : null;
bsw/jbe@1309:   } else{
bsw/jbe@1309:     return node.getAttribute(attributeName);
bsw/jbe@1309:   }
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Get all attributes of an element
bsw/jbe@1309:  *
bsw/jbe@1309:  * IE gives wrong results for hasAttribute/getAttribute, for example:
bsw/jbe@1309:  *    var td = document.createElement("td");
bsw/jbe@1309:  *    td.getAttribute("rowspan"); // => "1" in IE
bsw/jbe@1309:  *
bsw/jbe@1309:  * Therefore we have to check the element's outerHTML for the attribute
bsw/jbe@1309: */
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.getAttributes = function(node) {
bsw/jbe@1309:   var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(),
bsw/jbe@1309:       nodeName = node.nodeName,
bsw/jbe@1309:       attributes = [],
bsw/jbe@1309:       attr;
bsw/jbe@1309: 
bsw/jbe@1309:   for (attr in node.attributes) {
bsw/jbe@1309:     if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr)))  {
bsw/jbe@1309:       if (node.attributes[attr].specified) {
bsw/jbe@1309:         if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml.dom.isLoadedImage(node) === true) {
bsw/jbe@1309:           attributes['src'] = node.src;
bsw/jbe@1309:         } else if (wysihtml.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) {
bsw/jbe@1309:           if (node.attributes[attr].value !== 1) {
bsw/jbe@1309:             attributes[node.attributes[attr].name] = node.attributes[attr].value;
bsw/jbe@1309:           }
bsw/jbe@1309:         } else {
bsw/jbe@1309:           attributes[node.attributes[attr].name] = node.attributes[attr].value;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   return attributes;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Walks the dom tree from the given node up until it finds a match
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} node The from which to check the parent nodes
bsw/jbe@1309:  * @param {Object} matchingSet Object to match against, Properties for filtering element:
bsw/jbe@1309:  *   {
bsw/jbe@1309:  *     query: selector string,
bsw/jbe@1309:  *     classRegExp: regex,
bsw/jbe@1309:  *     styleProperty: string or [],
bsw/jbe@1309:  *     styleValue: string, [] or regex
bsw/jbe@1309:  *   }
bsw/jbe@1309:  * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
bsw/jbe@1309:  * @param {Element} Optional, defines the container that limits the search
bsw/jbe@1309:  *
bsw/jbe@1309:  * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
bsw/jbe@1309: */
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.getParentElement = (function() {
bsw/jbe@1309: 
bsw/jbe@1309:   return function(node, properties, levels, container) {
bsw/jbe@1309:     levels = levels || 50;
bsw/jbe@1309:     while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
bsw/jbe@1309:       if (wysihtml.dom.domNode(node).test(properties)) {
bsw/jbe@1309:         return node;
bsw/jbe@1309:       }
bsw/jbe@1309:       node = node.parentNode;
bsw/jbe@1309:     }
bsw/jbe@1309:     return null;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: /* 
bsw/jbe@1309:  * Methods for fetching pasted html before it gets inserted into content
bsw/jbe@1309: **/
bsw/jbe@1309: 
bsw/jbe@1309: /* Modern event.clipboardData driven approach.
bsw/jbe@1309:  * Advantage is that it does not have to loose selection or modify dom to catch the data. 
bsw/jbe@1309:  * IE does not support though.
bsw/jbe@1309: **/
bsw/jbe@1309: wysihtml.dom.getPastedHtml = function(event) {
bsw/jbe@1309:   var html;
bsw/jbe@1309:   if (wysihtml.browser.supportsModernPaste() && event.clipboardData) {
bsw/jbe@1309:     if (wysihtml.lang.array(event.clipboardData.types).contains('text/html')) {
bsw/jbe@1309:       html = event.clipboardData.getData('text/html');
bsw/jbe@1309:     } else if (wysihtml.lang.array(event.clipboardData.types).contains('text/plain')) {
bsw/jbe@1309:       html = wysihtml.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   return html;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /* Older temprorary contenteditable as paste source catcher method for fallbacks */
bsw/jbe@1309: wysihtml.dom.getPastedHtmlWithDiv = function (composer, f) {
bsw/jbe@1309:   var selBookmark = composer.selection.getBookmark(),
bsw/jbe@1309:       doc = composer.element.ownerDocument,
bsw/jbe@1309:       cleanerDiv = doc.createElement('DIV'),
bsw/jbe@1309:       scrollPos = composer.getScrollPos();
bsw/jbe@1309:   
bsw/jbe@1309:   doc.body.appendChild(cleanerDiv);
bsw/jbe@1309: 
bsw/jbe@1309:   cleanerDiv.style.width = "1px";
bsw/jbe@1309:   cleanerDiv.style.height = "1px";
bsw/jbe@1309:   cleanerDiv.style.overflow = "hidden";
bsw/jbe@1309:   cleanerDiv.style.position = "absolute";
bsw/jbe@1309:   cleanerDiv.style.top = scrollPos.y + "px";
bsw/jbe@1309:   cleanerDiv.style.left = scrollPos.x + "px";
bsw/jbe@1309: 
bsw/jbe@1309:   cleanerDiv.setAttribute('contenteditable', 'true');
bsw/jbe@1309:   cleanerDiv.focus();
bsw/jbe@1309: 
bsw/jbe@1309:   setTimeout(function () {
bsw/jbe@1309:     var html;
bsw/jbe@1309: 
bsw/jbe@1309:     composer.selection.setBookmark(selBookmark);
bsw/jbe@1309:     html = cleanerDiv.innerHTML;
bsw/jbe@1309:     if (html && (/^
$/i).test(html.trim())) {
bsw/jbe@1309:       html = false;
bsw/jbe@1309:     }
bsw/jbe@1309:     f(html);
bsw/jbe@1309:     cleanerDiv.parentNode.removeChild(cleanerDiv);
bsw/jbe@1309:   }, 0);
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Get element's style for a specific css property
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} element The element on which to retrieve the style
bsw/jbe@1309:  * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.getStyle("display").from(document.body);
bsw/jbe@1309:  *    // => "block"
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.getStyle = (function() {
bsw/jbe@1309:   var stylePropertyMapping = {
bsw/jbe@1309:         "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
bsw/jbe@1309:       },
bsw/jbe@1309:       REG_EXP_CAMELIZE = /\-[a-z]/g;
bsw/jbe@1309: 
bsw/jbe@1309:   function camelize(str) {
bsw/jbe@1309:     return str.replace(REG_EXP_CAMELIZE, function(match) {
bsw/jbe@1309:       return match.charAt(1).toUpperCase();
bsw/jbe@1309:     });
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   return function(property) {
bsw/jbe@1309:     return {
bsw/jbe@1309:       from: function(element) {
bsw/jbe@1309:         if (element.nodeType !== wysihtml.ELEMENT_NODE) {
bsw/jbe@1309:           return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var doc               = element.ownerDocument,
bsw/jbe@1309:             camelizedProperty = stylePropertyMapping[property] || camelize(property),
bsw/jbe@1309:             style             = element.style,
bsw/jbe@1309:             currentStyle      = element.currentStyle,
bsw/jbe@1309:             styleValue        = style[camelizedProperty];
bsw/jbe@1309:         if (styleValue) {
bsw/jbe@1309:           return styleValue;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
bsw/jbe@1309:         // window.getComputedStyle, since it returns css property values in their original unit:
bsw/jbe@1309:         // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
bsw/jbe@1309:         // gives you the original "50%".
bsw/jbe@1309:         // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
bsw/jbe@1309:         if (currentStyle) {
bsw/jbe@1309:           try {
bsw/jbe@1309:             return currentStyle[camelizedProperty];
bsw/jbe@1309:           } catch(e) {
bsw/jbe@1309:             //ie will occasionally fail for unknown reasons. swallowing exception
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var win                 = doc.defaultView || doc.parentWindow,
bsw/jbe@1309:             needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
bsw/jbe@1309:             originalOverflow,
bsw/jbe@1309:             returnValue;
bsw/jbe@1309: 
bsw/jbe@1309:         if (win.getComputedStyle) {
bsw/jbe@1309:           // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
bsw/jbe@1309:           // therfore we remove and restore the scrollbar and calculate the value in between
bsw/jbe@1309:           if (needsOverflowReset) {
bsw/jbe@1309:             originalOverflow = style.overflow;
bsw/jbe@1309:             style.overflow = "hidden";
bsw/jbe@1309:           }
bsw/jbe@1309:           returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
bsw/jbe@1309:           if (needsOverflowReset) {
bsw/jbe@1309:             style.overflow = originalOverflow || "";
bsw/jbe@1309:           }
bsw/jbe@1309:           return returnValue;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     };
bsw/jbe@1309:   };
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.getTextNodes = function(node, ingoreEmpty){
bsw/jbe@1309:   var all = [];
bsw/jbe@1309:   for (node=node.firstChild;node;node=node.nextSibling){
bsw/jbe@1309:     if (node.nodeType == 3) {
bsw/jbe@1309:       if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
bsw/jbe@1309:         all.push(node);
bsw/jbe@1309:       }
bsw/jbe@1309:     } else {
bsw/jbe@1309:       all = all.concat(wysihtml.dom.getTextNodes(node, ingoreEmpty));
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   return all;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * High performant way to check whether an element with a specific class name is in the given document
bsw/jbe@1309:  * Optimized for being heavily executed
bsw/jbe@1309:  * Unleashes the power of live node lists
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Object} doc The document object of the context where to check
bsw/jbe@1309:  * @param {String} tagName Upper cased tag name
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.hasElementWithClassName(document, "foobar");
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var LIVE_CACHE          = {},
bsw/jbe@1309:       DOCUMENT_IDENTIFIER = 1;
bsw/jbe@1309: 
bsw/jbe@1309:   function _getDocumentIdentifier(doc) {
bsw/jbe@1309:     return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.dom.hasElementWithClassName = function(doc, className) {
bsw/jbe@1309:     // getElementsByClassName is not supported by IE<9
bsw/jbe@1309:     // but is sometimes mocked via library code (which then doesn't return live node lists)
bsw/jbe@1309:     if (!wysihtml.browser.supportsNativeGetElementsByClassName()) {
bsw/jbe@1309:       return !!doc.querySelector("." + className);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var key         = _getDocumentIdentifier(doc) + ":" + className,
bsw/jbe@1309:         cacheEntry  = LIVE_CACHE[key];
bsw/jbe@1309:     if (!cacheEntry) {
bsw/jbe@1309:       cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return cacheEntry.length > 0;
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * High performant way to check whether an element with a specific tag name is in the given document
bsw/jbe@1309:  * Optimized for being heavily executed
bsw/jbe@1309:  * Unleashes the power of live node lists
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Object} doc The document object of the context where to check
bsw/jbe@1309:  * @param {String} tagName Upper cased tag name
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.hasElementWithTagName(document, "IMG");
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.hasElementWithTagName = (function() {
bsw/jbe@1309:   var LIVE_CACHE          = {},
bsw/jbe@1309:       DOCUMENT_IDENTIFIER = 1;
bsw/jbe@1309: 
bsw/jbe@1309:   function _getDocumentIdentifier(doc) {
bsw/jbe@1309:     return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   return function(doc, tagName) {
bsw/jbe@1309:     var key         = _getDocumentIdentifier(doc) + ":" + tagName,
bsw/jbe@1309:         cacheEntry  = LIVE_CACHE[key];
bsw/jbe@1309:     if (!cacheEntry) {
bsw/jbe@1309:       cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return cacheEntry.length > 0;
bsw/jbe@1309:   };
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.insert = function(elementToInsert) {
bsw/jbe@1309:   return {
bsw/jbe@1309:     after: function(element) {
bsw/jbe@1309:       element.parentNode.insertBefore(elementToInsert, element.nextSibling);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     before: function(element) {
bsw/jbe@1309:       element.parentNode.insertBefore(elementToInsert, element);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     into: function(element) {
bsw/jbe@1309:       element.appendChild(elementToInsert);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.insertCSS = function(rules) {
bsw/jbe@1309:   rules = rules.join("\n");
bsw/jbe@1309: 
bsw/jbe@1309:   return {
bsw/jbe@1309:     into: function(doc) {
bsw/jbe@1309:       var styleElement = doc.createElement("style");
bsw/jbe@1309:       styleElement.type = "text/css";
bsw/jbe@1309: 
bsw/jbe@1309:       if (styleElement.styleSheet) {
bsw/jbe@1309:         styleElement.styleSheet.cssText = rules;
bsw/jbe@1309:       } else {
bsw/jbe@1309:         styleElement.appendChild(doc.createTextNode(rules));
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var link = doc.querySelector("head link");
bsw/jbe@1309:       if (link) {
bsw/jbe@1309:         link.parentNode.insertBefore(styleElement, link);
bsw/jbe@1309:         return;
bsw/jbe@1309:       } else {
bsw/jbe@1309:         var head = doc.querySelector("head");
bsw/jbe@1309:         if (head) {
bsw/jbe@1309:           head.appendChild(styleElement);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:    * Check whether the given node is a proper loaded image
bsw/jbe@1309:    * FIXME: Returns undefined when unknown (Chrome, Safari)
bsw/jbe@1309: */
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.isLoadedImage = function (node) {
bsw/jbe@1309:   try {
bsw/jbe@1309:     return node.complete && !node.mozMatchesSelector(":-moz-broken");
bsw/jbe@1309:   } catch(e) {
bsw/jbe@1309:     if (node.complete && node.readyState === "complete") {
bsw/jbe@1309:       return true;
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: // TODO: Refactor dom tree traversing here
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   wysihtml.dom.lineBreaks = function(node) {
bsw/jbe@1309: 
bsw/jbe@1309:     function _isLineBreak(n) {
bsw/jbe@1309:       return n.nodeName === "BR";
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Checks whether the elment causes a visual line break
bsw/jbe@1309:      * (
 or block elements)
bsw/jbe@1309:      */
bsw/jbe@1309:     function _isLineBreakOrBlockElement(element) {
bsw/jbe@1309:       if (_isLineBreak(element)) {
bsw/jbe@1309:         return true;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (wysihtml.dom.getStyle("display").from(element) === "block") {
bsw/jbe@1309:         return true;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return {
bsw/jbe@1309: 
bsw/jbe@1309:       /* wysihtml.dom.lineBreaks(element).add();
bsw/jbe@1309:        *
bsw/jbe@1309:        * Adds line breaks before and after the given node if the previous and next siblings
bsw/jbe@1309:        * aren't already causing a visual line break (block element or 
)
bsw/jbe@1309:        */
bsw/jbe@1309:       add: function(options) {
bsw/jbe@1309:         var doc             = node.ownerDocument,
bsw/jbe@1309:           nextSibling     = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
bsw/jbe@1309:           previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
bsw/jbe@1309: 
bsw/jbe@1309:         if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
bsw/jbe@1309:           wysihtml.dom.insert(doc.createElement("br")).after(node);
bsw/jbe@1309:         }
bsw/jbe@1309:         if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
bsw/jbe@1309:           wysihtml.dom.insert(doc.createElement("br")).before(node);
bsw/jbe@1309:         }
bsw/jbe@1309:       },
bsw/jbe@1309: 
bsw/jbe@1309:       /* wysihtml.dom.lineBreaks(element).remove();
bsw/jbe@1309:        *
bsw/jbe@1309:        * Removes line breaks before and after the given node
bsw/jbe@1309:        */
bsw/jbe@1309:       remove: function(options) {
bsw/jbe@1309:         var nextSibling     = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
bsw/jbe@1309:             previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
bsw/jbe@1309: 
bsw/jbe@1309:         if (nextSibling && _isLineBreak(nextSibling)) {
bsw/jbe@1309:           nextSibling.parentNode.removeChild(nextSibling);
bsw/jbe@1309:         }
bsw/jbe@1309:         if (previousSibling && _isLineBreak(previousSibling)) {
bsw/jbe@1309:           previousSibling.parentNode.removeChild(previousSibling);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     };
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: /**
bsw/jbe@1309:  * Method to set dom events
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.observe = function(element, eventNames, handler) {
bsw/jbe@1309:   eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
bsw/jbe@1309: 
bsw/jbe@1309:   var handlerWrapper,
bsw/jbe@1309:       eventName,
bsw/jbe@1309:       i       = 0,
bsw/jbe@1309:       length  = eventNames.length;
bsw/jbe@1309: 
bsw/jbe@1309:   for (; i foo bar
bsw/jbe@1309:  *
bsw/jbe@1309:  *    var userHTML = 'I'm a table! 
';
bsw/jbe@1309:  *    wysihtml.dom.parse(userHTML);
bsw/jbe@1309:  *    // => 'I'm a table!'
bsw/jbe@1309:  *
bsw/jbe@1309:  *    var userHTML = 'foobar
foobar';
bsw/jbe@1309:  *    wysihtml.dom.parse(userHTML, {
bsw/jbe@1309:  *      tags: {
bsw/jbe@1309:  *        div: undefined,
bsw/jbe@1309:  *        br:  true
bsw/jbe@1309:  *      }
bsw/jbe@1309:  *    });
bsw/jbe@1309:  *    // => ''
bsw/jbe@1309:  *
bsw/jbe@1309:  *    var userHTML = 'foobar';
bsw/jbe@1309:  *    wysihtml.dom.parse(userHTML, {
bsw/jbe@1309:  *      classes: {
bsw/jbe@1309:  *        red:    1,
bsw/jbe@1309:  *        green:  1
bsw/jbe@1309:  *      },
bsw/jbe@1309:  *      tags: {
bsw/jbe@1309:  *        div: {
bsw/jbe@1309:  *          rename_tag:     "p"
bsw/jbe@1309:  *        }
bsw/jbe@1309:  *      }
bsw/jbe@1309:  *    });
bsw/jbe@1309:  *    // => 'foo
bar
'
bsw/jbe@1309:  */
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.parse = function(elementOrHtml_current, config_current) {
bsw/jbe@1309:   /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors.
bsw/jbe@1309:    * Refactor whole code as this method while workind is kind of awkward too */
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
bsw/jbe@1309:    * new DOMParser().parseFromString(' ') will cause a parseError since the
bsw/jbe@1309:    * node isn't closed
bsw/jbe@1309:    *
bsw/jbe@1309:    * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
bsw/jbe@1309:    */
bsw/jbe@1309:   var NODE_TYPE_MAPPING = {
bsw/jbe@1309:         "1": _handleElement,
bsw/jbe@1309:         "3": _handleText,
bsw/jbe@1309:         "8": _handleComment
bsw/jbe@1309:       },
bsw/jbe@1309:       // Rename unknown tags to this
bsw/jbe@1309:       DEFAULT_NODE_NAME   = "span",
bsw/jbe@1309:       WHITE_SPACE_REG_EXP = /\s+/,
bsw/jbe@1309:       defaultRules        = { tags: {}, classes: {} },
bsw/jbe@1309:       currentRules        = {},
bsw/jbe@1309:       blockElements       = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" ,
bsw/jbe@1309:                              "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU",
bsw/jbe@1309:                              "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"];
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Iterates over all childs of the element, recreates them, appends them into a document fragment
bsw/jbe@1309:    * which later replaces the entire body content
bsw/jbe@1309:    */
bsw/jbe@1309:    function parse(elementOrHtml, config) {
bsw/jbe@1309:     wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
bsw/jbe@1309: 
bsw/jbe@1309:     var context       = config.context || elementOrHtml.ownerDocument || document,
bsw/jbe@1309:         fragment      = context.createDocumentFragment(),
bsw/jbe@1309:         isString      = typeof(elementOrHtml) === "string",
bsw/jbe@1309:         clearInternals = false,
bsw/jbe@1309:         element,
bsw/jbe@1309:         newNode,
bsw/jbe@1309:         firstChild;
bsw/jbe@1309: 
bsw/jbe@1309:     if (config.clearInternals === true) {
bsw/jbe@1309:       clearInternals = true;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (isString) {
bsw/jbe@1309:       element = wysihtml.dom.getAsDom(elementOrHtml, context);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       element = elementOrHtml;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (currentRules.selectors) {
bsw/jbe@1309:       _applySelectorRules(element, currentRules.selectors);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     while (element.firstChild) {
bsw/jbe@1309:       firstChild = element.firstChild;
bsw/jbe@1309:       newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
bsw/jbe@1309:       if (newNode) {
bsw/jbe@1309:         fragment.appendChild(newNode);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (firstChild !== newNode) {
bsw/jbe@1309:         element.removeChild(firstChild);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (config.unjoinNbsps) {
bsw/jbe@1309:       // replace joined non-breakable spaces with unjoined
bsw/jbe@1309:       var txtnodes = wysihtml.dom.getTextNodes(fragment);
bsw/jbe@1309:       for (var n = txtnodes.length; n--;) {
bsw/jbe@1309:         txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Clear element contents
bsw/jbe@1309:     element.innerHTML = "";
bsw/jbe@1309: 
bsw/jbe@1309:     // Insert new DOM tree
bsw/jbe@1309:     element.appendChild(fragment);
bsw/jbe@1309: 
bsw/jbe@1309:     return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
bsw/jbe@1309:     var oldNodeType     = oldNode.nodeType,
bsw/jbe@1309:         oldChilds       = oldNode.childNodes,
bsw/jbe@1309:         oldChildsLength = oldChilds.length,
bsw/jbe@1309:         method          = NODE_TYPE_MAPPING[oldNodeType],
bsw/jbe@1309:         i               = 0,
bsw/jbe@1309:         fragment,
bsw/jbe@1309:         newNode,
bsw/jbe@1309:         newChild,
bsw/jbe@1309:         nodeDisplay;
bsw/jbe@1309: 
bsw/jbe@1309:     // Passes directly elemets with uneditable class
bsw/jbe@1309:     if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) {
bsw/jbe@1309:         return oldNode;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     newNode = method && method(oldNode, clearInternals);
bsw/jbe@1309: 
bsw/jbe@1309:     // Remove or unwrap node in case of return value null or false
bsw/jbe@1309:     if (!newNode) {
bsw/jbe@1309:         if (newNode === false) {
bsw/jbe@1309:             // false defines that tag should be removed but contents should remain (unwrap)
bsw/jbe@1309:             fragment = oldNode.ownerDocument.createDocumentFragment();
bsw/jbe@1309: 
bsw/jbe@1309:             for (i = oldChildsLength; i--;) {
bsw/jbe@1309:               if (oldChilds[i]) {
bsw/jbe@1309:                 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
bsw/jbe@1309:                 if (newChild) {
bsw/jbe@1309:                   if (oldChilds[i] === newChild) {
bsw/jbe@1309:                     i--;
bsw/jbe@1309:                   }
bsw/jbe@1309:                   fragment.insertBefore(newChild, fragment.firstChild);
bsw/jbe@1309:                 }
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode);
bsw/jbe@1309: 
bsw/jbe@1309:             if (nodeDisplay === '') {
bsw/jbe@1309:               // Handle display style when element not in dom
bsw/jbe@1309:               nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : "";
bsw/jbe@1309:             }
bsw/jbe@1309:             if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) {
bsw/jbe@1309:               fragment.appendChild(oldNode.ownerDocument.createElement("br"));
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // TODO: try to minimize surplus spaces
bsw/jbe@1309:             if (wysihtml.lang.array([
bsw/jbe@1309:                 "div", "pre", "p",
bsw/jbe@1309:                 "table", "td", "th",
bsw/jbe@1309:                 "ul", "ol", "li",
bsw/jbe@1309:                 "dd", "dl",
bsw/jbe@1309:                 "footer", "header", "section",
bsw/jbe@1309:                 "h1", "h2", "h3", "h4", "h5", "h6"
bsw/jbe@1309:             ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
bsw/jbe@1309:                 // add space at first when unwraping non-textflow elements
bsw/jbe@1309:                 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
bsw/jbe@1309:                   fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (fragment.normalize) {
bsw/jbe@1309:               fragment.normalize();
bsw/jbe@1309:             }
bsw/jbe@1309:             return fragment;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           // Remove
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Converts all childnodes
bsw/jbe@1309:     for (i=0; i
') will cause a parseError since the
bsw/jbe@1309:    * node isn't closed
bsw/jbe@1309:    *
bsw/jbe@1309:    * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
bsw/jbe@1309:    */
bsw/jbe@1309:   var NODE_TYPE_MAPPING = {
bsw/jbe@1309:         "1": _handleElement,
bsw/jbe@1309:         "3": _handleText,
bsw/jbe@1309:         "8": _handleComment
bsw/jbe@1309:       },
bsw/jbe@1309:       // Rename unknown tags to this
bsw/jbe@1309:       DEFAULT_NODE_NAME   = "span",
bsw/jbe@1309:       WHITE_SPACE_REG_EXP = /\s+/,
bsw/jbe@1309:       defaultRules        = { tags: {}, classes: {} },
bsw/jbe@1309:       currentRules        = {},
bsw/jbe@1309:       blockElements       = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" ,
bsw/jbe@1309:                              "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU",
bsw/jbe@1309:                              "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"];
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Iterates over all childs of the element, recreates them, appends them into a document fragment
bsw/jbe@1309:    * which later replaces the entire body content
bsw/jbe@1309:    */
bsw/jbe@1309:    function parse(elementOrHtml, config) {
bsw/jbe@1309:     wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
bsw/jbe@1309: 
bsw/jbe@1309:     var context       = config.context || elementOrHtml.ownerDocument || document,
bsw/jbe@1309:         fragment      = context.createDocumentFragment(),
bsw/jbe@1309:         isString      = typeof(elementOrHtml) === "string",
bsw/jbe@1309:         clearInternals = false,
bsw/jbe@1309:         element,
bsw/jbe@1309:         newNode,
bsw/jbe@1309:         firstChild;
bsw/jbe@1309: 
bsw/jbe@1309:     if (config.clearInternals === true) {
bsw/jbe@1309:       clearInternals = true;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (isString) {
bsw/jbe@1309:       element = wysihtml.dom.getAsDom(elementOrHtml, context);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       element = elementOrHtml;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (currentRules.selectors) {
bsw/jbe@1309:       _applySelectorRules(element, currentRules.selectors);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     while (element.firstChild) {
bsw/jbe@1309:       firstChild = element.firstChild;
bsw/jbe@1309:       newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
bsw/jbe@1309:       if (newNode) {
bsw/jbe@1309:         fragment.appendChild(newNode);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (firstChild !== newNode) {
bsw/jbe@1309:         element.removeChild(firstChild);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (config.unjoinNbsps) {
bsw/jbe@1309:       // replace joined non-breakable spaces with unjoined
bsw/jbe@1309:       var txtnodes = wysihtml.dom.getTextNodes(fragment);
bsw/jbe@1309:       for (var n = txtnodes.length; n--;) {
bsw/jbe@1309:         txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Clear element contents
bsw/jbe@1309:     element.innerHTML = "";
bsw/jbe@1309: 
bsw/jbe@1309:     // Insert new DOM tree
bsw/jbe@1309:     element.appendChild(fragment);
bsw/jbe@1309: 
bsw/jbe@1309:     return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
bsw/jbe@1309:     var oldNodeType     = oldNode.nodeType,
bsw/jbe@1309:         oldChilds       = oldNode.childNodes,
bsw/jbe@1309:         oldChildsLength = oldChilds.length,
bsw/jbe@1309:         method          = NODE_TYPE_MAPPING[oldNodeType],
bsw/jbe@1309:         i               = 0,
bsw/jbe@1309:         fragment,
bsw/jbe@1309:         newNode,
bsw/jbe@1309:         newChild,
bsw/jbe@1309:         nodeDisplay;
bsw/jbe@1309: 
bsw/jbe@1309:     // Passes directly elemets with uneditable class
bsw/jbe@1309:     if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) {
bsw/jbe@1309:         return oldNode;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     newNode = method && method(oldNode, clearInternals);
bsw/jbe@1309: 
bsw/jbe@1309:     // Remove or unwrap node in case of return value null or false
bsw/jbe@1309:     if (!newNode) {
bsw/jbe@1309:         if (newNode === false) {
bsw/jbe@1309:             // false defines that tag should be removed but contents should remain (unwrap)
bsw/jbe@1309:             fragment = oldNode.ownerDocument.createDocumentFragment();
bsw/jbe@1309: 
bsw/jbe@1309:             for (i = oldChildsLength; i--;) {
bsw/jbe@1309:               if (oldChilds[i]) {
bsw/jbe@1309:                 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
bsw/jbe@1309:                 if (newChild) {
bsw/jbe@1309:                   if (oldChilds[i] === newChild) {
bsw/jbe@1309:                     i--;
bsw/jbe@1309:                   }
bsw/jbe@1309:                   fragment.insertBefore(newChild, fragment.firstChild);
bsw/jbe@1309:                 }
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode);
bsw/jbe@1309: 
bsw/jbe@1309:             if (nodeDisplay === '') {
bsw/jbe@1309:               // Handle display style when element not in dom
bsw/jbe@1309:               nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : "";
bsw/jbe@1309:             }
bsw/jbe@1309:             if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) {
bsw/jbe@1309:               fragment.appendChild(oldNode.ownerDocument.createElement("br"));
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             // TODO: try to minimize surplus spaces
bsw/jbe@1309:             if (wysihtml.lang.array([
bsw/jbe@1309:                 "div", "pre", "p",
bsw/jbe@1309:                 "table", "td", "th",
bsw/jbe@1309:                 "ul", "ol", "li",
bsw/jbe@1309:                 "dd", "dl",
bsw/jbe@1309:                 "footer", "header", "section",
bsw/jbe@1309:                 "h1", "h2", "h3", "h4", "h5", "h6"
bsw/jbe@1309:             ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
bsw/jbe@1309:                 // add space at first when unwraping non-textflow elements
bsw/jbe@1309:                 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
bsw/jbe@1309:                   fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (fragment.normalize) {
bsw/jbe@1309:               fragment.normalize();
bsw/jbe@1309:             }
bsw/jbe@1309:             return fragment;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           // Remove
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Converts all childnodes
bsw/jbe@1309:     for (i=0; i elements
bsw/jbe@1309:     if (cleanUp &&
bsw/jbe@1309:         newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
bsw/jbe@1309:         (!newNode.childNodes.length ||
bsw/jbe@1309:          ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) ||
bsw/jbe@1309:          !newNode.attributes.length)
bsw/jbe@1309:         ) {
bsw/jbe@1309:       fragment = newNode.ownerDocument.createDocumentFragment();
bsw/jbe@1309:       while (newNode.firstChild) {
bsw/jbe@1309:         fragment.appendChild(newNode.firstChild);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (fragment.normalize) {
bsw/jbe@1309:         fragment.normalize();
bsw/jbe@1309:       }
bsw/jbe@1309:       return fragment;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (newNode.normalize) {
bsw/jbe@1309:       newNode.normalize();
bsw/jbe@1309:     }
bsw/jbe@1309:     return newNode;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _applySelectorRules (element, selectorRules) {
bsw/jbe@1309:     var sel, method, els;
bsw/jbe@1309: 
bsw/jbe@1309:     for (sel in selectorRules) {
bsw/jbe@1309:       if (selectorRules.hasOwnProperty(sel)) {
bsw/jbe@1309:         if (wysihtml.lang.object(selectorRules[sel]).isFunction()) {
bsw/jbe@1309:           method = selectorRules[sel];
bsw/jbe@1309:         } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) {
bsw/jbe@1309:           method = elementHandlingMethods[selectorRules[sel]];
bsw/jbe@1309:         }
bsw/jbe@1309:         els = element.querySelectorAll(sel);
bsw/jbe@1309:         for (var i = els.length; i--;) {
bsw/jbe@1309:           method(els[i]);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _handleElement(oldNode, clearInternals) {
bsw/jbe@1309:     var rule,
bsw/jbe@1309:         newNode,
bsw/jbe@1309:         tagRules    = currentRules.tags,
bsw/jbe@1309:         nodeName    = oldNode.nodeName.toLowerCase(),
bsw/jbe@1309:         scopeName   = oldNode.scopeName,
bsw/jbe@1309:         renameTag;
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * We already parsed that element
bsw/jbe@1309:      * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
bsw/jbe@1309:      */
bsw/jbe@1309:     if (oldNode._wysihtml) {
bsw/jbe@1309:       return null;
bsw/jbe@1309:     }
bsw/jbe@1309:     oldNode._wysihtml = 1;
bsw/jbe@1309: 
bsw/jbe@1309:     if (oldNode.className === "wysihtml-temp") {
bsw/jbe@1309:       return null;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * IE is the only browser who doesn't include the namespace in the
bsw/jbe@1309:      * nodeName, that's why we have to prepend it by ourselves
bsw/jbe@1309:      * scopeName is a proprietary IE feature
bsw/jbe@1309:      * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
bsw/jbe@1309:      */
bsw/jbe@1309:     if (scopeName && scopeName != "HTML") {
bsw/jbe@1309:       nodeName = scopeName + ":" + nodeName;
bsw/jbe@1309:     }
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Repair node
bsw/jbe@1309:      * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
bsw/jbe@1309:      * A  doesn't need to be closed according HTML4-5 spec, we simply replace it with a 
 to preserve its content and layout
bsw/jbe@1309:      */
bsw/jbe@1309:     if ("outerHTML" in oldNode) {
bsw/jbe@1309:       if (!wysihtml.browser.autoClosesUnclosedTags() &&
bsw/jbe@1309:           oldNode.nodeName === "P" &&
bsw/jbe@1309:           oldNode.outerHTML.slice(-4).toLowerCase() !== "") {
bsw/jbe@1309:         nodeName = "div";
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (nodeName in tagRules) {
bsw/jbe@1309:       rule = tagRules[nodeName];
bsw/jbe@1309:       if (!rule || rule.remove) {
bsw/jbe@1309:         return null;
bsw/jbe@1309:       } else if (rule.unwrap) {
bsw/jbe@1309:         return false;
bsw/jbe@1309:       }
bsw/jbe@1309:       rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
bsw/jbe@1309:     } else if (oldNode.firstChild) {
bsw/jbe@1309:       rule = { rename_tag: DEFAULT_NODE_NAME };
bsw/jbe@1309:     } else {
bsw/jbe@1309:       // Remove empty unknown elements
bsw/jbe@1309:       return null;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // tests if type condition is met or node should be removed/unwrapped/renamed
bsw/jbe@1309:     if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) {
bsw/jbe@1309:       if (rule.remove_action) {
bsw/jbe@1309:         if (rule.remove_action === "unwrap") {
bsw/jbe@1309:           return false;
bsw/jbe@1309:         } else if (rule.remove_action === "rename") {
bsw/jbe@1309:           renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:       } else {
bsw/jbe@1309:         return null;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
bsw/jbe@1309:     _handleAttributes(oldNode, newNode, rule, clearInternals);
bsw/jbe@1309:     _handleStyles(oldNode, newNode, rule);
bsw/jbe@1309: 
bsw/jbe@1309:     oldNode = null;
bsw/jbe@1309: 
bsw/jbe@1309:     if (newNode.normalize) { newNode.normalize(); }
bsw/jbe@1309:     return newNode;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _testTypes(oldNode, rules, types, clearInternals) {
bsw/jbe@1309:     var definition, type;
bsw/jbe@1309: 
bsw/jbe@1309:     // do not interfere with placeholder span or pasting caret position is not maintained
bsw/jbe@1309:     if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) {
bsw/jbe@1309:       return true;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     for (type in types) {
bsw/jbe@1309:       if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) {
bsw/jbe@1309:         definition = rules.type_definitions[type];
bsw/jbe@1309:         if (_testType(oldNode, definition)) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function array_contains(a, obj) {
bsw/jbe@1309:       var i = a.length;
bsw/jbe@1309:       while (i--) {
bsw/jbe@1309:          if (a[i] === obj) {
bsw/jbe@1309:              return true;
bsw/jbe@1309:          }
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _testType(oldNode, definition) {
bsw/jbe@1309: 
bsw/jbe@1309:     var nodeClasses = oldNode.getAttribute("class"),
bsw/jbe@1309:         nodeStyles =  oldNode.getAttribute("style"),
bsw/jbe@1309:         classesLength, s, s_corrected, a, attr, currentClass, styleProp;
bsw/jbe@1309: 
bsw/jbe@1309:     // test for methods
bsw/jbe@1309:     if (definition.methods) {
bsw/jbe@1309:       for (var m in definition.methods) {
bsw/jbe@1309:         if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
bsw/jbe@1309: 
bsw/jbe@1309:           if (typeCeckMethods[m](oldNode)) {
bsw/jbe@1309:             return true;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // test for classes, if one found return true
bsw/jbe@1309:     if (nodeClasses && definition.classes) {
bsw/jbe@1309:       nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP);
bsw/jbe@1309:       classesLength = nodeClasses.length;
bsw/jbe@1309:       for (var i = 0; i < classesLength; i++) {
bsw/jbe@1309:         if (definition.classes[nodeClasses[i]]) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // test for styles, if one found return true
bsw/jbe@1309:     if (nodeStyles && definition.styles) {
bsw/jbe@1309: 
bsw/jbe@1309:       nodeStyles = nodeStyles.split(';');
bsw/jbe@1309:       for (s in definition.styles) {
bsw/jbe@1309:         if (definition.styles.hasOwnProperty(s)) {
bsw/jbe@1309:           for (var sp = nodeStyles.length; sp--;) {
bsw/jbe@1309:             styleProp = nodeStyles[sp].split(':');
bsw/jbe@1309: 
bsw/jbe@1309:             if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) {
bsw/jbe@1309:               if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) {
bsw/jbe@1309:                 return true;
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // test for attributes in general against regex match
bsw/jbe@1309:     if (definition.attrs) {
bsw/jbe@1309:         for (a in definition.attrs) {
bsw/jbe@1309:             if (definition.attrs.hasOwnProperty(a)) {
bsw/jbe@1309:                 attr = wysihtml.dom.getAttribute(oldNode, a);
bsw/jbe@1309:                 if (typeof(attr) === "string") {
bsw/jbe@1309:                     if (attr.search(definition.attrs[a]) > -1) {
bsw/jbe@1309:                         return true;
bsw/jbe@1309:                     }
bsw/jbe@1309:                 }
bsw/jbe@1309:             }
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _handleStyles(oldNode, newNode, rule) {
bsw/jbe@1309:     var s, v;
bsw/jbe@1309:     if(rule && rule.keep_styles) {
bsw/jbe@1309:       for (s in rule.keep_styles) {
bsw/jbe@1309:         if (rule.keep_styles.hasOwnProperty(s)) {
bsw/jbe@1309:           v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s];
bsw/jbe@1309:           // value can be regex and if so should match or style skipped
bsw/jbe@1309:           if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) {
bsw/jbe@1309:             continue;
bsw/jbe@1309:           }
bsw/jbe@1309:           if (s === "float") {
bsw/jbe@1309:             // IE compability
bsw/jbe@1309:             newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
bsw/jbe@1309:            } else if (oldNode.style[s]) {
bsw/jbe@1309:              newNode.style[s] = v;
bsw/jbe@1309:            }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   function _getAttributesBeginningWith(beginning, attributes) {
bsw/jbe@1309:     var returnAttributes = [];
bsw/jbe@1309:     for (var attr in attributes) {
bsw/jbe@1309:       if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) {
bsw/jbe@1309:         returnAttributes.push(attr);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return returnAttributes;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
bsw/jbe@1309:     var method = wysihtml.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName],
bsw/jbe@1309:         newAttributeValue;
bsw/jbe@1309: 
bsw/jbe@1309:     if (method) {
bsw/jbe@1309:       newAttributeValue = method(attributeValue, nodeName);
bsw/jbe@1309:       if (typeof(newAttributeValue) === "string") {
bsw/jbe@1309:         return newAttributeValue;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _checkAttributes(oldNode, local_attributes) {
bsw/jbe@1309:     var globalAttributes  = wysihtml.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes
bsw/jbe@1309:         checkAttributes   = wysihtml.lang.object(globalAttributes).merge( wysihtml.lang.object(local_attributes || {}).clone()).get(),
bsw/jbe@1309:         attributes        = {},
bsw/jbe@1309:         oldAttributes     = wysihtml.dom.getAttributes(oldNode),
bsw/jbe@1309:         attributeName, newValue, matchingAttributes;
bsw/jbe@1309: 
bsw/jbe@1309:     for (attributeName in checkAttributes) {
bsw/jbe@1309:       if ((/\*$/).test(attributeName)) {
bsw/jbe@1309: 
bsw/jbe@1309:         matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
bsw/jbe@1309:         for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
bsw/jbe@1309: 
bsw/jbe@1309:           newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
bsw/jbe@1309:           if (newValue !== false) {
bsw/jbe@1309:             attributes[matchingAttributes[i]] = newValue;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       } else {
bsw/jbe@1309:         newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
bsw/jbe@1309:         if (newValue !== false) {
bsw/jbe@1309:           attributes[attributeName] = newValue;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return attributes;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // TODO: refactor. Too long to read
bsw/jbe@1309:   function _handleAttributes(oldNode, newNode, rule, clearInternals) {
bsw/jbe@1309:     var attributes          = {},                         // fresh new set of attributes to set on newNode
bsw/jbe@1309:         setClass            = rule.set_class,             // classes to set
bsw/jbe@1309:         addClass            = rule.add_class,             // add classes based on existing attributes
bsw/jbe@1309:         addStyle            = rule.add_style,             // add styles based on existing attributes
bsw/jbe@1309:         setAttributes       = rule.set_attributes,        // attributes to set on the current node
bsw/jbe@1309:         allowedClasses      = currentRules.classes,
bsw/jbe@1309:         i                   = 0,
bsw/jbe@1309:         classes             = [],
bsw/jbe@1309:         styles              = [],
bsw/jbe@1309:         newClasses          = [],
bsw/jbe@1309:         oldClasses          = [],
bsw/jbe@1309:         classesLength,
bsw/jbe@1309:         newClassesLength,
bsw/jbe@1309:         currentClass,
bsw/jbe@1309:         newClass,
bsw/jbe@1309:         attributeName,
bsw/jbe@1309:         method;
bsw/jbe@1309: 
bsw/jbe@1309:     if (setAttributes) {
bsw/jbe@1309:       attributes = wysihtml.lang.object(setAttributes).clone();
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // check/convert values of attributes
bsw/jbe@1309:     attributes = wysihtml.lang.object(attributes).merge(_checkAttributes(oldNode,  rule.check_attributes)).get();
bsw/jbe@1309: 
bsw/jbe@1309:     if (setClass) {
bsw/jbe@1309:       classes.push(setClass);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (addClass) {
bsw/jbe@1309:       for (attributeName in addClass) {
bsw/jbe@1309:         method = addClassMethods[addClass[attributeName]];
bsw/jbe@1309:         if (!method) {
bsw/jbe@1309:           continue;
bsw/jbe@1309:         }
bsw/jbe@1309:         newClass = method(wysihtml.dom.getAttribute(oldNode, attributeName));
bsw/jbe@1309:         if (typeof(newClass) === "string") {
bsw/jbe@1309:           classes.push(newClass);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (addStyle) {
bsw/jbe@1309:       for (attributeName in addStyle) {
bsw/jbe@1309:         method = addStyleMethods[addStyle[attributeName]];
bsw/jbe@1309:         if (!method) {
bsw/jbe@1309:           continue;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         newStyle = method(wysihtml.dom.getAttribute(oldNode, attributeName));
bsw/jbe@1309:         if (typeof(newStyle) === "string") {
bsw/jbe@1309:           styles.push(newStyle);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:     if (typeof(allowedClasses) === "string" && allowedClasses === "any") {
bsw/jbe@1309:       if (oldNode.getAttribute("class")) {
bsw/jbe@1309:         if (currentRules.classes_blacklist) {
bsw/jbe@1309:           oldClasses = oldNode.getAttribute("class");
bsw/jbe@1309:           if (oldClasses) {
bsw/jbe@1309:             classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           classesLength = classes.length;
bsw/jbe@1309:           for (; i 0) {
bsw/jbe@1309:           attributes["class"] = wysihtml.lang.array(classes).unique().join(" ");
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     } else {
bsw/jbe@1309:       // make sure that wysihtml temp class doesn't get stripped out
bsw/jbe@1309:       if (!clearInternals) {
bsw/jbe@1309:         allowedClasses["_wysihtml-temp-placeholder"] = 1;
bsw/jbe@1309:         allowedClasses["_rangySelectionBoundary"] = 1;
bsw/jbe@1309:         allowedClasses["wysiwyg-tmp-selected-cell"] = 1;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // add old classes last
bsw/jbe@1309:       oldClasses = oldNode.getAttribute("class");
bsw/jbe@1309:       if (oldClasses) {
bsw/jbe@1309:         classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
bsw/jbe@1309:       }
bsw/jbe@1309:       classesLength = classes.length;
bsw/jbe@1309:       for (; i under https when it's new attribute value is non-https
bsw/jbe@1309:       // TODO: Investigate this further and check for smarter handling
bsw/jbe@1309:       try {
bsw/jbe@1309:         newNode.setAttribute(attributeName, attributes[attributeName]);
bsw/jbe@1309:       } catch(e) {}
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // IE8 sometimes loses the width/height attributes when those are set before the "src"
bsw/jbe@1309:     // so we make sure to set them again
bsw/jbe@1309:     if (attributes.src) {
bsw/jbe@1309:       if (typeof(attributes.width) !== "undefined") {
bsw/jbe@1309:         newNode.setAttribute("width", attributes.width);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (typeof(attributes.height) !== "undefined") {
bsw/jbe@1309:         newNode.setAttribute("height", attributes.height);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _handleText(oldNode) {
bsw/jbe@1309:     var nextSibling = oldNode.nextSibling;
bsw/jbe@1309:     if (nextSibling && nextSibling.nodeType === wysihtml.TEXT_NODE) {
bsw/jbe@1309:       // Concatenate text nodes
bsw/jbe@1309:       nextSibling.data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309:     } else {
bsw/jbe@1309:       // \uFEFF = wysihtml.INVISIBLE_SPACE (used as a hack in certain rich text editing situations)
bsw/jbe@1309:       var data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309:       return oldNode.ownerDocument.createTextNode(data);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _handleComment(oldNode) {
bsw/jbe@1309:     if (currentRules.comments) {
bsw/jbe@1309:       return oldNode.ownerDocument.createComment(oldNode.nodeValue);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // ------------ attribute checks ------------ \\
bsw/jbe@1309:   var attributeCheckMethods = {
bsw/jbe@1309:     url: (function() {
bsw/jbe@1309:       var REG_EXP = /^https?:\/\//i;
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         if (!attributeValue || !attributeValue.match(REG_EXP)) {
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:         return attributeValue.replace(REG_EXP, function(match) {
bsw/jbe@1309:           return match.toLowerCase();
bsw/jbe@1309:         });
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     src: (function() {
bsw/jbe@1309:       var REG_EXP = /^(\/|https?:\/\/)/i;
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         if (!attributeValue || !attributeValue.match(REG_EXP)) {
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:         return attributeValue.replace(REG_EXP, function(match) {
bsw/jbe@1309:           return match.toLowerCase();
bsw/jbe@1309:         });
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     href: (function() {
bsw/jbe@1309:       var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i;
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         if (!attributeValue || !attributeValue.match(REG_EXP)) {
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:         return attributeValue.replace(REG_EXP, function(match) {
bsw/jbe@1309:           return match.toLowerCase();
bsw/jbe@1309:         });
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     alt: (function() {
bsw/jbe@1309:       var REG_EXP = /[^ a-z0-9_\-]/gi;
bsw/jbe@1309:       return function(attributeValue, nodeName) {
bsw/jbe@1309:         if (!attributeValue) {
bsw/jbe@1309:           if (nodeName === "IMG") {
bsw/jbe@1309:             return "";
bsw/jbe@1309:           } else {
bsw/jbe@1309:             return null;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:         return attributeValue.replace(REG_EXP, "");
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     // Integers. Does not work with floating point numbers and units
bsw/jbe@1309:     numbers: (function() {
bsw/jbe@1309:       var REG_EXP = /\D/g;
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         attributeValue = (attributeValue || "").replace(REG_EXP, "");
bsw/jbe@1309:         return attributeValue || null;
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     // Useful for with/height attributes where floating points and percentages are allowed
bsw/jbe@1309:     dimension: (function() {
bsw/jbe@1309:       var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/;
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3");
bsw/jbe@1309:         return attributeValue || null;
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     any: (function() {
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         if (!attributeValue) {
bsw/jbe@1309:           return null;
bsw/jbe@1309:         }
bsw/jbe@1309:         return attributeValue;
bsw/jbe@1309:       };
bsw/jbe@1309:     })()
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // ------------ style converter (converts an html attribute to a style) ------------ \\
bsw/jbe@1309:   var addStyleMethods = {
bsw/jbe@1309:     align_text: (function() {
bsw/jbe@1309:       var mapping = {
bsw/jbe@1309:         left:     "text-align: left;",
bsw/jbe@1309:         right:    "text-align: right;",
bsw/jbe@1309:         center:   "text-align: center;"
bsw/jbe@1309:       };
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // ------------ class converter (converts an html attribute to a class name) ------------ \\
bsw/jbe@1309:   var addClassMethods = {
bsw/jbe@1309:     align_img: (function() {
bsw/jbe@1309:       var mapping = {
bsw/jbe@1309:         left:   "wysiwyg-float-left",
bsw/jbe@1309:         right:  "wysiwyg-float-right"
bsw/jbe@1309:       };
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     align_text: (function() {
bsw/jbe@1309:       var mapping = {
bsw/jbe@1309:         left:     "wysiwyg-text-align-left",
bsw/jbe@1309:         right:    "wysiwyg-text-align-right",
bsw/jbe@1309:         center:   "wysiwyg-text-align-center",
bsw/jbe@1309:         justify:  "wysiwyg-text-align-justify"
bsw/jbe@1309:       };
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     clear_br: (function() {
bsw/jbe@1309:       var mapping = {
bsw/jbe@1309:         left:   "wysiwyg-clear-left",
bsw/jbe@1309:         right:  "wysiwyg-clear-right",
bsw/jbe@1309:         both:   "wysiwyg-clear-both",
bsw/jbe@1309:         all:    "wysiwyg-clear-both"
bsw/jbe@1309:       };
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309:       };
bsw/jbe@1309:     })(),
bsw/jbe@1309: 
bsw/jbe@1309:     size_font: (function() {
bsw/jbe@1309:       var mapping = {
bsw/jbe@1309:         "1": "wysiwyg-font-size-xx-small",
bsw/jbe@1309:         "2": "wysiwyg-font-size-small",
bsw/jbe@1309:         "3": "wysiwyg-font-size-medium",
bsw/jbe@1309:         "4": "wysiwyg-font-size-large",
bsw/jbe@1309:         "5": "wysiwyg-font-size-x-large",
bsw/jbe@1309:         "6": "wysiwyg-font-size-xx-large",
bsw/jbe@1309:         "7": "wysiwyg-font-size-xx-large",
bsw/jbe@1309:         "-": "wysiwyg-font-size-smaller",
bsw/jbe@1309:         "+": "wysiwyg-font-size-larger"
bsw/jbe@1309:       };
bsw/jbe@1309:       return function(attributeValue) {
bsw/jbe@1309:         return mapping[String(attributeValue).charAt(0)];
bsw/jbe@1309:       };
bsw/jbe@1309:     })()
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // checks if element is possibly visible
bsw/jbe@1309:   var typeCeckMethods = {
bsw/jbe@1309:     has_visible_contet: (function() {
bsw/jbe@1309:       var txt,
bsw/jbe@1309:           isVisible = false,
bsw/jbe@1309:           visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
bsw/jbe@1309:                              'style', 'table', 'iframe', 'object', 'embed', 'audio',
bsw/jbe@1309:                              'svg', 'input', 'button', 'select','textarea', 'canvas'];
bsw/jbe@1309: 
bsw/jbe@1309:       return function(el) {
bsw/jbe@1309: 
bsw/jbe@1309:         // has visible innertext. so is visible
bsw/jbe@1309:         txt = (el.innerText || el.textContent).replace(/\s/g, '');
bsw/jbe@1309:         if (txt && txt.length > 0) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // matches list of visible dimensioned elements
bsw/jbe@1309:         for (var i = visibleElements.length; i--;) {
bsw/jbe@1309:           if (el.querySelector(visibleElements[i])) {
bsw/jbe@1309:             return true;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // try to measure dimesions in last resort. (can find only of elements in dom)
bsw/jbe@1309:         if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return false;
bsw/jbe@1309:       };
bsw/jbe@1309:     })()
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var elementHandlingMethods = {
bsw/jbe@1309:     unwrap: function (element) {
bsw/jbe@1309:       wysihtml.dom.unwrap(element);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     remove: function (element) {
bsw/jbe@1309:       element.parentNode.removeChild(element);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   return parse(elementOrHtml_current, config_current);
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: // does a selector query on element or array of elements
bsw/jbe@1309: wysihtml.dom.query = function(elements, query) {
bsw/jbe@1309:     var ret = [],
bsw/jbe@1309:         q;
bsw/jbe@1309: 
bsw/jbe@1309:     if (elements.nodeType) {
bsw/jbe@1309:         elements = [elements];
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     for (var e = 0, len = elements.length; e < len; e++) {
bsw/jbe@1309:         q = elements[e].querySelectorAll(query);
bsw/jbe@1309:         if (q) {
bsw/jbe@1309:             for(var i = q.length; i--; ret.unshift(q[i]));
bsw/jbe@1309:         }
bsw/jbe@1309:     }
bsw/jbe@1309:     return ret;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Checks for empty text node childs and removes them
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} node The element in which to cleanup
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.removeEmptyTextNodes(element);
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.removeEmptyTextNodes = function(node) {
bsw/jbe@1309:   var childNode,
bsw/jbe@1309:       childNodes        = wysihtml.lang.array(node.childNodes).get(),
bsw/jbe@1309:       childNodesLength  = childNodes.length,
bsw/jbe@1309:       i                 = 0;
bsw/jbe@1309: 
bsw/jbe@1309:   for (; i to a ) and keeps its childs
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} element The list element which should be renamed
bsw/jbe@1309:  * @param {Element} newNodeName The desired tag name
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    
bsw/jbe@1309:  *    
bsw/jbe@1309:  *      - eminembsw/jbe@1309:  *
- dr. drebsw/jbe@1309:  *
- 50 Centbsw/jbe@1309:  *
bsw/jbe@1309:  *
bsw/jbe@1309:  *    
bsw/jbe@1309:  *
bsw/jbe@1309:  *    
bsw/jbe@1309:  *
bsw/jbe@1309:  *      - eminembsw/jbe@1309:  *
- dr. drebsw/jbe@1309:  *
- 50 Centbsw/jbe@1309:  *
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.renameElement = function(element, newNodeName) {
bsw/jbe@1309:   var newElement = element.ownerDocument.createElement(newNodeName),
bsw/jbe@1309:       firstChild;
bsw/jbe@1309:   while (firstChild = element.firstChild) {
bsw/jbe@1309:     newElement.appendChild(firstChild);
bsw/jbe@1309:   }
bsw/jbe@1309:   wysihtml.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
bsw/jbe@1309:   
bsw/jbe@1309:   if (element.parentNode) {
bsw/jbe@1309:     element.parentNode.replaceChild(newElement, element);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   return newElement;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Takes an element, removes it and replaces it with it's childs
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Object} node The node which to replace with it's child nodes
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    
bsw/jbe@1309:  *      hello
bsw/jbe@1309:  *    
bsw/jbe@1309:  *    
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.dom.replaceWithChildNodes = function(node) {
bsw/jbe@1309:   if (!node.parentNode) {
bsw/jbe@1309:     return;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   while (node.firstChild) {
bsw/jbe@1309:     node.parentNode.insertBefore(node.firstChild, node);
bsw/jbe@1309:   }
bsw/jbe@1309:   node.parentNode.removeChild(node);
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Unwraps an unordered/ordered list
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} element The list element which should be unwrapped
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    
bsw/jbe@1309:  *
bsw/jbe@1309:  *      - eminembsw/jbe@1309:  *
- dr. drebsw/jbe@1309:  *
- 50 Centbsw/jbe@1309:  *
bsw/jbe@1309:  *
bsw/jbe@1309:  *    
bsw/jbe@1309:  *
bsw/jbe@1309:  *    
bsw/jbe@1309:  *    eminem
bsw/jbe@1309:  *    dr. dre
bsw/jbe@1309:  *    50 Cent
bsw/jbe@1309:  */
bsw/jbe@1309: (function(dom) {
bsw/jbe@1309:   function _isBlockElement(node) {
bsw/jbe@1309:     return dom.getStyle("display").from(node) === "block";
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _isLineBreak(node) {
bsw/jbe@1309:     return node.nodeName === "BR";
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function _appendLineBreak(element) {
bsw/jbe@1309:     var lineBreak = element.ownerDocument.createElement("br");
bsw/jbe@1309:     element.appendChild(lineBreak);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function resolveList(list, useLineBreaks) {
bsw/jbe@1309:     if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var doc             = list.ownerDocument,
bsw/jbe@1309:         fragment        = doc.createDocumentFragment(),
bsw/jbe@1309:         previousSibling = wysihtml.dom.domNode(list).prev({ignoreBlankTexts: true}),
bsw/jbe@1309:         nextSibling = wysihtml.dom.domNode(list).next({ignoreBlankTexts: true}),
bsw/jbe@1309:         firstChild,
bsw/jbe@1309:         lastChild,
bsw/jbe@1309:         isLastChild,
bsw/jbe@1309:         shouldAppendLineBreak,
bsw/jbe@1309:         paragraph,
bsw/jbe@1309:         listItem,
bsw/jbe@1309:         lastListItem = list.lastElementChild || list.lastChild,
bsw/jbe@1309:         isLastItem;
bsw/jbe@1309: 
bsw/jbe@1309:     if (useLineBreaks) {
bsw/jbe@1309:       // Insert line break if list is after a non-block element
bsw/jbe@1309:       if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) {
bsw/jbe@1309:         _appendLineBreak(fragment);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       while (listItem = (list.firstElementChild || list.firstChild)) {
bsw/jbe@1309:         lastChild = listItem.lastChild;
bsw/jbe@1309:         isLastItem = listItem === lastListItem;
bsw/jbe@1309:         while (firstChild = listItem.firstChild) {
bsw/jbe@1309:           isLastChild           = firstChild === lastChild;
bsw/jbe@1309:           // This needs to be done before appending it to the fragment, as it otherwise will lose style information
bsw/jbe@1309:           shouldAppendLineBreak = (!isLastItem || (nextSibling && !_isBlockElement(nextSibling))) && isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
bsw/jbe@1309:           fragment.appendChild(firstChild);
bsw/jbe@1309:           if (shouldAppendLineBreak) {
bsw/jbe@1309:             _appendLineBreak(fragment);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         listItem.parentNode.removeChild(listItem);
bsw/jbe@1309:       }
bsw/jbe@1309:     } else {
bsw/jbe@1309:       while (listItem = (list.firstElementChild || list.firstChild)) {
bsw/jbe@1309:         if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) {
bsw/jbe@1309:           while (firstChild = listItem.firstChild) {
bsw/jbe@1309:             fragment.appendChild(firstChild);
bsw/jbe@1309:           }
bsw/jbe@1309:         } else {
bsw/jbe@1309:           paragraph = doc.createElement("p");
bsw/jbe@1309:           while (firstChild = listItem.firstChild) {
bsw/jbe@1309:             paragraph.appendChild(firstChild);
bsw/jbe@1309:           }
bsw/jbe@1309:           fragment.appendChild(paragraph);
bsw/jbe@1309:         }
bsw/jbe@1309:         listItem.parentNode.removeChild(listItem);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     list.parentNode.replaceChild(fragment, list);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   dom.resolveList = resolveList;
bsw/jbe@1309: })(wysihtml.dom);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
bsw/jbe@1309:  *
bsw/jbe@1309:  * Browser Compatibility:
bsw/jbe@1309:  *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
bsw/jbe@1309:  *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
bsw/jbe@1309:  *
bsw/jbe@1309:  * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
bsw/jbe@1309:  *    - sandboxing doesn't work correctly with inlined content (src="javascript:'...'")
bsw/jbe@1309:  *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
bsw/jbe@1309:  *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
bsw/jbe@1309:  *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
bsw/jbe@1309:  *      can do anything as if the sandbox attribute wasn't set
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
bsw/jbe@1309:  * @param {Object} [config] Optional parameters
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    new wysihtml.dom.Sandbox(function(sandbox) {
bsw/jbe@1309:  *      sandbox.getWindow().document.body.innerHTML = ' ';
bsw/jbe@1309:  *    });
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var /**
bsw/jbe@1309:        * Default configuration
bsw/jbe@1309:        */
bsw/jbe@1309:       doc                 = document,
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Properties to unset/protect on the window object
bsw/jbe@1309:        */
bsw/jbe@1309:       windowProperties    = [
bsw/jbe@1309:         "parent", "top", "opener", "frameElement", "frames",
bsw/jbe@1309:         "localStorage", "globalStorage", "sessionStorage", "indexedDB"
bsw/jbe@1309:       ],
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Properties on the window object which are set to an empty function
bsw/jbe@1309:        */
bsw/jbe@1309:       windowProperties2   = [
bsw/jbe@1309:         "open", "close", "openDialog", "showModalDialog",
bsw/jbe@1309:         "alert", "confirm", "prompt",
bsw/jbe@1309:         "openDatabase", "postMessage",
bsw/jbe@1309:         "XMLHttpRequest", "XDomainRequest"
bsw/jbe@1309:       ],
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Properties to unset/protect on the document object
bsw/jbe@1309:        */
bsw/jbe@1309:       documentProperties  = [
bsw/jbe@1309:         "referrer",
bsw/jbe@1309:         "write", "open", "close"
bsw/jbe@1309:       ];
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.dom.Sandbox = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.dom.Sandbox.prototype */ {
bsw/jbe@1309: 
bsw/jbe@1309:     constructor: function(readyCallback, config) {
bsw/jbe@1309:       this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
bsw/jbe@1309:       this.config   = wysihtml.lang.object({}).merge(config).get();
bsw/jbe@1309:       if (!this.config.className) {
bsw/jbe@1309:         this.config.className = "wysihtml-sandbox";
bsw/jbe@1309:       }
bsw/jbe@1309:       this.editableArea   = this._createIframe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     insertInto: function(element) {
bsw/jbe@1309:       if (typeof(element) === "string") {
bsw/jbe@1309:         element = doc.getElementById(element);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       element.appendChild(this.editableArea);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getIframe: function() {
bsw/jbe@1309:       return this.editableArea;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getWindow: function() {
bsw/jbe@1309:       this._readyError();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getDocument: function() {
bsw/jbe@1309:       this._readyError();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     destroy: function() {
bsw/jbe@1309:       var iframe = this.getIframe();
bsw/jbe@1309:       iframe.parentNode.removeChild(iframe);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _readyError: function() {
bsw/jbe@1309:       throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Creates the sandbox iframe
bsw/jbe@1309:      *
bsw/jbe@1309:      * Some important notes:
bsw/jbe@1309:      *  - We can't use HTML5 sandbox for now:
bsw/jbe@1309:      *    setting it causes that the iframe's dom can't be accessed from the outside
bsw/jbe@1309:      *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
bsw/jbe@1309:      *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
bsw/jbe@1309:      *    In order to make this happen we need to set the "allow-scripts" flag.
bsw/jbe@1309:      *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
bsw/jbe@1309:      *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
bsw/jbe@1309:      *  - IE needs to have the security="restricted" attribute set before the iframe is
bsw/jbe@1309:      *    inserted into the dom tree
bsw/jbe@1309:      *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
bsw/jbe@1309:      *    though it supports it
bsw/jbe@1309:      *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
bsw/jbe@1309:      *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
bsw/jbe@1309:      *    on the onreadystatechange event
bsw/jbe@1309:      */
bsw/jbe@1309:     _createIframe: function() {
bsw/jbe@1309:       var that   = this,
bsw/jbe@1309:           iframe = doc.createElement("iframe");
bsw/jbe@1309:       iframe.className = this.config.className;
bsw/jbe@1309:       wysihtml.dom.setAttributes({
bsw/jbe@1309:         "security":           "restricted",
bsw/jbe@1309:         "allowtransparency":  "true",
bsw/jbe@1309:         "frameborder":        0,
bsw/jbe@1309:         "width":              0,
bsw/jbe@1309:         "height":             0,
bsw/jbe@1309:         "marginwidth":        0,
bsw/jbe@1309:         "marginheight":       0
bsw/jbe@1309:       }).on(iframe);
bsw/jbe@1309: 
bsw/jbe@1309:       // Setting the src like this prevents ssl warnings in IE6
bsw/jbe@1309:       if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
bsw/jbe@1309:         iframe.src = "javascript:''";
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       iframe.onload = function() {
bsw/jbe@1309:         iframe.onreadystatechange = iframe.onload = null;
bsw/jbe@1309:         that._onLoadIframe(iframe);
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       iframe.onreadystatechange = function() {
bsw/jbe@1309:         if (/loaded|complete/.test(iframe.readyState)) {
bsw/jbe@1309:           iframe.onreadystatechange = iframe.onload = null;
bsw/jbe@1309:           that._onLoadIframe(iframe);
bsw/jbe@1309:         }
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       return iframe;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Callback for when the iframe has finished loading
bsw/jbe@1309:      */
bsw/jbe@1309:     _onLoadIframe: function(iframe) {
bsw/jbe@1309:       // don't resume when the iframe got unloaded (eg. by removing it from the dom)
bsw/jbe@1309:       if (!wysihtml.dom.contains(doc.documentElement, iframe)) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var that           = this,
bsw/jbe@1309:           iframeWindow   = iframe.contentWindow,
bsw/jbe@1309:           iframeDocument = iframe.contentWindow.document,
bsw/jbe@1309:           charset        = doc.characterSet || doc.charset || "utf-8",
bsw/jbe@1309:           sandboxHtml    = this._getHtml({
bsw/jbe@1309:             charset:      charset,
bsw/jbe@1309:             stylesheets:  this.config.stylesheets
bsw/jbe@1309:           });
bsw/jbe@1309: 
bsw/jbe@1309:       // Create the basic dom tree including proper DOCTYPE and charset
bsw/jbe@1309:       iframeDocument.open("text/html", "replace");
bsw/jbe@1309:       iframeDocument.write(sandboxHtml);
bsw/jbe@1309:       iframeDocument.close();
bsw/jbe@1309: 
bsw/jbe@1309:       this.getWindow = function() { return iframe.contentWindow; };
bsw/jbe@1309:       this.getDocument = function() { return iframe.contentWindow.document; };
bsw/jbe@1309: 
bsw/jbe@1309:       // Catch js errors and pass them to the parent's onerror event
bsw/jbe@1309:       // addEventListener("error") doesn't work properly in some browsers
bsw/jbe@1309:       // TODO: apparently this doesn't work in IE9!
bsw/jbe@1309:       iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
bsw/jbe@1309:         throw new Error("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber);
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       if (!wysihtml.browser.supportsSandboxedIframes()) {
bsw/jbe@1309:         // Unset a bunch of sensitive variables
bsw/jbe@1309:         // Please note: This isn't hack safe!
bsw/jbe@1309:         // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
bsw/jbe@1309:         // IE is secure though, which is the most important thing, since IE is the only browser, who
bsw/jbe@1309:         // takes over scripts & styles into contentEditable elements when copied from external websites
bsw/jbe@1309:         // or applications (Microsoft Word, ...)
bsw/jbe@1309:         var i, length;
bsw/jbe@1309:         for (i=0, length=windowProperties.length; i
';
bsw/jbe@1309:  *    });
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var /**
bsw/jbe@1309:        * Default configuration
bsw/jbe@1309:        */
bsw/jbe@1309:       doc                 = document,
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Properties to unset/protect on the window object
bsw/jbe@1309:        */
bsw/jbe@1309:       windowProperties    = [
bsw/jbe@1309:         "parent", "top", "opener", "frameElement", "frames",
bsw/jbe@1309:         "localStorage", "globalStorage", "sessionStorage", "indexedDB"
bsw/jbe@1309:       ],
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Properties on the window object which are set to an empty function
bsw/jbe@1309:        */
bsw/jbe@1309:       windowProperties2   = [
bsw/jbe@1309:         "open", "close", "openDialog", "showModalDialog",
bsw/jbe@1309:         "alert", "confirm", "prompt",
bsw/jbe@1309:         "openDatabase", "postMessage",
bsw/jbe@1309:         "XMLHttpRequest", "XDomainRequest"
bsw/jbe@1309:       ],
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Properties to unset/protect on the document object
bsw/jbe@1309:        */
bsw/jbe@1309:       documentProperties  = [
bsw/jbe@1309:         "referrer",
bsw/jbe@1309:         "write", "open", "close"
bsw/jbe@1309:       ];
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.dom.Sandbox = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.dom.Sandbox.prototype */ {
bsw/jbe@1309: 
bsw/jbe@1309:     constructor: function(readyCallback, config) {
bsw/jbe@1309:       this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
bsw/jbe@1309:       this.config   = wysihtml.lang.object({}).merge(config).get();
bsw/jbe@1309:       if (!this.config.className) {
bsw/jbe@1309:         this.config.className = "wysihtml-sandbox";
bsw/jbe@1309:       }
bsw/jbe@1309:       this.editableArea   = this._createIframe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     insertInto: function(element) {
bsw/jbe@1309:       if (typeof(element) === "string") {
bsw/jbe@1309:         element = doc.getElementById(element);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       element.appendChild(this.editableArea);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getIframe: function() {
bsw/jbe@1309:       return this.editableArea;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getWindow: function() {
bsw/jbe@1309:       this._readyError();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getDocument: function() {
bsw/jbe@1309:       this._readyError();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     destroy: function() {
bsw/jbe@1309:       var iframe = this.getIframe();
bsw/jbe@1309:       iframe.parentNode.removeChild(iframe);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _readyError: function() {
bsw/jbe@1309:       throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Creates the sandbox iframe
bsw/jbe@1309:      *
bsw/jbe@1309:      * Some important notes:
bsw/jbe@1309:      *  - We can't use HTML5 sandbox for now:
bsw/jbe@1309:      *    setting it causes that the iframe's dom can't be accessed from the outside
bsw/jbe@1309:      *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
bsw/jbe@1309:      *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
bsw/jbe@1309:      *    In order to make this happen we need to set the "allow-scripts" flag.
bsw/jbe@1309:      *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
bsw/jbe@1309:      *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
bsw/jbe@1309:      *  - IE needs to have the security="restricted" attribute set before the iframe is
bsw/jbe@1309:      *    inserted into the dom tree
bsw/jbe@1309:      *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
bsw/jbe@1309:      *    though it supports it
bsw/jbe@1309:      *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
bsw/jbe@1309:      *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
bsw/jbe@1309:      *    on the onreadystatechange event
bsw/jbe@1309:      */
bsw/jbe@1309:     _createIframe: function() {
bsw/jbe@1309:       var that   = this,
bsw/jbe@1309:           iframe = doc.createElement("iframe");
bsw/jbe@1309:       iframe.className = this.config.className;
bsw/jbe@1309:       wysihtml.dom.setAttributes({
bsw/jbe@1309:         "security":           "restricted",
bsw/jbe@1309:         "allowtransparency":  "true",
bsw/jbe@1309:         "frameborder":        0,
bsw/jbe@1309:         "width":              0,
bsw/jbe@1309:         "height":             0,
bsw/jbe@1309:         "marginwidth":        0,
bsw/jbe@1309:         "marginheight":       0
bsw/jbe@1309:       }).on(iframe);
bsw/jbe@1309: 
bsw/jbe@1309:       // Setting the src like this prevents ssl warnings in IE6
bsw/jbe@1309:       if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
bsw/jbe@1309:         iframe.src = "javascript:''";
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       iframe.onload = function() {
bsw/jbe@1309:         iframe.onreadystatechange = iframe.onload = null;
bsw/jbe@1309:         that._onLoadIframe(iframe);
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       iframe.onreadystatechange = function() {
bsw/jbe@1309:         if (/loaded|complete/.test(iframe.readyState)) {
bsw/jbe@1309:           iframe.onreadystatechange = iframe.onload = null;
bsw/jbe@1309:           that._onLoadIframe(iframe);
bsw/jbe@1309:         }
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       return iframe;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Callback for when the iframe has finished loading
bsw/jbe@1309:      */
bsw/jbe@1309:     _onLoadIframe: function(iframe) {
bsw/jbe@1309:       // don't resume when the iframe got unloaded (eg. by removing it from the dom)
bsw/jbe@1309:       if (!wysihtml.dom.contains(doc.documentElement, iframe)) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var that           = this,
bsw/jbe@1309:           iframeWindow   = iframe.contentWindow,
bsw/jbe@1309:           iframeDocument = iframe.contentWindow.document,
bsw/jbe@1309:           charset        = doc.characterSet || doc.charset || "utf-8",
bsw/jbe@1309:           sandboxHtml    = this._getHtml({
bsw/jbe@1309:             charset:      charset,
bsw/jbe@1309:             stylesheets:  this.config.stylesheets
bsw/jbe@1309:           });
bsw/jbe@1309: 
bsw/jbe@1309:       // Create the basic dom tree including proper DOCTYPE and charset
bsw/jbe@1309:       iframeDocument.open("text/html", "replace");
bsw/jbe@1309:       iframeDocument.write(sandboxHtml);
bsw/jbe@1309:       iframeDocument.close();
bsw/jbe@1309: 
bsw/jbe@1309:       this.getWindow = function() { return iframe.contentWindow; };
bsw/jbe@1309:       this.getDocument = function() { return iframe.contentWindow.document; };
bsw/jbe@1309: 
bsw/jbe@1309:       // Catch js errors and pass them to the parent's onerror event
bsw/jbe@1309:       // addEventListener("error") doesn't work properly in some browsers
bsw/jbe@1309:       // TODO: apparently this doesn't work in IE9!
bsw/jbe@1309:       iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
bsw/jbe@1309:         throw new Error("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber);
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       if (!wysihtml.browser.supportsSandboxedIframes()) {
bsw/jbe@1309:         // Unset a bunch of sensitive variables
bsw/jbe@1309:         // Please note: This isn't hack safe!
bsw/jbe@1309:         // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
bsw/jbe@1309:         // IE is secure though, which is the most important thing, since IE is the only browser, who
bsw/jbe@1309:         // takes over scripts & styles into contentEditable elements when copied from external websites
bsw/jbe@1309:         // or applications (Microsoft Word, ...)
bsw/jbe@1309:         var i, length;
bsw/jbe@1309:         for (i=0, length=windowProperties.length; i';
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       templateVars.stylesheets = html;
bsw/jbe@1309: 
bsw/jbe@1309:       return wysihtml.lang.string(
bsw/jbe@1309:         ''
bsw/jbe@1309:         + '#{stylesheets}'
bsw/jbe@1309:         + ''
bsw/jbe@1309:       ).interpolate(templateVars);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Method to unset/override existing variables
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    // Make cookie unreadable and unwritable
bsw/jbe@1309:      *    this._unset(document, "cookie", "", true);
bsw/jbe@1309:      */
bsw/jbe@1309:     _unset: function(object, property, value, setter) {
bsw/jbe@1309:       try { object[property] = value; } catch(e) {}
bsw/jbe@1309: 
bsw/jbe@1309:       try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
bsw/jbe@1309:       if (setter) {
bsw/jbe@1309:         try { object.__defineSetter__(property, function() {}); } catch(e) {}
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (!wysihtml.browser.crashesWhenDefineProperty(property)) {
bsw/jbe@1309:         try {
bsw/jbe@1309:           var config = {
bsw/jbe@1309:             get: function() { return value; }
bsw/jbe@1309:           };
bsw/jbe@1309:           if (setter) {
bsw/jbe@1309:             config.set = function() {};
bsw/jbe@1309:           }
bsw/jbe@1309:           Object.defineProperty(object, property, config);
bsw/jbe@1309:         } catch(e) {}
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   });
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: (function() {
bsw/jbe@1309:   var mapping = {
bsw/jbe@1309:     "className": "class"
bsw/jbe@1309:   };
bsw/jbe@1309:   wysihtml.dom.setAttributes = function(attributes) {
bsw/jbe@1309:     return {
bsw/jbe@1309:       on: function(element) {
bsw/jbe@1309:         for (var i in attributes) {
bsw/jbe@1309:           element.setAttribute(mapping[i] || i, attributes[i]);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     };
bsw/jbe@1309:   };
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.setStyles = function(styles) {
bsw/jbe@1309:   return {
bsw/jbe@1309:     on: function(element) {
bsw/jbe@1309:       var style = element.style;
bsw/jbe@1309:       if (typeof(styles) === "string") {
bsw/jbe@1309:         style.cssText += ";" + styles;
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309:       for (var i in styles) {
bsw/jbe@1309:         if (i === "float") {
bsw/jbe@1309:           style.cssFloat = styles[i];
bsw/jbe@1309:           style.styleFloat = styles[i];
bsw/jbe@1309:         } else {
bsw/jbe@1309:           style[i] = styles[i];
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Simulate HTML5 placeholder attribute
bsw/jbe@1309:  *
bsw/jbe@1309:  * Needed since
bsw/jbe@1309:  *    - div[contentEditable] elements don't support it
bsw/jbe@1309:  *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Object} parent Instance of main wysihtml.Editor class
bsw/jbe@1309:  * @param {Element} view Instance of wysihtml.views.* class
bsw/jbe@1309:  * @param {String} placeholderText
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
bsw/jbe@1309:  */
bsw/jbe@1309: (function(dom) {
bsw/jbe@1309:   dom.simulatePlaceholder = function(editor, view, placeholderText, placeholderClassName) {
bsw/jbe@1309:     var CLASS_NAME = placeholderClassName || "wysihtml-placeholder",
bsw/jbe@1309:         unset = function() {
bsw/jbe@1309:           var composerIsVisible   = view.element.offsetWidth > 0 && view.element.offsetHeight > 0;
bsw/jbe@1309:           if (view.hasPlaceholderSet()) {
bsw/jbe@1309:             view.clear();
bsw/jbe@1309:             view.element.focus();
bsw/jbe@1309:             if (composerIsVisible ) {
bsw/jbe@1309:               setTimeout(function() {
bsw/jbe@1309:                 var sel = view.selection.getSelection();
bsw/jbe@1309:                 if (!sel.focusNode || !sel.anchorNode) {
bsw/jbe@1309:                   view.selection.selectNode(view.element.firstChild || view.element);
bsw/jbe@1309:                 }
bsw/jbe@1309:               }, 0);
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:           view.placeholderSet = false;
bsw/jbe@1309:           dom.removeClass(view.element, CLASS_NAME);
bsw/jbe@1309:         },
bsw/jbe@1309:         set = function() {
bsw/jbe@1309:           if (view.isEmpty() && !view.placeholderSet) {
bsw/jbe@1309:             view.placeholderSet = true;
bsw/jbe@1309:             view.setValue(placeholderText, false);
bsw/jbe@1309:             dom.addClass(view.element, CLASS_NAME);
bsw/jbe@1309:           }
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:     editor
bsw/jbe@1309:       .on("set_placeholder", set)
bsw/jbe@1309:       .on("unset_placeholder", unset)
bsw/jbe@1309:       .on("focus:composer", unset)
bsw/jbe@1309:       .on("paste:composer", unset)
bsw/jbe@1309:       .on("blur:composer", set);
bsw/jbe@1309: 
bsw/jbe@1309:     set();
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml.dom);
bsw/jbe@1309: 
bsw/jbe@1309: (function(dom) {
bsw/jbe@1309:   var documentElement = document.documentElement;
bsw/jbe@1309:   if ("textContent" in documentElement) {
bsw/jbe@1309:     dom.setTextContent = function(element, text) {
bsw/jbe@1309:       element.textContent = text;
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     dom.getTextContent = function(element) {
bsw/jbe@1309:       return element.textContent;
bsw/jbe@1309:     };
bsw/jbe@1309:   } else if ("innerText" in documentElement) {
bsw/jbe@1309:     dom.setTextContent = function(element, text) {
bsw/jbe@1309:       element.innerText = text;
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     dom.getTextContent = function(element) {
bsw/jbe@1309:       return element.innerText;
bsw/jbe@1309:     };
bsw/jbe@1309:   } else {
bsw/jbe@1309:     dom.setTextContent = function(element, text) {
bsw/jbe@1309:       element.nodeValue = text;
bsw/jbe@1309:     };
bsw/jbe@1309: 
bsw/jbe@1309:     dom.getTextContent = function(element) {
bsw/jbe@1309:       return element.nodeValue;
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: })(wysihtml.dom);
bsw/jbe@1309: 
bsw/jbe@1309: /* Unwraps element and returns list of childNodes that the node contained.
bsw/jbe@1309:  *
bsw/jbe@1309:  * Example:
bsw/jbe@1309:  *    var childnodes = wysihtml.dom.unwrap(document.querySelector('.unwrap-me'));
bsw/jbe@1309: */
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.dom.unwrap = function(node) {
bsw/jbe@1309:   var children = [];
bsw/jbe@1309:   if (node.parentNode) {
bsw/jbe@1309:     while (node.lastChild) {
bsw/jbe@1309:       children.unshift(node.lastChild);
bsw/jbe@1309:       wysihtml.dom.insert(node.lastChild).after(node);
bsw/jbe@1309:     }
bsw/jbe@1309:     node.parentNode.removeChild(node);
bsw/jbe@1309:   }
bsw/jbe@1309:   return children;
bsw/jbe@1309: };
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Fix most common html formatting misbehaviors of browsers implementation when inserting
bsw/jbe@1309:  * content via copy & paste contentEditable
bsw/jbe@1309:  *
bsw/jbe@1309:  * @author Christopher Blum
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.quirks.cleanPastedHTML = (function() {
bsw/jbe@1309: 
bsw/jbe@1309:   var styleToRegex = function (styleStr) {
bsw/jbe@1309:     var trimmedStr = wysihtml.lang.string(styleStr).trim(),
bsw/jbe@1309:         escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
bsw/jbe@1309: 
bsw/jbe@1309:     return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
bsw/jbe@1309:     var newRules = wysihtml.lang.object(rules).clone(true),
bsw/jbe@1309:         tag, style;
bsw/jbe@1309: 
bsw/jbe@1309:     for (tag in newRules.tags) {
bsw/jbe@1309: 
bsw/jbe@1309:       if (newRules.tags.hasOwnProperty(tag)) {
bsw/jbe@1309:         if (newRules.tags[tag].keep_styles) {
bsw/jbe@1309:           for (style in newRules.tags[tag].keep_styles) {
bsw/jbe@1309:             if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) {
bsw/jbe@1309:               if (exceptStyles[style]) {
bsw/jbe@1309:                 newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]);
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return newRules;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var pickRuleset = function(ruleset, html) {
bsw/jbe@1309:     var pickedSet, defaultSet;
bsw/jbe@1309: 
bsw/jbe@1309:     if (!ruleset) {
bsw/jbe@1309:       return null;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     for (var i = 0, max = ruleset.length; i < max; i++) {
bsw/jbe@1309:       if (!ruleset[i].condition) {
bsw/jbe@1309:         defaultSet = ruleset[i].set;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (ruleset[i].condition && ruleset[i].condition.test(html)) {
bsw/jbe@1309:         return ruleset[i].set;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return defaultSet;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   return function(html, options) {
bsw/jbe@1309:     var exceptStyles = {
bsw/jbe@1309:           'color': wysihtml.dom.getStyle("color").from(options.referenceNode),
bsw/jbe@1309:           'fontSize': wysihtml.dom.getStyle("font-size").from(options.referenceNode)
bsw/jbe@1309:         },
bsw/jbe@1309:         rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
bsw/jbe@1309:         newHtml;
bsw/jbe@1309: 
bsw/jbe@1309:     newHtml = wysihtml.dom.parse(html, {
bsw/jbe@1309:       "rules": rules,
bsw/jbe@1309:       "cleanUp": true, //  elements, empty or without attributes, should be removed/replaced with their content
bsw/jbe@1309:       "context": options.referenceNode.ownerDocument,
bsw/jbe@1309:       "uneditableClass": options.uneditableClass,
bsw/jbe@1309:       "clearInternals" : true, // don't paste temprorary selection and other markings
bsw/jbe@1309:       "unjoinNbsps" : true
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     return newHtml;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
bsw/jbe@1309:  * @exaple
bsw/jbe@1309:  *    wysihtml.quirks.ensureProperClearing(myContentEditableElement);
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.quirks.ensureProperClearing = (function() {
bsw/jbe@1309:   var clearIfNecessary = function() {
bsw/jbe@1309:     var element = this;
bsw/jbe@1309:     setTimeout(function() {
bsw/jbe@1309:       var innerHTML = element.innerHTML.toLowerCase();
bsw/jbe@1309:       if (innerHTML == " 
" ||
bsw/jbe@1309:           innerHTML == " 
 
") {
bsw/jbe@1309:         element.innerHTML = "";
bsw/jbe@1309:       }
bsw/jbe@1309:     }, 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   return function(composer) {
bsw/jbe@1309:     wysihtml.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
bsw/jbe@1309:   };
bsw/jbe@1309: })();
bsw/jbe@1309: 
bsw/jbe@1309: // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
bsw/jbe@1309: //
bsw/jbe@1309: // In Firefox this:
bsw/jbe@1309: //      var d = document.createElement("div");
bsw/jbe@1309: //      d.innerHTML ='';
bsw/jbe@1309: //      d.innerHTML;
bsw/jbe@1309: // will result in:
bsw/jbe@1309: //      
bsw/jbe@1309: // which is wrong
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var TILDE_ESCAPED = "%7E";
bsw/jbe@1309:   wysihtml.quirks.getCorrectInnerHTML = function(element) {
bsw/jbe@1309:     var innerHTML = element.innerHTML;
bsw/jbe@1309:     if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
bsw/jbe@1309:       return innerHTML;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
bsw/jbe@1309:         url,
bsw/jbe@1309:         urlToSearch,
bsw/jbe@1309:         length,
bsw/jbe@1309:         i;
bsw/jbe@1309:     for (i=0, length=elementsWithTilde.length; i end_depth) {
bsw/jbe@1309:             range.setStartBefore(range.startContainer);
bsw/jbe@1309:             start_depth = getDepth(common, range.startContainer);
bsw/jbe@1309:         }
bsw/jbe@1309:         else {
bsw/jbe@1309:             range.setEndAfter(range.endContainer);
bsw/jbe@1309:             end_depth = getDepth(common, range.endContainer);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.Selection = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.Selection.prototype */ {
bsw/jbe@1309:     constructor: function(editor, contain, unselectableClass) {
bsw/jbe@1309:       // Make sure that our external range library is initialized
bsw/jbe@1309:       rangy.init();
bsw/jbe@1309: 
bsw/jbe@1309:       this.editor   = editor;
bsw/jbe@1309:       this.composer = editor.composer;
bsw/jbe@1309:       this.doc      = this.composer.doc;
bsw/jbe@1309:       this.win      = this.composer.win;
bsw/jbe@1309:       this.contain = contain;
bsw/jbe@1309:       this.unselectableClass = unselectableClass || false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Get the current selection as a bookmark to be able to later restore it
bsw/jbe@1309:      *
bsw/jbe@1309:      * @return {Object} An object that represents the current selection
bsw/jbe@1309:      */
bsw/jbe@1309:     getBookmark: function() {
bsw/jbe@1309:       var range = this.getRange();
bsw/jbe@1309:       return range && range.cloneRange();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Restore a selection retrieved via wysihtml.Selection.prototype.getBookmark
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Object} bookmark An object that represents the current selection
bsw/jbe@1309:      */
bsw/jbe@1309:     setBookmark: function(bookmark) {
bsw/jbe@1309:       if (!bookmark) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.setSelection(bookmark);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Set the caret in front of the given node
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Object} node The element or text node where to position the caret in front of
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    selection.setBefore(myElement);
bsw/jbe@1309:      */
bsw/jbe@1309:     setBefore: function(node) {
bsw/jbe@1309:       var range = rangy.createRange(this.doc);
bsw/jbe@1309:       range.setStartBefore(node);
bsw/jbe@1309:       range.setEndBefore(node);
bsw/jbe@1309:       return this.setSelection(range);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail.
bsw/jbe@1309:     // Webkit has an issue with placing caret into places where there are no textnodes near by.
bsw/jbe@1309:     createTemporaryCaretSpaceAfter: function (node) {
bsw/jbe@1309:       var caretPlaceholder = this.doc.createElement('span'),
bsw/jbe@1309:           caretPlaceholderText = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE),
bsw/jbe@1309:           placeholderRemover = (function(event) {
bsw/jbe@1309:             // Self-destructs the caret and keeps the text inserted into it by user
bsw/jbe@1309:             var lastChild;
bsw/jbe@1309: 
bsw/jbe@1309:             this.contain.removeEventListener('mouseup', placeholderRemover);
bsw/jbe@1309:             this.contain.removeEventListener('keydown', keyDownHandler);
bsw/jbe@1309:             this.contain.removeEventListener('touchstart', placeholderRemover);
bsw/jbe@1309:             this.contain.removeEventListener('focus', placeholderRemover);
bsw/jbe@1309:             this.contain.removeEventListener('blur', placeholderRemover);
bsw/jbe@1309:             this.contain.removeEventListener('paste', delayedPlaceholderRemover);
bsw/jbe@1309:             this.contain.removeEventListener('drop', delayedPlaceholderRemover);
bsw/jbe@1309:             this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover);
bsw/jbe@1309: 
bsw/jbe@1309:             // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack
bsw/jbe@1309:             // Otherwise the wrapper can just be removed
bsw/jbe@1309:             if (caretPlaceholder && caretPlaceholder.parentNode) {
bsw/jbe@1309:               caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309:               if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) {
bsw/jbe@1309:                 lastChild = caretPlaceholder.lastChild;
bsw/jbe@1309:                 wysihtml.dom.unwrap(caretPlaceholder);
bsw/jbe@1309:                 this.setAfter(lastChild);
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
bsw/jbe@1309:               }
bsw/jbe@1309: 
bsw/jbe@1309:             }
bsw/jbe@1309:           }).bind(this),
bsw/jbe@1309:           delayedPlaceholderRemover = function (event) {
bsw/jbe@1309:             if (caretPlaceholder && caretPlaceholder.parentNode) {
bsw/jbe@1309:               setTimeout(placeholderRemover, 0);
bsw/jbe@1309:             }
bsw/jbe@1309:           },
bsw/jbe@1309:           keyDownHandler = function(event) {
bsw/jbe@1309:             if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) {
bsw/jbe@1309:               placeholderRemover();
bsw/jbe@1309:             }
bsw/jbe@1309:           };
bsw/jbe@1309: 
bsw/jbe@1309:       caretPlaceholder.className = '_wysihtml-temp-caret-fix';
bsw/jbe@1309:       caretPlaceholder.style.position = 'absolute';
bsw/jbe@1309:       caretPlaceholder.style.display = 'block';
bsw/jbe@1309:       caretPlaceholder.style.minWidth = '1px';
bsw/jbe@1309:       caretPlaceholder.style.zIndex = '99999';
bsw/jbe@1309:       caretPlaceholder.appendChild(caretPlaceholderText);
bsw/jbe@1309: 
bsw/jbe@1309:       node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
bsw/jbe@1309:       this.setBefore(caretPlaceholderText);
bsw/jbe@1309: 
bsw/jbe@1309:       // Remove the caret fix on any of the following events (some are delayed as content change happens after event)
bsw/jbe@1309:       this.contain.addEventListener('mouseup', placeholderRemover);
bsw/jbe@1309:       this.contain.addEventListener('keydown', keyDownHandler);
bsw/jbe@1309:       this.contain.addEventListener('touchstart', placeholderRemover);
bsw/jbe@1309:       this.contain.addEventListener('focus', placeholderRemover);
bsw/jbe@1309:       this.contain.addEventListener('blur', placeholderRemover);
bsw/jbe@1309:       this.contain.addEventListener('paste', delayedPlaceholderRemover);
bsw/jbe@1309:       this.contain.addEventListener('drop', delayedPlaceholderRemover);
bsw/jbe@1309:       this.contain.addEventListener('beforepaste', delayedPlaceholderRemover);
bsw/jbe@1309: 
bsw/jbe@1309:       return caretPlaceholder;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Set the caret after the given node
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Object} node The element or text node where to position the caret in front of
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    selection.setBefore(myElement);
bsw/jbe@1309:      * callback is an optional parameter accepting a function to execute when selection ahs been set
bsw/jbe@1309:      */
bsw/jbe@1309:     setAfter: function(node, notVisual, callback) {
bsw/jbe@1309:       var win = this.win,
bsw/jbe@1309:           range = rangy.createRange(this.doc),
bsw/jbe@1309:           fixWebkitSelection = function() {
bsw/jbe@1309:             // Webkit fails to add selection if there are no textnodes in that region
bsw/jbe@1309:             // (like an uneditable container at the end of content).
bsw/jbe@1309:             var parent = node.parentNode,
bsw/jbe@1309:                 lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null;
bsw/jbe@1309: 
bsw/jbe@1309:             if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) {
bsw/jbe@1309:               if (notVisual) {
bsw/jbe@1309:                 // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation
bsw/jbe@1309:                 // and remove itself in call stack end instead on user interaction
bsw/jbe@1309:                 var caretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309:                 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
bsw/jbe@1309:                 this.selectNode(caretPlaceholder);
bsw/jbe@1309:                 setTimeout(function() {
bsw/jbe@1309:                   if (caretPlaceholder && caretPlaceholder.parentNode) {
bsw/jbe@1309:                     caretPlaceholder.parentNode.removeChild(caretPlaceholder);
bsw/jbe@1309:                   }
bsw/jbe@1309:                 }, 0);
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 this.createTemporaryCaretSpaceAfter(node);
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:           }.bind(this),
bsw/jbe@1309:           sel;
bsw/jbe@1309: 
bsw/jbe@1309:       range.setStartAfter(node);
bsw/jbe@1309:       range.setEndAfter(node);
bsw/jbe@1309: 
bsw/jbe@1309:       // In IE contenteditable must be focused before we can set selection
bsw/jbe@1309:       // thus setting the focus if activeElement is not this composer
bsw/jbe@1309:       if (!document.activeElement || document.activeElement !== this.composer.element) {
bsw/jbe@1309:         var scrollPos = this.composer.getScrollPos();
bsw/jbe@1309:         this.composer.element.focus();
bsw/jbe@1309:         this.composer.setScrollPos(scrollPos);
bsw/jbe@1309:         setTimeout(function() {
bsw/jbe@1309:           sel = this.setSelection(range);
bsw/jbe@1309:           fixWebkitSelection();
bsw/jbe@1309:           if (callback) {
bsw/jbe@1309:             callback(sel);
bsw/jbe@1309:           }
bsw/jbe@1309:         }.bind(this), 0);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         sel = this.setSelection(range);
bsw/jbe@1309:         fixWebkitSelection();
bsw/jbe@1309:         if (callback) {
bsw/jbe@1309:           callback(sel);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Ability to select/mark nodes
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Element} node The node/element to select
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    selection.selectNode(document.getElementById("my-image"));
bsw/jbe@1309:      */
bsw/jbe@1309:     selectNode: function(node, avoidInvisibleSpace) {
bsw/jbe@1309:       var range           = rangy.createRange(this.doc),
bsw/jbe@1309:           isElement       = node.nodeType === wysihtml.ELEMENT_NODE,
bsw/jbe@1309:           canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
bsw/jbe@1309:           content         = isElement ? node.innerHTML : node.data,
bsw/jbe@1309:           isEmpty         = (content === "" || content === wysihtml.INVISIBLE_SPACE),
bsw/jbe@1309:           displayStyle    = dom.getStyle("display").from(node),
bsw/jbe@1309:           isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
bsw/jbe@1309: 
bsw/jbe@1309:       if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
bsw/jbe@1309:         // Make sure that caret is visible in node by inserting a zero width no breaking space
bsw/jbe@1309:         try { node.innerHTML = wysihtml.INVISIBLE_SPACE; } catch(e) {}
bsw/jbe@1309:       }
bsw/jbe@1309:       if (canHaveHTML) {
bsw/jbe@1309:         range.selectNodeContents(node);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         range.selectNode(node);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (canHaveHTML && isEmpty && isElement) {
bsw/jbe@1309:         range.collapse(isBlockElement);
bsw/jbe@1309:       } else if (canHaveHTML && isEmpty) {
bsw/jbe@1309:         range.setStartAfter(node);
bsw/jbe@1309:         range.setEndAfter(node);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.setSelection(range);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Get the node which contains the selection
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
bsw/jbe@1309:      * @return {Object} The node that contains the caret
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    var nodeThatContainsCaret = selection.getSelectedNode();
bsw/jbe@1309:      */
bsw/jbe@1309:     getSelectedNode: function(controlRange) {
bsw/jbe@1309:       var selection,
bsw/jbe@1309:           range;
bsw/jbe@1309: 
bsw/jbe@1309:       if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
bsw/jbe@1309:         range = this.doc.selection.createRange();
bsw/jbe@1309:         if (range && range.length) {
bsw/jbe@1309:           return range.item(0);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       selection = this.getSelection(this.doc);
bsw/jbe@1309:       if (selection.focusNode === selection.anchorNode) {
bsw/jbe@1309:         return selection.focusNode;
bsw/jbe@1309:       } else {
bsw/jbe@1309:         range = this.getRange(this.doc);
bsw/jbe@1309:         return range ? range.commonAncestorContainer : this.doc.body;
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     fixSelBorders: function() {
bsw/jbe@1309:       var range = this.getRange();
bsw/jbe@1309:       expandRangeToSurround(range);
bsw/jbe@1309:       this.setSelection(range);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getSelectedOwnNodes: function(controlRange) {
bsw/jbe@1309:       var selection,
bsw/jbe@1309:           ranges = this.getOwnRanges(),
bsw/jbe@1309:           ownNodes = [];
bsw/jbe@1309: 
bsw/jbe@1309:       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309:           ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
bsw/jbe@1309:       }
bsw/jbe@1309:       return ownNodes;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     findNodesInSelection: function(nodeTypes) {
bsw/jbe@1309:       var ranges = this.getOwnRanges(),
bsw/jbe@1309:           nodes = [], curNodes;
bsw/jbe@1309:       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309:         curNodes = ranges[i].getNodes([1], function(node) {
bsw/jbe@1309:             return wysihtml.lang.array(nodeTypes).contains(node.nodeName);
bsw/jbe@1309:         });
bsw/jbe@1309:         nodes = nodes.concat(curNodes);
bsw/jbe@1309:       }
bsw/jbe@1309:       return nodes;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     filterElements: function(filter) {
bsw/jbe@1309:       var ranges = this.getOwnRanges(),
bsw/jbe@1309:           nodes = [], curNodes;
bsw/jbe@1309: 
bsw/jbe@1309:       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309:         curNodes = ranges[i].getNodes([1], function(element){
bsw/jbe@1309:           return filter(element, ranges[i]);
bsw/jbe@1309:         });
bsw/jbe@1309:         nodes = nodes.concat(curNodes);
bsw/jbe@1309:       }
bsw/jbe@1309:       return nodes;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     containsUneditable: function() {
bsw/jbe@1309:       var uneditables = this.getOwnUneditables(),
bsw/jbe@1309:           selection = this.getSelection();
bsw/jbe@1309: 
bsw/jbe@1309:       for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
bsw/jbe@1309:         if (selection.containsNode(uneditables[i])) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Deletes selection contents making sure uneditables/unselectables are not partially deleted
bsw/jbe@1309:     // Triggers wysihtml:uneditable:delete custom event on all deleted uneditables if customevents suppoorted
bsw/jbe@1309:     deleteContents: function()  {
bsw/jbe@1309:       var range = this.getRange();
bsw/jbe@1309:       this.deleteRangeContents(range);
bsw/jbe@1309:       this.setSelection(range);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Makes sure all uneditable sare notified before deleting contents
bsw/jbe@1309:     deleteRangeContents: function (range) {
bsw/jbe@1309:       var startParent, endParent, uneditables, ev;
bsw/jbe@1309: 
bsw/jbe@1309:       if (this.unselectableClass) {
bsw/jbe@1309:         if ((startParent = wysihtml.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
bsw/jbe@1309:           range.setStartBefore(startParent);
bsw/jbe@1309:         }
bsw/jbe@1309:         if ((endParent = wysihtml.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
bsw/jbe@1309:           range.setEndAfter(endParent);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         // If customevents present notify uneditable elements of being deleted
bsw/jbe@1309:         uneditables = range.getNodes([1], (function (node) {
bsw/jbe@1309:           return wysihtml.dom.hasClass(node, this.unselectableClass);
bsw/jbe@1309:         }).bind(this));
bsw/jbe@1309:         for (var i = uneditables.length; i--;) {
bsw/jbe@1309:           try {
bsw/jbe@1309:             ev = new CustomEvent("wysihtml:uneditable:delete");
bsw/jbe@1309:             uneditables[i].dispatchEvent(ev);
bsw/jbe@1309:           } catch (err) {}
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       range.deleteContents();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getCaretNode: function () {
bsw/jbe@1309:       var selection = this.getSelection();
bsw/jbe@1309:       return (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getPreviousNode: function(node, ignoreEmpty) {
bsw/jbe@1309:       var displayStyle;
bsw/jbe@1309:       if (!node) {
bsw/jbe@1309:         var selection = this.getSelection();
bsw/jbe@1309:         node = (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (node === this.contain) {
bsw/jbe@1309:           return false;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var ret = node.previousSibling,
bsw/jbe@1309:           parent;
bsw/jbe@1309: 
bsw/jbe@1309:       if (ret === this.contain) {
bsw/jbe@1309:           return false;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
bsw/jbe@1309:          // do not count comments and other node types
bsw/jbe@1309:          ret = this.getPreviousNode(ret, ignoreEmpty);
bsw/jbe@1309:       } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
bsw/jbe@1309:         // do not count empty textnodes as previous nodes
bsw/jbe@1309:         ret = this.getPreviousNode(ret, ignoreEmpty);
bsw/jbe@1309:       } else if (ignoreEmpty && ret && ret.nodeType === 1) {
bsw/jbe@1309:         // Do not count empty nodes if param set.
bsw/jbe@1309:         // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like
bsw/jbe@1309:         displayStyle = wysihtml.dom.getStyle("display").from(ret);
bsw/jbe@1309:         if (
bsw/jbe@1309:             !wysihtml.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) &&
bsw/jbe@1309:             !wysihtml.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) &&
bsw/jbe@1309:             (/^[\s]*$/).test(ret.innerHTML)
bsw/jbe@1309:           ) {
bsw/jbe@1309:             ret = this.getPreviousNode(ret, ignoreEmpty);
bsw/jbe@1309:           }
bsw/jbe@1309:       } else if (!ret && node !== this.contain) {
bsw/jbe@1309:         parent = node.parentNode;
bsw/jbe@1309:         if (parent !== this.contain) {
bsw/jbe@1309:             ret = this.getPreviousNode(parent, ignoreEmpty);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return (ret !== this.contain) ? ret : false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Gather info about caret location (caret node, previous and next node)
bsw/jbe@1309:     getNodesNearCaret: function() {
bsw/jbe@1309:       if (!this.isCollapsed()) {
bsw/jbe@1309:         throw "Selection must be caret when using selection.getNodesNearCaret()";
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var r = this.getOwnRanges(),
bsw/jbe@1309:           caretNode, prevNode, nextNode, offset;
bsw/jbe@1309: 
bsw/jbe@1309:       if (r && r.length > 0) {
bsw/jbe@1309:         if (r[0].startContainer.nodeType === 1) {
bsw/jbe@1309:           caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1];
bsw/jbe@1309:           if (!caretNode && r[0].startOffset === 0) {
bsw/jbe@1309:             // Is first position before all nodes
bsw/jbe@1309:             nextNode = r[0].startContainer.childNodes[0];
bsw/jbe@1309:           } else if (caretNode) {
bsw/jbe@1309:             prevNode = caretNode.previousSibling;
bsw/jbe@1309:             nextNode = caretNode.nextSibling;
bsw/jbe@1309:           }
bsw/jbe@1309:         } else {
bsw/jbe@1309:           if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) {
bsw/jbe@1309:             caretNode = r[0].startContainer.previousSibling;
bsw/jbe@1309:             if (caretNode.nodeType === 3) {
bsw/jbe@1309:               offset = caretNode.data.length;
bsw/jbe@1309:             }
bsw/jbe@1309:           } else {
bsw/jbe@1309:             caretNode = r[0].startContainer;
bsw/jbe@1309:             offset = r[0].startOffset;
bsw/jbe@1309:           }
bsw/jbe@1309:           prevNode = caretNode.previousSibling;
bsw/jbe@1309:           nextNode = caretNode.nextSibling;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return {
bsw/jbe@1309:           "caretNode": caretNode,
bsw/jbe@1309:           "prevNode": prevNode,
bsw/jbe@1309:           "nextNode": nextNode,
bsw/jbe@1309:           "textOffset": offset
bsw/jbe@1309:         };
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return null;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getSelectionParentsByTag: function(tagName) {
bsw/jbe@1309:       var nodes = this.getSelectedOwnNodes(),
bsw/jbe@1309:           curEl, parents = [];
bsw/jbe@1309: 
bsw/jbe@1309:       for (var i = 0, maxi = nodes.length; i < maxi; i++) {
bsw/jbe@1309:         curEl = (nodes[i].nodeName &&  nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain);
bsw/jbe@1309:         if (curEl) {
bsw/jbe@1309:           parents.push(curEl);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return (parents.length) ? parents : null;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getRangeToNodeEnd: function() {
bsw/jbe@1309:       if (this.isCollapsed()) {
bsw/jbe@1309:         var range = this.getRange(),
bsw/jbe@1309:             sNode, pos, lastR;
bsw/jbe@1309:         if (range) {
bsw/jbe@1309:           sNode = range.startContainer;
bsw/jbe@1309:           pos = range.startOffset;
bsw/jbe@1309:           lastR = rangy.createRange(this.doc);
bsw/jbe@1309: 
bsw/jbe@1309:           lastR.selectNodeContents(sNode);
bsw/jbe@1309:           lastR.setStart(sNode, pos);
bsw/jbe@1309:           return lastR;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getRangeToNodeBeginning: function() {
bsw/jbe@1309:       if (this.isCollapsed()) {
bsw/jbe@1309:         var range = this.getRange(),
bsw/jbe@1309:             sNode = range.startContainer,
bsw/jbe@1309:             pos = range.startOffset,
bsw/jbe@1309:             lastR = rangy.createRange(this.doc);
bsw/jbe@1309: 
bsw/jbe@1309:         lastR.selectNodeContents(sNode);
bsw/jbe@1309:         lastR.setEnd(sNode, pos);
bsw/jbe@1309:         return lastR;
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // This function returns if caret is last in a node (no textual visible content follows)
bsw/jbe@1309:     caretIsInTheEndOfNode: function(ignoreIfSpaceIsBeforeCaret) {
bsw/jbe@1309:       var r = rangy.createRange(this.doc),
bsw/jbe@1309:           s = this.getSelection(),
bsw/jbe@1309:           rangeToNodeEnd = this.getRangeToNodeEnd(),
bsw/jbe@1309:           endc, endtxt, beginc, begintxt;
bsw/jbe@1309: 
bsw/jbe@1309:       if (rangeToNodeEnd) {
bsw/jbe@1309:         endc = rangeToNodeEnd.cloneContents();
bsw/jbe@1309:         endtxt = endc.textContent;
bsw/jbe@1309: 
bsw/jbe@1309:         if ((/^\s*$/).test(endtxt)) {
bsw/jbe@1309:           if (ignoreIfSpaceIsBeforeCaret) {
bsw/jbe@1309:             beginc = this.getRangeToNodeBeginning().cloneContents();
bsw/jbe@1309:             begintxt = beginc.textContent;
bsw/jbe@1309:             return !(/[\u00A0 ][\s\uFEFF]*$/).test(begintxt);
bsw/jbe@1309:           } else {
bsw/jbe@1309:             return true;
bsw/jbe@1309:           }
bsw/jbe@1309:         } else {
bsw/jbe@1309:           return false;
bsw/jbe@1309:         }
bsw/jbe@1309:       } else {
bsw/jbe@1309:         return false;
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     caretIsFirstInSelection: function(includeLineBreaks) {
bsw/jbe@1309:       var r = rangy.createRange(this.doc),
bsw/jbe@1309:           s = this.getSelection(),
bsw/jbe@1309:           range = this.getRange(),
bsw/jbe@1309:           startNode = getRangeNode(range.startContainer, range.startOffset);
bsw/jbe@1309: 
bsw/jbe@1309:       if (startNode) {
bsw/jbe@1309:         if (startNode.nodeType === wysihtml.TEXT_NODE) {
bsw/jbe@1309:           if (!startNode.parentNode) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309:           if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml.dom.domNode(startNode.previousSibling).is.block())) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309:           var ws = this.win.getComputedStyle(startNode.parentNode).whiteSpace;
bsw/jbe@1309:           return (ws === "pre" || ws === "pre-wrap") ? range.startOffset === 0 : (/^\s*$/).test(startNode.data.substr(0,range.startOffset));
bsw/jbe@1309:         } else if (includeLineBreaks && wysihtml.dom.domNode(startNode).is.lineBreak()) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           r.selectNodeContents(this.getRange().commonAncestorContainer);
bsw/jbe@1309:           r.collapse(true);
bsw/jbe@1309:           return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     caretIsInTheBeginnig: function(ofNode) {
bsw/jbe@1309:         var selection = this.getSelection(),
bsw/jbe@1309:             node = selection.anchorNode,
bsw/jbe@1309:             offset = selection.anchorOffset;
bsw/jbe@1309:         if (ofNode && node) {
bsw/jbe@1309:           return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml.dom.getParentElement(node.parentNode, { query: ofNode }, 1)));
bsw/jbe@1309:         } else if (node) {
bsw/jbe@1309:           return (offset === 0 && !this.getPreviousNode(node, true));
bsw/jbe@1309:         }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Returns object describing node/text before selection
bsw/jbe@1309:     // If includePrevLeaves is true returns  also previous last leaf child if selection is in the beginning of current node
bsw/jbe@1309:     getBeforeSelection: function(includePrevLeaves) {
bsw/jbe@1309:       var sel = this.getSelection(),
bsw/jbe@1309:           startNode = (sel.isBackwards()) ? sel.focusNode : sel.anchorNode,
bsw/jbe@1309:           startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset,
bsw/jbe@1309:           rng = this.createRange(), endNode, inTmpCaret;
bsw/jbe@1309: 
bsw/jbe@1309:       // If start is textnode and all is whitespace before caret. Set start offset to 0
bsw/jbe@1309:       if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) {
bsw/jbe@1309:         startOffset = 0;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Escape temproray helper nodes if selection in them
bsw/jbe@1309:       inTmpCaret = wysihtml.dom.getParentElement(startNode, { query: '._wysihtml-temp-caret-fix' }, 1);
bsw/jbe@1309:       if (inTmpCaret) {
bsw/jbe@1309:         startNode = inTmpCaret.parentNode;
bsw/jbe@1309:         startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (startNode) {
bsw/jbe@1309:         if (startOffset > 0) {
bsw/jbe@1309:           if (startNode.nodeType === 3) {
bsw/jbe@1309:             rng.setStart(startNode, 0);
bsw/jbe@1309:             rng.setEnd(startNode, startOffset);
bsw/jbe@1309:             return {
bsw/jbe@1309:               type: "text",
bsw/jbe@1309:               range: rng,
bsw/jbe@1309:               offset : startOffset,
bsw/jbe@1309:               node: startNode
bsw/jbe@1309:             };
bsw/jbe@1309:           } else {
bsw/jbe@1309:             rng.setStartBefore(startNode.childNodes[0]);
bsw/jbe@1309:             endNode = startNode.childNodes[startOffset - 1];
bsw/jbe@1309:             rng.setEndAfter(endNode);
bsw/jbe@1309:             return {
bsw/jbe@1309:               type: "element",
bsw/jbe@1309:               range: rng,
bsw/jbe@1309:               offset : startOffset,
bsw/jbe@1309:               node: endNode
bsw/jbe@1309:             };
bsw/jbe@1309:           }
bsw/jbe@1309:         } else {
bsw/jbe@1309:           rng.setStartAndEnd(startNode, 0);
bsw/jbe@1309: 
bsw/jbe@1309:           if (includePrevLeaves) {
bsw/jbe@1309:             var prevNode = this.getPreviousNode(startNode, true),
bsw/jbe@1309:                 prevLeaf = null;
bsw/jbe@1309: 
bsw/jbe@1309:             if(prevNode) {
bsw/jbe@1309:               if (prevNode.nodeType === 1 && wysihtml.dom.hasClass(prevNode, this.unselectableClass)) {
bsw/jbe@1309:                 prevLeaf = prevNode;
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 prevLeaf = wysihtml.dom.domNode(prevNode).lastLeafNode();
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (prevLeaf) {
bsw/jbe@1309:               return {
bsw/jbe@1309:                 type: "leafnode",
bsw/jbe@1309:                 range: rng,
bsw/jbe@1309:                 offset : startOffset,
bsw/jbe@1309:                 node: prevLeaf
bsw/jbe@1309:               };
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           return {
bsw/jbe@1309:             type: "none",
bsw/jbe@1309:             range: rng,
bsw/jbe@1309:             offset : startOffset,
bsw/jbe@1309:             node: startNode
bsw/jbe@1309:           };
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return null;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // TODO: Figure out a method from following 2 that would work universally
bsw/jbe@1309:     executeAndRestoreRangy: function(method, restoreScrollPosition) {
bsw/jbe@1309:       var sel = rangy.saveSelection(this.win);
bsw/jbe@1309:       if (!sel) {
bsw/jbe@1309:         method();
bsw/jbe@1309:       } else {
bsw/jbe@1309:         try {
bsw/jbe@1309:           method();
bsw/jbe@1309:         } catch(e) {
bsw/jbe@1309:           setTimeout(function() { throw e; }, 0);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       rangy.restoreSelection(sel);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
bsw/jbe@1309:     executeAndRestore: function(method, restoreScrollPosition) {
bsw/jbe@1309:       var body                  = this.doc.body,
bsw/jbe@1309:           oldScrollTop          = restoreScrollPosition && body.scrollTop,
bsw/jbe@1309:           oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
bsw/jbe@1309:           className             = "_wysihtml-temp-placeholder",
bsw/jbe@1309:           placeholderHtml       = '' + wysihtml.INVISIBLE_SPACE + '',
bsw/jbe@1309:           range                 = this.getRange(true),
bsw/jbe@1309:           caretPlaceholder,
bsw/jbe@1309:           newCaretPlaceholder,
bsw/jbe@1309:           nextSibling, prevSibling,
bsw/jbe@1309:           node, node2, range2,
bsw/jbe@1309:           newRange;
bsw/jbe@1309: 
bsw/jbe@1309:       // Nothing selected, execute and say goodbye
bsw/jbe@1309:       if (!range) {
bsw/jbe@1309:         method(body, body);
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (!range.collapsed) {
bsw/jbe@1309:         range2 = range.cloneRange();
bsw/jbe@1309:         node2 = range2.createContextualFragment(placeholderHtml);
bsw/jbe@1309:         range2.collapse(false);
bsw/jbe@1309:         range2.insertNode(node2);
bsw/jbe@1309:         range2.detach();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       node = range.createContextualFragment(placeholderHtml);
bsw/jbe@1309:       range.insertNode(node);
bsw/jbe@1309: 
bsw/jbe@1309:       if (node2) {
bsw/jbe@1309:         caretPlaceholder = this.contain.querySelectorAll("." + className);
bsw/jbe@1309:         range.setStartBefore(caretPlaceholder[0]);
bsw/jbe@1309:         range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
bsw/jbe@1309:       }
bsw/jbe@1309:       this.setSelection(range);
bsw/jbe@1309: 
bsw/jbe@1309:       // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
bsw/jbe@1309:       try {
bsw/jbe@1309:         method(range.startContainer, range.endContainer);
bsw/jbe@1309:       } catch(e) {
bsw/jbe@1309:         setTimeout(function() { throw e; }, 0);
bsw/jbe@1309:       }
bsw/jbe@1309:       caretPlaceholder = this.contain.querySelectorAll("." + className);
bsw/jbe@1309:       if (caretPlaceholder && caretPlaceholder.length) {
bsw/jbe@1309:         newRange = rangy.createRange(this.doc);
bsw/jbe@1309:         nextSibling = caretPlaceholder[0].nextSibling;
bsw/jbe@1309:         if (caretPlaceholder.length > 1) {
bsw/jbe@1309:           prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
bsw/jbe@1309:         }
bsw/jbe@1309:         if (prevSibling && nextSibling) {
bsw/jbe@1309:           newRange.setStartBefore(nextSibling);
bsw/jbe@1309:           newRange.setEndAfter(prevSibling);
bsw/jbe@1309:         } else {
bsw/jbe@1309:           newCaretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309:           dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
bsw/jbe@1309:           newRange.setStartBefore(newCaretPlaceholder);
bsw/jbe@1309:           newRange.setEndAfter(newCaretPlaceholder);
bsw/jbe@1309:         }
bsw/jbe@1309:         this.setSelection(newRange);
bsw/jbe@1309:         for (var i = caretPlaceholder.length; i--;) {
bsw/jbe@1309:           caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       } else {
bsw/jbe@1309:         // fallback for when all hell breaks loose
bsw/jbe@1309:         this.contain.focus();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (restoreScrollPosition) {
bsw/jbe@1309:         body.scrollTop  = oldScrollTop;
bsw/jbe@1309:         body.scrollLeft = oldScrollLeft;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
bsw/jbe@1309:       try {
bsw/jbe@1309:         caretPlaceholder.parentNode.removeChild(caretPlaceholder);
bsw/jbe@1309:       } catch(e2) {}
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     set: function(node, offset) {
bsw/jbe@1309:       var newRange = rangy.createRange(this.doc);
bsw/jbe@1309:       newRange.setStart(node, offset || 0);
bsw/jbe@1309:       this.setSelection(newRange);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Insert html at the caret or selection position and move the cursor after the inserted html
bsw/jbe@1309:      * Replaces selection content if present
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {String} html HTML string to insert
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    selection.insertHTML("foobar
");
bsw/jbe@1309:      */
bsw/jbe@1309:     insertHTML: function(html) {
bsw/jbe@1309:       var range     = this.getRange(),
bsw/jbe@1309:           node = this.doc.createElement('DIV'),
bsw/jbe@1309:           fragment = this.doc.createDocumentFragment(),
bsw/jbe@1309:           lastChild, lastEditorElement;
bsw/jbe@1309: 
bsw/jbe@1309:       if (range) {
bsw/jbe@1309:         range.deleteContents();
bsw/jbe@1309:         node.innerHTML = html;
bsw/jbe@1309:         lastChild = node.lastChild;
bsw/jbe@1309: 
bsw/jbe@1309:         while (node.firstChild) {
bsw/jbe@1309:           fragment.appendChild(node.firstChild);
bsw/jbe@1309:         }
bsw/jbe@1309:         range.insertNode(fragment);
bsw/jbe@1309: 
bsw/jbe@1309:         lastEditorElement = this.contain.lastChild;
bsw/jbe@1309:         while (lastEditorElement && lastEditorElement.nodeType === 3 && lastEditorElement.previousSibling && (/^\s*$/).test(lastEditorElement.data)) {
bsw/jbe@1309:           lastEditorElement = lastEditorElement.previousSibling;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (lastChild) {
bsw/jbe@1309:           // fixes some pad cases mostly on webkit where last nr is needed
bsw/jbe@1309:           if (lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === 1) {
bsw/jbe@1309:             this.contain.appendChild(this.doc.createElement('br'));
bsw/jbe@1309:           }
bsw/jbe@1309:           this.setAfter(lastChild);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Insert a node at the caret position and move the cursor behind it
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Object} node HTML string to insert
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    selection.insertNode(document.createTextNode("foobar"));
bsw/jbe@1309:      */
bsw/jbe@1309:     insertNode: function(node) {
bsw/jbe@1309:       var range = this.getRange();
bsw/jbe@1309:       if (range) {
bsw/jbe@1309:         range.deleteContents();
bsw/jbe@1309:         range.insertNode(node);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     canAppendChild: function (node) {
bsw/jbe@1309:       var anchorNode, anchorNodeTagNameLower,
bsw/jbe@1309:           voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"],
bsw/jbe@1309:           range = this.getRange();
bsw/jbe@1309: 
bsw/jbe@1309:       anchorNode = node || range.startContainer;
bsw/jbe@1309: 
bsw/jbe@1309:       if (anchorNode) {
bsw/jbe@1309:         anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return voidElements.indexOf(anchorNodeTagNameLower) === -1;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     splitElementAtCaret: function (element, insertNode) {
bsw/jbe@1309:       var sel = this.getSelection(),
bsw/jbe@1309:           range, contentAfterRangeStart,
bsw/jbe@1309:           firstChild, lastChild, childNodes;
bsw/jbe@1309: 
bsw/jbe@1309:       if (sel.rangeCount > 0) {
bsw/jbe@1309:         range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with
bsw/jbe@1309: 
bsw/jbe@1309:         range.setEndAfter(element); // Place the end of the range after the element
bsw/jbe@1309:         contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment
bsw/jbe@1309: 
bsw/jbe@1309:         childNodes = contentAfterRangeStart.childNodes;
bsw/jbe@1309: 
bsw/jbe@1309:         // Empty elements are cleaned up from extracted content
bsw/jbe@1309:         for (var i = childNodes.length; i --;) {
bsw/jbe@1309:           if (!wysihtml.dom.domNode(childNodes[i]).is.visible()) {
bsw/jbe@1309:             contentAfterRangeStart.removeChild(childNodes[i]);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling);
bsw/jbe@1309: 
bsw/jbe@1309:         if (insertNode) {
bsw/jbe@1309:           firstChild = insertNode.firstChild || insertNode;
bsw/jbe@1309:           lastChild = insertNode.lastChild || insertNode;
bsw/jbe@1309: 
bsw/jbe@1309:           element.parentNode.insertBefore(insertNode, element.nextSibling);
bsw/jbe@1309: 
bsw/jbe@1309:           // Select inserted node contents
bsw/jbe@1309:           if (firstChild && lastChild) {
bsw/jbe@1309:              range.setStartBefore(firstChild);
bsw/jbe@1309:              range.setEndAfter(lastChild);
bsw/jbe@1309:              this.setSelection(range);
bsw/jbe@1309:           }
bsw/jbe@1309:         } else {
bsw/jbe@1309:           range.setStartAfter(element);
bsw/jbe@1309:           range.setEndAfter(element);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (!wysihtml.dom.domNode(element).is.visible()) {
bsw/jbe@1309:           if (wysihtml.dom.getTextContent(element) === '') {
bsw/jbe@1309:             element.parentNode.removeChild(element);
bsw/jbe@1309:           } else {
bsw/jbe@1309:             element.parentNode.replaceChild(this.doc.createTextNode(" "), element);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Wraps current selection with the given node
bsw/jbe@1309:      *
bsw/jbe@1309:      * @param {Object} node The node to surround the selected elements with
bsw/jbe@1309:      */
bsw/jbe@1309:     surround: function(nodeOptions) {
bsw/jbe@1309:       var ranges = this.getOwnRanges(),
bsw/jbe@1309:           node, nodes = [];
bsw/jbe@1309:       if (ranges.length == 0) {
bsw/jbe@1309:         return nodes;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       for (var i = ranges.length; i--;) {
bsw/jbe@1309:         node = this.doc.createElement(nodeOptions.nodeName);
bsw/jbe@1309:         nodes.push(node);
bsw/jbe@1309:         if (nodeOptions.className) {
bsw/jbe@1309:           node.className = nodeOptions.className;
bsw/jbe@1309:         }
bsw/jbe@1309:         if (nodeOptions.cssStyle) {
bsw/jbe@1309:           node.setAttribute('style', nodeOptions.cssStyle);
bsw/jbe@1309:         }
bsw/jbe@1309:         try {
bsw/jbe@1309:           // This only works when the range boundaries are not overlapping other elements
bsw/jbe@1309:           ranges[i].surroundContents(node);
bsw/jbe@1309:           this.selectNode(node);
bsw/jbe@1309:         } catch(e) {
bsw/jbe@1309:           // fallback
bsw/jbe@1309:           node.appendChild(ranges[i].extractContents());
bsw/jbe@1309:           ranges[i].insertNode(node);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return nodes;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Scroll the current caret position into the view
bsw/jbe@1309:      * FIXME: This is a bit hacky, there might be a smarter way of doing this
bsw/jbe@1309:      *
bsw/jbe@1309:      * @example
bsw/jbe@1309:      *    selection.scrollIntoView();
bsw/jbe@1309:      */
bsw/jbe@1309:     scrollIntoView: function() {
bsw/jbe@1309:       var doc           = this.doc,
bsw/jbe@1309:           tolerance     = 5, // px
bsw/jbe@1309:           hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
bsw/jbe@1309:           tempElement   = doc._wysihtmlScrollIntoViewElement = doc._wysihtmlScrollIntoViewElement || (function() {
bsw/jbe@1309:             var element = doc.createElement("span");
bsw/jbe@1309:             // The element needs content in order to be able to calculate it's position properly
bsw/jbe@1309:             element.innerHTML = wysihtml.INVISIBLE_SPACE;
bsw/jbe@1309:             return element;
bsw/jbe@1309:           })(),
bsw/jbe@1309:           offsetTop;
bsw/jbe@1309: 
bsw/jbe@1309:       if (hasScrollBars) {
bsw/jbe@1309:         this.insertNode(tempElement);
bsw/jbe@1309:         offsetTop = _getCumulativeOffsetTop(tempElement);
bsw/jbe@1309:         tempElement.parentNode.removeChild(tempElement);
bsw/jbe@1309:         if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
bsw/jbe@1309:           doc.body.scrollTop = offsetTop;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Select line where the caret is in
bsw/jbe@1309:      */
bsw/jbe@1309:     selectLine: function() {
bsw/jbe@1309:       var r = rangy.createRange();
bsw/jbe@1309:       if (wysihtml.browser.supportsSelectionModify()) {
bsw/jbe@1309:         this._selectLine_W3C();
bsw/jbe@1309:       } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
bsw/jbe@1309:         // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
bsw/jbe@1309:         this._selectLineUniversal();
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     includeRangyRangeHelpers: function() {
bsw/jbe@1309:       var s = this.getSelection(),
bsw/jbe@1309:           r = s.getRangeAt(0),
bsw/jbe@1309:           isHelperNode = function(node) {
bsw/jbe@1309:             return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
bsw/jbe@1309:           },
bsw/jbe@1309:           getNodeLength = function (node) {
bsw/jbe@1309:             if (node.nodeType === 1) {
bsw/jbe@1309:               return node.childNodes && node.childNodes.length || 0;
bsw/jbe@1309:             } else {
bsw/jbe@1309:               return node.data && node.data.length || 0;
bsw/jbe@1309:             }
bsw/jbe@1309:           },
bsw/jbe@1309:           anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
bsw/jbe@1309:           fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
bsw/jbe@1309: 
bsw/jbe@1309:       if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
bsw/jbe@1309:         r.setEndAfter(fnode.nextSibling);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
bsw/jbe@1309:         r.setStartBefore(anode.previousSibling);
bsw/jbe@1309:       }
bsw/jbe@1309:       r.select();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * See https://developer.mozilla.org/en/DOM/Selection/modify
bsw/jbe@1309:      */
bsw/jbe@1309:     _selectLine_W3C: function() {
bsw/jbe@1309:       var selection = this.win.getSelection(),
bsw/jbe@1309:           initialBoundry = [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset];
bsw/jbe@1309: 
bsw/jbe@1309:       selection.modify("move", "left", "lineboundary");
bsw/jbe@1309:       selection.modify("extend", "right", "lineboundary");
bsw/jbe@1309: 
bsw/jbe@1309:       // IF lineboundary extending did not change selection try universal fallback (FF fails sometimes without a reason)
bsw/jbe@1309:       if (selection.anchorNode === initialBoundry[0] &&
bsw/jbe@1309:           selection.anchorOffset === initialBoundry[1] &&
bsw/jbe@1309:           selection.focusNode === initialBoundry[2] &&
bsw/jbe@1309:           selection.focusOffset === initialBoundry[3]
bsw/jbe@1309:       ) {
bsw/jbe@1309:         this._selectLineUniversal();
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.includeRangyRangeHelpers();
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // collapses selection to current line beginning or end
bsw/jbe@1309:     toLineBoundary: function (location, collapse) {
bsw/jbe@1309:       collapse = (typeof collapse === 'undefined') ? false : collapse;
bsw/jbe@1309:       if (wysihtml.browser.supportsSelectionModify()) {
bsw/jbe@1309:         var selection = this.win.getSelection();
bsw/jbe@1309: 
bsw/jbe@1309:         selection.modify("extend", location, "lineboundary");
bsw/jbe@1309:         if (collapse) {
bsw/jbe@1309:           if (location === "left") {
bsw/jbe@1309:             selection.collapseToStart();
bsw/jbe@1309:           } else if (location === "right") {
bsw/jbe@1309:             selection.collapseToEnd();
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getRangeRect: function(r) {
bsw/jbe@1309:       var textNode = this.doc.createTextNode("i"),
bsw/jbe@1309:           testNode = this.doc.createTextNode("i"),
bsw/jbe@1309:           rect, cr;
bsw/jbe@1309: 
bsw/jbe@1309:       /*testNode.style.visibility = "hidden";
bsw/jbe@1309:       testNode.style.width = "0px";
bsw/jbe@1309:       testNode.style.display = "inline-block";
bsw/jbe@1309:       testNode.style.overflow = "hidden";
bsw/jbe@1309:       testNode.appendChild(textNode);*/
bsw/jbe@1309: 
bsw/jbe@1309:       if (r.collapsed) {
bsw/jbe@1309:         r.insertNode(testNode);
bsw/jbe@1309:         r.selectNode(testNode);
bsw/jbe@1309:         rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309:         r.deleteContents();
bsw/jbe@1309: 
bsw/jbe@1309:       } else {
bsw/jbe@1309:         rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return rect;
bsw/jbe@1309: 
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _selectLineUniversal: function() {
bsw/jbe@1309:       var s = this.getSelection(),
bsw/jbe@1309:           r = s.getRangeAt(0),
bsw/jbe@1309:           rect,
bsw/jbe@1309:           startRange, endRange, testRange,
bsw/jbe@1309:           count = 0,
bsw/jbe@1309:           amount, testRect, found,
bsw/jbe@1309:           that = this,
bsw/jbe@1309:           isLineBreakingElement = function(el) {
bsw/jbe@1309:             return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml.lang.array(['BR', 'HR']).contains(el.nodeName));
bsw/jbe@1309:           },
bsw/jbe@1309:           prevNode = function(node) {
bsw/jbe@1309:             var pnode = node;
bsw/jbe@1309:             if (pnode) {
bsw/jbe@1309:               while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
bsw/jbe@1309:                 pnode = pnode.previousSibling;
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:             return pnode;
bsw/jbe@1309:           };
bsw/jbe@1309: 
bsw/jbe@1309:       startRange = r.cloneRange();
bsw/jbe@1309:       endRange = r.cloneRange();
bsw/jbe@1309: 
bsw/jbe@1309:       if (r.collapsed) {
bsw/jbe@1309:         // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
bsw/jbe@1309:         // TODO: figure out a shorter and more readable way
bsw/jbe@1309:         if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
bsw/jbe@1309:           r.moveEnd('character', 1);
bsw/jbe@1309:         } 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) {
bsw/jbe@1309:           r.moveEnd('character', 1);
bsw/jbe@1309:         } else if (
bsw/jbe@1309:           r.startOffset > 0 &&
bsw/jbe@1309:           (
bsw/jbe@1309:             r.startContainer.nodeType === 3 ||
bsw/jbe@1309:             (
bsw/jbe@1309:               r.startContainer.nodeType === 1 &&
bsw/jbe@1309:               !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))
bsw/jbe@1309:             )
bsw/jbe@1309:           )
bsw/jbe@1309:         ) {
bsw/jbe@1309:           r.moveStart('character', -1);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       if (!r.collapsed) {
bsw/jbe@1309:         r.insertNode(this.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Is probably just empty line as can not be expanded
bsw/jbe@1309:       rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309:       // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes;
bsw/jbe@1309:       do {
bsw/jbe@1309:         amount = r.moveStart('character', -1);
bsw/jbe@1309:         testRect =  r.nativeRange.getBoundingClientRect();
bsw/jbe@1309: 
bsw/jbe@1309:         if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
bsw/jbe@1309:           r.moveStart('character', 1);
bsw/jbe@1309:           found = true;
bsw/jbe@1309:         }
bsw/jbe@1309:         count++;
bsw/jbe@1309:       } while (amount !== 0 && !found && count < 2000);
bsw/jbe@1309:       count = 0;
bsw/jbe@1309:       found = false;
bsw/jbe@1309:       rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309: 
bsw/jbe@1309:       if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) {
bsw/jbe@1309:         do {
bsw/jbe@1309:           amount = r.moveEnd('character', 1);
bsw/jbe@1309:           testRect =  r.nativeRange.getBoundingClientRect();
bsw/jbe@1309:           if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
bsw/jbe@1309:             r.moveEnd('character', -1);
bsw/jbe@1309: 
bsw/jbe@1309:             // Fix a IE line end marked by linebreak element although caret is before it
bsw/jbe@1309:             // If causes problems should be changed to be applied only to IE
bsw/jbe@1309:             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) {
bsw/jbe@1309:               if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
bsw/jbe@1309:                 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
bsw/jbe@1309:               } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
bsw/jbe@1309:                 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:             found = true;
bsw/jbe@1309:           }
bsw/jbe@1309:           count++;
bsw/jbe@1309:         } while (amount !== 0 && !found && count < 2000);
bsw/jbe@1309:       }
bsw/jbe@1309:       r.select();
bsw/jbe@1309:       this.includeRangyRangeHelpers();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getText: function() {
bsw/jbe@1309:       var selection = this.getSelection();
bsw/jbe@1309:       return selection ? selection.toString() : "";
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getNodes: function(nodeType, filter) {
bsw/jbe@1309:       var range = this.getRange();
bsw/jbe@1309:       if (range) {
bsw/jbe@1309:         return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         return [];
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Gets all the elements in selection with nodeType
bsw/jbe@1309:     // Ignores the elements not belonging to current editable area
bsw/jbe@1309:     // If filter is defined nodes must pass the filter function with true to be included in list
bsw/jbe@1309:     getOwnNodes: function(nodeType, filter, splitBounds) {
bsw/jbe@1309:       var ranges = this.getOwnRanges(),
bsw/jbe@1309:           nodes = [];
bsw/jbe@1309:       for (var r = 0, rmax = ranges.length; r < rmax; r++) {
bsw/jbe@1309:         if (ranges[r]) {
bsw/jbe@1309:           if (splitBounds) {
bsw/jbe@1309:             ranges[r].splitBoundaries();
bsw/jbe@1309:           }
bsw/jbe@1309:           nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter));
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return nodes;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     fixRangeOverflow: function(range) {
bsw/jbe@1309:       if (this.contain && this.contain.firstChild && range) {
bsw/jbe@1309:         var containment = range.compareNode(this.contain);
bsw/jbe@1309:         if (containment !== 2) {
bsw/jbe@1309:           if (containment === 1) {
bsw/jbe@1309:             range.setStartBefore(this.contain.firstChild);
bsw/jbe@1309:           }
bsw/jbe@1309:           if (containment === 0) {
bsw/jbe@1309:             range.setEndAfter(this.contain.lastChild);
bsw/jbe@1309:           }
bsw/jbe@1309:           if (containment === 3) {
bsw/jbe@1309:             range.setStartBefore(this.contain.firstChild);
bsw/jbe@1309:             range.setEndAfter(this.contain.lastChild);
bsw/jbe@1309:           }
bsw/jbe@1309:         } else if (this._detectInlineRangeProblems(range)) {
bsw/jbe@1309:           var previousElementSibling = range.endContainer.previousElementSibling;
bsw/jbe@1309:           if (previousElementSibling) {
bsw/jbe@1309:             range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _endOffsetForNode: function(node) {
bsw/jbe@1309:       var range = document.createRange();
bsw/jbe@1309:       range.selectNodeContents(node);
bsw/jbe@1309:       return range.endOffset;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _detectInlineRangeProblems: function(range) {
bsw/jbe@1309:       var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
bsw/jbe@1309:       return (
bsw/jbe@1309:         range.endOffset == 0 &&
bsw/jbe@1309:         position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
bsw/jbe@1309:       );
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getRange: function(dontFix) {
bsw/jbe@1309:       var selection = this.getSelection(),
bsw/jbe@1309:           range = selection && selection.rangeCount && selection.getRangeAt(0);
bsw/jbe@1309: 
bsw/jbe@1309:       if (dontFix !== true) {
bsw/jbe@1309:         this.fixRangeOverflow(range);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return range;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getOwnUneditables: function() {
bsw/jbe@1309:       var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
bsw/jbe@1309:           deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
bsw/jbe@1309: 
bsw/jbe@1309:       return wysihtml.lang.array(allUneditables).without(deepUneditables);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Returns an array of ranges that belong only to this editable
bsw/jbe@1309:     // Needed as uneditable block in contenteditabel can split range into pieces
bsw/jbe@1309:     // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
bsw/jbe@1309:     getOwnRanges: function()  {
bsw/jbe@1309:       var ranges = [],
bsw/jbe@1309:           r = this.getRange(),
bsw/jbe@1309:           tmpRanges;
bsw/jbe@1309: 
bsw/jbe@1309:       if (r) { ranges.push(r); }
bsw/jbe@1309: 
bsw/jbe@1309:       if (this.unselectableClass && this.contain && r) {
bsw/jbe@1309:         var uneditables = this.getOwnUneditables(),
bsw/jbe@1309:             tmpRange;
bsw/jbe@1309:         if (uneditables.length > 0) {
bsw/jbe@1309:           for (var i = 0, imax = uneditables.length; i < imax; i++) {
bsw/jbe@1309:             tmpRanges = [];
bsw/jbe@1309:             for (var j = 0, jmax = ranges.length; j < jmax; j++) {
bsw/jbe@1309:               if (ranges[j]) {
bsw/jbe@1309:                 switch (ranges[j].compareNode(uneditables[i])) {
bsw/jbe@1309:                   case 2:
bsw/jbe@1309:                     // all selection inside uneditable. remove
bsw/jbe@1309:                   break;
bsw/jbe@1309:                   case 3:
bsw/jbe@1309:                     //section begins before and ends after uneditable. spilt
bsw/jbe@1309:                     tmpRange = ranges[j].cloneRange();
bsw/jbe@1309:                     tmpRange.setEndBefore(uneditables[i]);
bsw/jbe@1309:                     tmpRanges.push(tmpRange);
bsw/jbe@1309: 
bsw/jbe@1309:                     tmpRange = ranges[j].cloneRange();
bsw/jbe@1309:                     tmpRange.setStartAfter(uneditables[i]);
bsw/jbe@1309:                     tmpRanges.push(tmpRange);
bsw/jbe@1309:                   break;
bsw/jbe@1309:                   default:
bsw/jbe@1309:                     // in all other cases uneditable does not touch selection. dont modify
bsw/jbe@1309:                     tmpRanges.push(ranges[j]);
bsw/jbe@1309:                 }
bsw/jbe@1309:               }
bsw/jbe@1309:               ranges = tmpRanges;
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return ranges;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getSelection: function() {
bsw/jbe@1309:       return rangy.getSelection(this.win);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Sets selection in document to a given range
bsw/jbe@1309:     // Set selection method detects if it fails to set any selection in document and returns null on fail
bsw/jbe@1309:     // (especially needed in webkit where some ranges just can not create selection for no reason)
bsw/jbe@1309:     setSelection: function(range) {
bsw/jbe@1309:       var selection = rangy.getSelection(this.win);
bsw/jbe@1309:       selection.setSingleRange(range);
bsw/jbe@1309:       return (selection && selection.anchorNode && selection.focusNode) ? selection : null;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:     // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area
bsw/jbe@1309:     selectAll: function() {
bsw/jbe@1309:       var range = this.createRange(),
bsw/jbe@1309:           composer = this.composer,
bsw/jbe@1309:           that = this,
bsw/jbe@1309:           blankEndNode = getWebkitSelectionFixNode(this.composer.element),
bsw/jbe@1309:           blankStartNode = getWebkitSelectionFixNode(this.composer.element),
bsw/jbe@1309:           s;
bsw/jbe@1309: 
bsw/jbe@1309:       var doSelect = function() {
bsw/jbe@1309:         range.setStart(composer.element, 0);
bsw/jbe@1309:         range.setEnd(composer.element, composer.element.childNodes.length);
bsw/jbe@1309:         s = that.setSelection(range);
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:       var notSelected = function() {
bsw/jbe@1309:         return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None"));
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       wysihtml.dom.removeInvisibleSpaces(this.composer.element);
bsw/jbe@1309:       doSelect();
bsw/jbe@1309: 
bsw/jbe@1309:       if (this.composer.element.firstChild && notSelected())  {
bsw/jbe@1309:         // Try fixing end
bsw/jbe@1309:         this.composer.element.appendChild(blankEndNode);
bsw/jbe@1309:         doSelect();
bsw/jbe@1309: 
bsw/jbe@1309:         if (notSelected()) {
bsw/jbe@1309:           // Remove end fix
bsw/jbe@1309:           blankEndNode.parentNode.removeChild(blankEndNode);
bsw/jbe@1309: 
bsw/jbe@1309:           // Try fixing beginning
bsw/jbe@1309:           this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild);
bsw/jbe@1309:           doSelect();
bsw/jbe@1309: 
bsw/jbe@1309:           if (notSelected()) {
bsw/jbe@1309:             // Try fixing both
bsw/jbe@1309:             this.composer.element.appendChild(blankEndNode);
bsw/jbe@1309:             doSelect();
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     createRange: function() {
bsw/jbe@1309:       return rangy.createRange(this.doc);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     isCollapsed: function() {
bsw/jbe@1309:         return this.getSelection().isCollapsed;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getHtml: function() {
bsw/jbe@1309:       return this.getSelection().toHtml();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getPlainText: function () {
bsw/jbe@1309:       return this.getSelection().toString();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     isEndToEndInNode: function(nodeNames) {
bsw/jbe@1309:       var range = this.getRange(),
bsw/jbe@1309:           parentElement = range.commonAncestorContainer,
bsw/jbe@1309:           startNode = range.startContainer,
bsw/jbe@1309:           endNode = range.endContainer;
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:         if (parentElement.nodeType === wysihtml.TEXT_NODE) {
bsw/jbe@1309:           parentElement = parentElement.parentNode;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (startNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
bsw/jbe@1309:           return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (endNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
bsw/jbe@1309:           return false;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         while (startNode && startNode !== parentElement) {
bsw/jbe@1309:           if (startNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, startNode)) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309:           if (wysihtml.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309:           startNode = startNode.parentNode;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         while (endNode && endNode !== parentElement) {
bsw/jbe@1309:           if (endNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, endNode)) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309:           if (wysihtml.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
bsw/jbe@1309:             return false;
bsw/jbe@1309:           }
bsw/jbe@1309:           endNode = endNode.parentNode;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return (wysihtml.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     isInThisEditable: function() {
bsw/jbe@1309:       var sel = this.getSelection(),
bsw/jbe@1309:           fnode = sel.focusNode,
bsw/jbe@1309:           anode = sel.anchorNode;
bsw/jbe@1309: 
bsw/jbe@1309:       // In IE node contains will not work for textnodes, thus taking parentNode
bsw/jbe@1309:       if (fnode && fnode.nodeType !== 1) {
bsw/jbe@1309:         fnode = fnode.parentNode;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (anode && anode.nodeType !== 1) {
bsw/jbe@1309:         anode = anode.parentNode;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return anode && fnode &&
bsw/jbe@1309:              (wysihtml.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) &&
bsw/jbe@1309:              (wysihtml.dom.contains(this.composer.element, anode) || this.composer.element === anode);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     deselect: function() {
bsw/jbe@1309:       var sel = this.getSelection();
bsw/jbe@1309:       sel && sel.removeAllRanges();
bsw/jbe@1309:     }
bsw/jbe@1309:   });
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Rich Text Query/Formatting Commands
bsw/jbe@1309:  *
bsw/jbe@1309:  * @example
bsw/jbe@1309:  *    var commands = new wysihtml.Commands(editor);
bsw/jbe@1309:  */
bsw/jbe@1309: wysihtml.Commands = Base.extend(
bsw/jbe@1309:   /** @scope wysihtml.Commands.prototype */ {
bsw/jbe@1309:   constructor: function(editor) {
bsw/jbe@1309:     this.editor   = editor;
bsw/jbe@1309:     this.composer = editor.composer;
bsw/jbe@1309:     this.doc      = this.composer.doc;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Check whether the browser supports the given command
bsw/jbe@1309:    *
bsw/jbe@1309:    * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
bsw/jbe@1309:    * @example
bsw/jbe@1309:    *    commands.supports("createLink");
bsw/jbe@1309:    */
bsw/jbe@1309:   support: function(command) {
bsw/jbe@1309:     return wysihtml.browser.supportsCommand(this.doc, command);
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Check whether the browser supports the given command
bsw/jbe@1309:    *
bsw/jbe@1309:    * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
bsw/jbe@1309:    * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
bsw/jbe@1309:    * @example
bsw/jbe@1309:    *    commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
bsw/jbe@1309:    */
bsw/jbe@1309:   exec: function(command, value) {
bsw/jbe@1309:     var obj     = wysihtml.commands[command],
bsw/jbe@1309:         args    = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309:         method  = obj && obj.exec,
bsw/jbe@1309:         result  = null;
bsw/jbe@1309: 
bsw/jbe@1309:     // If composer ahs placeholder unset it before command
bsw/jbe@1309:     // Do not apply on commands that are behavioral 
bsw/jbe@1309:     if (this.composer.hasPlaceholderSet() && !wysihtml.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) {
bsw/jbe@1309:       this.composer.element.innerHTML = "";
bsw/jbe@1309:       this.composer.selection.selectNode(this.composer.element);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     this.editor.fire("beforecommand:composer");
bsw/jbe@1309: 
bsw/jbe@1309:     if (method) {
bsw/jbe@1309:       args.unshift(this.composer);
bsw/jbe@1309:       result = method.apply(obj, args);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       try {
bsw/jbe@1309:         // try/catch for buggy firefox
bsw/jbe@1309:         result = this.doc.execCommand(command, false, value);
bsw/jbe@1309:       } catch(e) {}
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     this.editor.fire("aftercommand:composer");
bsw/jbe@1309:     return result;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   remove: function(command, commandValue) {
bsw/jbe@1309:     var obj     = wysihtml.commands[command],
bsw/jbe@1309:         args    = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309:         method  = obj && obj.remove;
bsw/jbe@1309:     if (method) {
bsw/jbe@1309:       args.unshift(this.composer);
bsw/jbe@1309:       return method.apply(obj, args);
bsw/jbe@1309:     }
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * Check whether the current command is active
bsw/jbe@1309:    * If the caret is within a bold text, then calling this with command "bold" should return true
bsw/jbe@1309:    *
bsw/jbe@1309:    * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
bsw/jbe@1309:    * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
bsw/jbe@1309:    * @return {Boolean} Whether the command is active
bsw/jbe@1309:    * @example
bsw/jbe@1309:    *    var isCurrentSelectionBold = commands.state("bold");
bsw/jbe@1309:    */
bsw/jbe@1309:   state: function(command, commandValue) {
bsw/jbe@1309:     var obj     = wysihtml.commands[command],
bsw/jbe@1309:         args    = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309:         method  = obj && obj.state;
bsw/jbe@1309:     if (method) {
bsw/jbe@1309:       args.unshift(this.composer);
bsw/jbe@1309:       return method.apply(obj, args);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       try {
bsw/jbe@1309:         // try/catch for buggy firefox
bsw/jbe@1309:         return this.doc.queryCommandState(command);
bsw/jbe@1309:       } catch(e) {
bsw/jbe@1309:         return false;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   /* Get command state parsed value if command has stateValue parsing function */
bsw/jbe@1309:   stateValue: function(command) {
bsw/jbe@1309:     var obj     = wysihtml.commands[command],
bsw/jbe@1309:         args    = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309:         method  = obj && obj.stateValue;
bsw/jbe@1309:     if (method) {
bsw/jbe@1309:       args.unshift(this.composer);
bsw/jbe@1309:       return method.apply(obj, args);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: });
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   var nodeOptions = {
bsw/jbe@1309:     nodeName: "A",
bsw/jbe@1309:     toggle: false
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   function getOptions(value) {
bsw/jbe@1309:     var options = typeof value === 'object' ? value : {'href': value};
bsw/jbe@1309:     return wysihtml.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get();
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.commands.createLink  = {
bsw/jbe@1309:     exec: function(composer, command, value) {
bsw/jbe@1309:       var opts = getOptions(value);
bsw/jbe@1309: 
bsw/jbe@1309:       if (composer.selection.isCollapsed() && !this.state(composer, command)) {
bsw/jbe@1309:         var textNode = composer.doc.createTextNode(opts.attribute.href);
bsw/jbe@1309:         composer.selection.insertNode(textNode);
bsw/jbe@1309:         composer.selection.selectNode(textNode);
bsw/jbe@1309:       }
bsw/jbe@1309:       wysihtml.commands.formatInline.exec(composer, command, opts);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer, command) {
bsw/jbe@1309:       return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /* Formatblock
bsw/jbe@1309:  * Is used to insert block level elements 
bsw/jbe@1309:  * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...)
bsw/jbe@1309:  * 
bsw/jbe@1309: */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   var dom = wysihtml.dom,
bsw/jbe@1309:       // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
bsw/jbe@1309:       // instead of creating a H4 within a H1 which would result in semantically invalid html
bsw/jbe@1309:       UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre",
bsw/jbe@1309:       BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote",
bsw/jbe@1309:       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";
bsw/jbe@1309: 
bsw/jbe@1309:   function correctOptionsForSimilarityCheck(options) {
bsw/jbe@1309:     return {
bsw/jbe@1309:       nodeName: options.nodeName || null,
bsw/jbe@1309:       className: (!options.classRegExp) ? options.className || null : null,
bsw/jbe@1309:       classRegExp: options.classRegExp || null,
bsw/jbe@1309:       styleProperty: options.styleProperty || null
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function getRangeNode(node, offset) {
bsw/jbe@1309:     if (node.nodeType === 3) {
bsw/jbe@1309:       return node;
bsw/jbe@1309:     } else {
bsw/jbe@1309:       return node.childNodes[offset] || node;
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Returns if node is a line break
bsw/jbe@1309:   function isBr(n) {
bsw/jbe@1309:     return n && n.nodeType === 1 && n.nodeName === "BR";
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Is block level element
bsw/jbe@1309:   function isBlock(n, composer) {
bsw/jbe@1309:     return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // 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)
bsw/jbe@1309:   function isBookmark(n) {
bsw/jbe@1309:     return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Is line breaking node
bsw/jbe@1309:   function isLineBreaking(n, composer) {
bsw/jbe@1309:     return isBr(n) || isBlock(n, composer);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Removes empty block level elements
bsw/jbe@1309:   function cleanup(composer, newBlockElements) {
bsw/jbe@1309:     wysihtml.dom.removeInvisibleSpaces(composer.element);
bsw/jbe@1309:     var container = composer.element,
bsw/jbe@1309:         allElements = container.querySelectorAll(BLOCK_ELEMENTS),
bsw/jbe@1309:         noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
bsw/jbe@1309:         uneditables = container.querySelectorAll(noEditQuery),
bsw/jbe@1309:         elements = wysihtml.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
bsw/jbe@1309:         nbIdx;
bsw/jbe@1309: 
bsw/jbe@1309:     for (var i = elements.length; i--;) {
bsw/jbe@1309:       if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) {
bsw/jbe@1309:         // If cleanup removes some new block elements. remove them from newblocks array too
bsw/jbe@1309:         nbIdx = wysihtml.lang.array(newBlockElements).indexOf(elements[i]);
bsw/jbe@1309:         if (nbIdx > -1) {
bsw/jbe@1309:           newBlockElements.splice(nbIdx, 1);
bsw/jbe@1309:         }
bsw/jbe@1309:         elements[i].parentNode.removeChild(elements[i]);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     
bsw/jbe@1309:     return newBlockElements;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function defaultNodeName(composer) {
bsw/jbe@1309:     return composer.config.useLineBreaks ? "DIV" : "P";
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // The outermost un-nestable block element parent of from node
bsw/jbe@1309:   function findOuterBlock(node, container, allBlocks) {
bsw/jbe@1309:     var n = node,
bsw/jbe@1309:         block = null;
bsw/jbe@1309:         
bsw/jbe@1309:     while (n && container && n !== container) {
bsw/jbe@1309:       if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) {
bsw/jbe@1309:         block = n;
bsw/jbe@1309:       }
bsw/jbe@1309:       n = n.parentNode;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return block;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Clone for splitting the inner inline element out of its parent inline elements context
bsw/jbe@1309:   // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
bsw/jbe@1309:   function cloneOuterInlines(node, container) {
bsw/jbe@1309:     var n = node,
bsw/jbe@1309:         innerNode,
bsw/jbe@1309:         parentNode,
bsw/jbe@1309:         el = null,
bsw/jbe@1309:         el2;
bsw/jbe@1309: 
bsw/jbe@1309:     while (n && container && n !== container) {
bsw/jbe@1309:       if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
bsw/jbe@1309:         parentNode = n;
bsw/jbe@1309:         if (el === null) {
bsw/jbe@1309:           el = n.cloneNode(false);
bsw/jbe@1309:           innerNode = el;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           el2 = n.cloneNode(false);
bsw/jbe@1309:           el2.appendChild(el);
bsw/jbe@1309:           el = el2;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       n = n.parentNode;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return {
bsw/jbe@1309:       parent: parentNode,
bsw/jbe@1309:       outerNode: el,
bsw/jbe@1309:       innerNode: innerNode
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Formats an element according to options nodeName, className, styleProperty, styleValue
bsw/jbe@1309:   // If element is not defined, creates new element
bsw/jbe@1309:   // if opotions is null, remove format instead
bsw/jbe@1309:   function applyOptionsToElement(element, options, composer) {
bsw/jbe@1309: 
bsw/jbe@1309:     if (!element) {
bsw/jbe@1309:       element = composer.doc.createElement(options.nodeName || defaultNodeName(composer));
bsw/jbe@1309:       // Add invisible space as otherwise webkit cannot set selection or range to it correctly
bsw/jbe@1309:       element.appendChild(composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.nodeName && element.nodeName !== options.nodeName) {
bsw/jbe@1309:       element = dom.renameElement(element, options.nodeName);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Remove similar classes before applying className
bsw/jbe@1309:     if (options.classRegExp) {
bsw/jbe@1309:       element.className = element.className.replace(options.classRegExp, "");
bsw/jbe@1309:     }
bsw/jbe@1309:     if (options.className) {
bsw/jbe@1309:       element.classList.add(options.className);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.styleProperty && typeof options.styleValue !== "undefined") {
bsw/jbe@1309:       element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return element;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Unsets element properties by options
bsw/jbe@1309:   // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
bsw/jbe@1309:   function removeOptionsFromElement(element, options, composer) {
bsw/jbe@1309:     var style, classes,
bsw/jbe@1309:         prevNode = element.previousSibling,
bsw/jbe@1309:         nextNode = element.nextSibling,
bsw/jbe@1309:         unwrapped = false;
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.styleProperty) {
bsw/jbe@1309:       element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
bsw/jbe@1309:     }
bsw/jbe@1309:     if (options.className) {
bsw/jbe@1309:       element.classList.remove(options.className);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.classRegExp) {
bsw/jbe@1309:       element.className = element.className.replace(options.classRegExp, "");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Clean up blank class attribute
bsw/jbe@1309:     if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") {
bsw/jbe@1309:       element.removeAttribute('class');
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
bsw/jbe@1309:       style = element.getAttribute('style');
bsw/jbe@1309:       if (!style || style.trim() === '') {
bsw/jbe@1309:         dom.unwrap(element);
bsw/jbe@1309:         unwrapped = true;
bsw/jbe@1309:       } else {
bsw/jbe@1309:         element = dom.renameElement(element, defaultNodeName(composer));
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Clean up blank style attribute
bsw/jbe@1309:     if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
bsw/jbe@1309:       element.removeAttribute('style');
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (unwrapped) {
bsw/jbe@1309:       applySurroundingLineBreaks(prevNode, nextNode, composer);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Unwraps block level elements from inside content
bsw/jbe@1309:   // Useful as not all block level elements can contain other block-levels
bsw/jbe@1309:   function unwrapBlocksFromContent(element) {
bsw/jbe@1309:     var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
bsw/jbe@1309:         nextEl, prevEl;
bsw/jbe@1309: 
bsw/jbe@1309:     for (var i = blocks.length; i--;) {
bsw/jbe@1309:       nextEl = wysihtml.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
bsw/jbe@1309:       prevEl = wysihtml.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:       
bsw/jbe@1309:       if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
bsw/jbe@1309:         if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
bsw/jbe@1309:           blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
bsw/jbe@1309:         if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
bsw/jbe@1309:           blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       wysihtml.dom.unwrap(blocks[i]);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Fix ranges that visually cover whole block element to actually cover the block
bsw/jbe@1309:   function fixRangeCoverage(range, composer) {
bsw/jbe@1309:     var node,
bsw/jbe@1309:         start = range.startContainer,
bsw/jbe@1309:         end = range.endContainer;
bsw/jbe@1309: 
bsw/jbe@1309:     // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
bsw/jbe@1309:     // This ensures the wrapper node is modified and optios added to it
bsw/jbe@1309:     if (start && start.nodeType === 1 && start === end) {
bsw/jbe@1309:       if (start.firstChild === start.lastChild && range.endOffset === 1) {
bsw/jbe@1309:         if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
bsw/jbe@1309:           range.setStartBefore(start);
bsw/jbe@1309:           range.setEndAfter(end);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // 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
bsw/jbe@1309:     if (start && start.nodeType === 1 && end.nodeType === 3) {
bsw/jbe@1309:       if (start.firstChild === end && range.endOffset === end.data.length) {
bsw/jbe@1309:         if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
bsw/jbe@1309:           range.setEndAfter(start);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309:     
bsw/jbe@1309:     // 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
bsw/jbe@1309:     if (end && end.nodeType === 1 && start.nodeType === 3) {
bsw/jbe@1309:       if (end.firstChild === start && range.startOffset === 0) {
bsw/jbe@1309:         if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
bsw/jbe@1309:           range.setStartBefore(end);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // If range covers a whole textnode and the textnode is the only child of node, extend range to node 
bsw/jbe@1309:     if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
bsw/jbe@1309:       if (range.endOffset == end.data.length && range.startOffset === 0) {
bsw/jbe@1309:         node = start.parentNode;
bsw/jbe@1309:         if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
bsw/jbe@1309:           range.setStartBefore(node);
bsw/jbe@1309:           range.setEndAfter(node);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
bsw/jbe@1309:   // Some places do not allow block level elements inbetween (inside ul and outside li)
bsw/jbe@1309:   // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
bsw/jbe@1309:   function fixNotPermittedInsertionPoints(ranges) {
bsw/jbe@1309:     var newRanges = [],
bsw/jbe@1309:         lis, j, maxj, tmpRange, rangePos, closestLI;
bsw/jbe@1309:         
bsw/jbe@1309:     for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309:       
bsw/jbe@1309:       // Fixes range start and end positions if inside UL or OL element (outside of LI)
bsw/jbe@1309:       if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
bsw/jbe@1309:         ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
bsw/jbe@1309:         closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
bsw/jbe@1309:         if (closestLI.childNodes) {
bsw/jbe@1309:           ranges[i].setEnd(closestLI, closestLI.childNodes.length);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Get all LI eleemnts in selection (fully or partially covered)
bsw/jbe@1309:       // And make sure ranges are either inside LI or outside UL/OL
bsw/jbe@1309:       // Split and add new ranges as needed to cover same range content
bsw/jbe@1309:       // TODO: Needs improvement to accept DL, DD, DT
bsw/jbe@1309:       lis = ranges[i].getNodes([1], function(node) {
bsw/jbe@1309:         return node.nodeName === "LI";
bsw/jbe@1309:       });
bsw/jbe@1309:       if (lis.length > 0) {
bsw/jbe@1309:       
bsw/jbe@1309:         for (j = 0, maxj = lis.length; j < maxj; j++) {
bsw/jbe@1309:           rangePos = ranges[i].compareNode(lis[j]);
bsw/jbe@1309: 
bsw/jbe@1309:           // Fixes start of range that crosses LI border
bsw/jbe@1309:           if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
bsw/jbe@1309:             // Range starts before and ends inside the node
bsw/jbe@1309: 
bsw/jbe@1309:             tmpRange = ranges[i].cloneRange();
bsw/jbe@1309:             closestLI = wysihtml.dom.domNode(lis[j]).prev({nodeTypes: [1]});
bsw/jbe@1309:             
bsw/jbe@1309:             if (closestLI) {
bsw/jbe@1309:               tmpRange.setEnd(closestLI, closestLI.childNodes.length);
bsw/jbe@1309:             } else if (lis[j].closest('ul, ol')) {
bsw/jbe@1309:               tmpRange.setEndBefore(lis[j].closest('ul, ol'));
bsw/jbe@1309:             } else {
bsw/jbe@1309:               tmpRange.setEndBefore(lis[j]);
bsw/jbe@1309:             }
bsw/jbe@1309:             newRanges.push(tmpRange);
bsw/jbe@1309:             ranges[i].setStart(lis[j], 0);
bsw/jbe@1309:           }
bsw/jbe@1309:           
bsw/jbe@1309:           // Fixes end of range that crosses li border
bsw/jbe@1309:           if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
bsw/jbe@1309:             // Range starts inside the node and ends after node
bsw/jbe@1309:             
bsw/jbe@1309:             tmpRange = ranges[i].cloneRange();
bsw/jbe@1309:             tmpRange.setEnd(lis[j], lis[j].childNodes.length);
bsw/jbe@1309:             newRanges.push(tmpRange);
bsw/jbe@1309:             
bsw/jbe@1309:             // Find next LI in list and if present set range to it, else 
bsw/jbe@1309:             closestLI = wysihtml.dom.domNode(lis[j]).next({nodeTypes: [1]});
bsw/jbe@1309:             if (closestLI) {
bsw/jbe@1309:               ranges[i].setStart(closestLI, 0);
bsw/jbe@1309:             } else if (lis[j].closest('ul, ol')) {
bsw/jbe@1309:               ranges[i].setStartAfter(lis[j].closest('ul, ol'));
bsw/jbe@1309:             } else {
bsw/jbe@1309:               ranges[i].setStartAfter(lis[j]);
bsw/jbe@1309:             } 
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:         newRanges.push(ranges[i]);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         newRanges.push(ranges[i]);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return newRanges;
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Return options object with nodeName set if original did not have any
bsw/jbe@1309:   // Node name is set to local or global default
bsw/jbe@1309:   function getOptionsWithNodename(options, defaultName, composer) {
bsw/jbe@1309:     var correctedOptions = (options) ? wysihtml.lang.object(options).clone(true) : null;
bsw/jbe@1309:     if (correctedOptions) {  
bsw/jbe@1309:       correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
bsw/jbe@1309:     }
bsw/jbe@1309:     return correctedOptions;
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
bsw/jbe@1309:   // Also wraps empty clones of split parent tags around fragment to keep formatting
bsw/jbe@1309:   // 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)
bsw/jbe@1309:   function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
bsw/jbe@1309:     var rangeStartContainer = range.startContainer,
bsw/jbe@1309:         firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
bsw/jbe@1309:         outerInlines, first, last, prev, next;
bsw/jbe@1309:     
bsw/jbe@1309:     if (firstOuterBlock) {
bsw/jbe@1309:       // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
bsw/jbe@1309:       first = fragment.firstChild;
bsw/jbe@1309:       last = fragment.lastChild;
bsw/jbe@1309: 
bsw/jbe@1309:       composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
bsw/jbe@1309: 
bsw/jbe@1309:       next = wysihtml.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:       prev = wysihtml.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309: 
bsw/jbe@1309:       if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
bsw/jbe@1309:         first.parentNode.insertBefore(composer.doc.createElement('br'), first);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
bsw/jbe@1309:         next.parentNode.insertBefore(composer.doc.createElement('br'), next);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:     } else {
bsw/jbe@1309:       // Ensure node does not get inserted into an inline where it is not allowed
bsw/jbe@1309:       outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
bsw/jbe@1309:       if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
bsw/jbe@1309:         if (fragment.childNodes.length === 1) {
bsw/jbe@1309:           while(fragment.firstChild.firstChild) {
bsw/jbe@1309:             outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
bsw/jbe@1309:           }
bsw/jbe@1309:           fragment.firstChild.appendChild(outerInlines.outerNode);
bsw/jbe@1309:         }
bsw/jbe@1309:         composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         var fc = fragment.firstChild,
bsw/jbe@1309:             lc = fragment.lastChild;
bsw/jbe@1309: 
bsw/jbe@1309:         range.insertNode(fragment);
bsw/jbe@1309:         // restore range position as it might get lost in webkit sometimes
bsw/jbe@1309:         range.setStartBefore(fc);
bsw/jbe@1309:         range.setEndAfter(lc);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Removes all block formatting from range
bsw/jbe@1309:   function clearRangeBlockFromating(range, closestBlockName, composer) {
bsw/jbe@1309:     var r = range.cloneRange(),
bsw/jbe@1309:         prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
bsw/jbe@1309:         nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
bsw/jbe@1309:         content = r.extractContents(),
bsw/jbe@1309:         fragment = composer.doc.createDocumentFragment(),
bsw/jbe@1309:         children, blocks,
bsw/jbe@1309:         first = true;
bsw/jbe@1309:         
bsw/jbe@1309:     while(content.firstChild) {
bsw/jbe@1309:       // Iterate over all selection content first level childNodes
bsw/jbe@1309:       if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
bsw/jbe@1309:         // If node is a block element
bsw/jbe@1309:         // Split block formating and add new block to wrap caret
bsw/jbe@1309:         
bsw/jbe@1309:         unwrapBlocksFromContent(content.firstChild);
bsw/jbe@1309:         children = wysihtml.dom.unwrap(content.firstChild);
bsw/jbe@1309:         
bsw/jbe@1309:         // Add line break before if needed
bsw/jbe@1309:         if (children.length > 0) {
bsw/jbe@1309:           if (
bsw/jbe@1309:             (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
bsw/jbe@1309:             (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
bsw/jbe@1309:           ){
bsw/jbe@1309:             fragment.appendChild(composer.doc.createElement('BR'));
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:         
bsw/jbe@1309:         for (var c = 0, cmax = children.length; c < cmax; c++) {
bsw/jbe@1309:           fragment.appendChild(children[c]);
bsw/jbe@1309:         }
bsw/jbe@1309:         
bsw/jbe@1309:         // Add line break after if needed
bsw/jbe@1309:         if (children.length > 0) {
bsw/jbe@1309:           if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
bsw/jbe@1309:             if (nextNode || fragment.lastChild !== content.lastChild) {
bsw/jbe@1309:               fragment.appendChild(composer.doc.createElement('BR'));
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:         
bsw/jbe@1309:       } else {
bsw/jbe@1309:         fragment.appendChild(content.firstChild);
bsw/jbe@1309:       }
bsw/jbe@1309:       
bsw/jbe@1309:       first = false;
bsw/jbe@1309:     }
bsw/jbe@1309:     blocks = wysihtml.lang.array(fragment.childNodes).get();
bsw/jbe@1309:     injectFragmentToRange(fragment, r, composer);
bsw/jbe@1309:     return blocks;
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
bsw/jbe@1309:   function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
bsw/jbe@1309:     var prevPrev = prevNode && wysihtml.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:     if (isBr(nextNode)) {
bsw/jbe@1309:       nextNode.parentNode.removeChild(nextNode);
bsw/jbe@1309:     }
bsw/jbe@1309:     if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
bsw/jbe@1309:       prevNode.parentNode.removeChild(prevNode);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function applySurroundingLineBreaks(prevNode, nextNode, composer) {
bsw/jbe@1309:     var prevPrev;
bsw/jbe@1309: 
bsw/jbe@1309:     if (prevNode && isBookmark(prevNode)) {
bsw/jbe@1309:       prevNode = prevNode.previousSibling;
bsw/jbe@1309:     }
bsw/jbe@1309:     if (nextNode && isBookmark(nextNode)) {
bsw/jbe@1309:       nextNode = nextNode.nextSibling;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     prevPrev = prevNode && prevNode.previousSibling;
bsw/jbe@1309: 
bsw/jbe@1309:     if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
bsw/jbe@1309:       prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
bsw/jbe@1309:       nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   var isWhitespaceBefore = function (textNode, offset) {
bsw/jbe@1309:     var str = textNode.data ? textNode.data.slice(0, offset) : "";
bsw/jbe@1309:     return (/^\s*$/).test(str);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   var isWhitespaceAfter = function (textNode, offset) {
bsw/jbe@1309:     var str = textNode.data ? textNode.data.slice(offset) : "";
bsw/jbe@1309:     return (/^\s*$/).test(str);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   var trimBlankTextsAndBreaks = function(fragment) {
bsw/jbe@1309:     if (fragment) {
bsw/jbe@1309:       while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309:         fragment.removeChild(fragment.firstChild);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309:         fragment.removeChild(fragment.lastChild);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309:         fragment.removeChild(fragment.firstChild);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309:         fragment.removeChild(fragment.lastChild);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Wrap the range with a block level element
bsw/jbe@1309:   // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
bsw/jbe@1309:   function wrapRangeWithElement(range, options, closestBlockName, composer) {
bsw/jbe@1309:     var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
bsw/jbe@1309:         r = range.cloneRange(),
bsw/jbe@1309:         rangeStartContainer = r.startContainer,
bsw/jbe@1309:         startNode = getRangeNode(r.startContainer, r.startOffset),
bsw/jbe@1309:         endNode = getRangeNode(r.endContainer, r.endOffset),
bsw/jbe@1309:         prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode :  wysihtml.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
bsw/jbe@1309:         nextNode = (
bsw/jbe@1309:           (
bsw/jbe@1309:             r.endContainer.nodeType === 1 &&
bsw/jbe@1309:             r.endContainer.childNodes[r.endOffset] === endNode &&
bsw/jbe@1309:             (
bsw/jbe@1309:               endNode.nodeType === 1 ||
bsw/jbe@1309:               !isWhitespaceAfter(endNode, r.endOffset) &&
bsw/jbe@1309:               !wysihtml.dom.domNode(endNode).is.rangyBookmark()
bsw/jbe@1309:             )
bsw/jbe@1309:           ) || (
bsw/jbe@1309:             r.endContainer === endNode &&
bsw/jbe@1309:             endNode.nodeType === 3 &&
bsw/jbe@1309:             !isWhitespaceAfter(endNode, r.endOffset)
bsw/jbe@1309:           )
bsw/jbe@1309:         ) ? endNode : wysihtml.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
bsw/jbe@1309:         content = r.extractContents(),
bsw/jbe@1309:         fragment = composer.doc.createDocumentFragment(),
bsw/jbe@1309:         similarOuterBlock = similarOptions ? wysihtml.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
bsw/jbe@1309:         splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
bsw/jbe@1309:         firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
bsw/jbe@1309:         wrapper, blocks, children,
bsw/jbe@1309:         firstc, lastC;
bsw/jbe@1309: 
bsw/jbe@1309:     if (wysihtml.dom.domNode(nextNode).is.rangyBookmark()) {
bsw/jbe@1309:       endNode = nextNode;
bsw/jbe@1309:       nextNode = endNode.nextSibling;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     trimBlankTextsAndBreaks(content);
bsw/jbe@1309: 
bsw/jbe@1309:     if (options && options.nodeName === "BLOCKQUOTE") {
bsw/jbe@1309:       
bsw/jbe@1309:       // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
bsw/jbe@1309:       var tmpEl = applyOptionsToElement(null, options, composer);
bsw/jbe@1309:       tmpEl.appendChild(content);
bsw/jbe@1309:       fragment.appendChild(tmpEl);
bsw/jbe@1309:       blocks = [tmpEl];
bsw/jbe@1309:       
bsw/jbe@1309:     } else {
bsw/jbe@1309: 
bsw/jbe@1309:       if (!content.firstChild) {
bsw/jbe@1309:         // IF selection is caret (can happen if line is empty) add format around tag 
bsw/jbe@1309:         fragment.appendChild(applyOptionsToElement(null, options, composer));
bsw/jbe@1309:       } else {
bsw/jbe@1309: 
bsw/jbe@1309:         while(content.firstChild) {
bsw/jbe@1309:           // Iterate over all selection content first level childNodes
bsw/jbe@1309:           
bsw/jbe@1309:           if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
bsw/jbe@1309:             
bsw/jbe@1309:             // If node is a block element
bsw/jbe@1309:             // Escape(split) block formatting at caret
bsw/jbe@1309:             applyOptionsToElement(content.firstChild, options, composer);
bsw/jbe@1309:             if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
bsw/jbe@1309:               unwrapBlocksFromContent(content.firstChild);
bsw/jbe@1309:             }
bsw/jbe@1309:             fragment.appendChild(content.firstChild);
bsw/jbe@1309:             
bsw/jbe@1309:           } else {
bsw/jbe@1309:             
bsw/jbe@1309:             // Wrap subsequent non-block nodes inside new block element
bsw/jbe@1309:             wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
bsw/jbe@1309:             while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
bsw/jbe@1309:               if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
bsw/jbe@1309:                 unwrapBlocksFromContent(content.firstChild);
bsw/jbe@1309:               }
bsw/jbe@1309:               wrapper.appendChild(content.firstChild);
bsw/jbe@1309:             }
bsw/jbe@1309:             fragment.appendChild(wrapper);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       blocks = wysihtml.lang.array(fragment.childNodes).get();
bsw/jbe@1309:     }
bsw/jbe@1309:     injectFragmentToRange(fragment, r, composer, firstOuterBlock);
bsw/jbe@1309:     removeSurroundingLineBreaks(prevNode, nextNode, composer);
bsw/jbe@1309: 
bsw/jbe@1309:     // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block
bsw/jbe@1309:     // (if it contains rangy bookmark, so selection can be restored later correctly)
bsw/jbe@1309:     if (blocks.length > 0 &&
bsw/jbe@1309:       (
bsw/jbe@1309:         typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark()
bsw/jbe@1309:       )
bsw/jbe@1309:     ) {
bsw/jbe@1309:       blocks[blocks.length - 1].appendChild(composer.doc.createElement('br'));
bsw/jbe@1309:     }
bsw/jbe@1309:     return blocks;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Find closest block level element
bsw/jbe@1309:   function getParentBlockNodeName(element, composer) {
bsw/jbe@1309:     var parentNode = wysihtml.dom.getParentElement(element, {
bsw/jbe@1309:           query: BLOCK_ELEMENTS
bsw/jbe@1309:         }, null, composer.element);
bsw/jbe@1309: 
bsw/jbe@1309:     return (parentNode) ? parentNode.nodeName : null;
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Expands caret to cover the closest block that:
bsw/jbe@1309:   //   * cannot contain other block level elements (h1-6,p, etc)
bsw/jbe@1309:   //   * Has the same nodeName that is to be inserted
bsw/jbe@1309:   //   * has insertingNodeName
bsw/jbe@1309:   //   * is DIV if insertingNodeName is not present
bsw/jbe@1309:   //
bsw/jbe@1309:   // If nothing found selects the current line
bsw/jbe@1309:   function expandCaretToBlock(composer, insertingNodeName) {
bsw/jbe@1309:     var parent = wysihtml.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
bsw/jbe@1309:           query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
bsw/jbe@1309:         }, null, composer.element),
bsw/jbe@1309:         range;
bsw/jbe@1309: 
bsw/jbe@1309:     if (parent) {
bsw/jbe@1309:       range = composer.selection.createRange();
bsw/jbe@1309:       range.selectNode(parent);
bsw/jbe@1309:       composer.selection.setSelection(range);
bsw/jbe@1309:     } else if (!composer.isEmpty()) {
bsw/jbe@1309:       composer.selection.selectLine();
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
bsw/jbe@1309:   // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
bsw/jbe@1309:   function selectElements(newBlockElements, composer) {
bsw/jbe@1309:     var range = composer.selection.createRange(),
bsw/jbe@1309:         lastEl = newBlockElements[newBlockElements.length - 1],
bsw/jbe@1309:         lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 :  lastEl.length || 0;
bsw/jbe@1309: 
bsw/jbe@1309:     range.setStart(newBlockElements[0], 0);
bsw/jbe@1309:     range.setEnd(lastEl, lastOffset);
bsw/jbe@1309:     range.select();
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
bsw/jbe@1309:   // Return created/modified block level elements 
bsw/jbe@1309:   // Method can be either "apply" or "remove"
bsw/jbe@1309:   function formatSelection(method, composer, options) {
bsw/jbe@1309:     var ranges = composer.selection.getOwnRanges(),
bsw/jbe@1309:         newBlockElements = [],
bsw/jbe@1309:         closestBlockName;
bsw/jbe@1309:         
bsw/jbe@1309:     // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
bsw/jbe@1309:     ranges = fixNotPermittedInsertionPoints(ranges);
bsw/jbe@1309:         
bsw/jbe@1309:     for (var i = ranges.length; i--;) {
bsw/jbe@1309:       fixRangeCoverage(ranges[i], composer);
bsw/jbe@1309:       closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
bsw/jbe@1309:       if (method === "remove") {
bsw/jbe@1309:         newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
bsw/jbe@1309:       } else {
bsw/jbe@1309:         newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return newBlockElements;
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // If properties is passed as a string, look for tag with that tagName/query 
bsw/jbe@1309:   function parseOptions(options) {
bsw/jbe@1309:     if (typeof options === "string") {
bsw/jbe@1309:       options = {
bsw/jbe@1309:         nodeName: options.toUpperCase()
bsw/jbe@1309:       };
bsw/jbe@1309:     }
bsw/jbe@1309:     return options;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function caretIsOnEmptyLine(composer) {
bsw/jbe@1309:     var caretInfo;
bsw/jbe@1309:     if (composer.selection.isCollapsed()) {
bsw/jbe@1309:       caretInfo = composer.selection.getNodesNearCaret();
bsw/jbe@1309:       if (caretInfo && caretInfo.caretNode) {
bsw/jbe@1309:         if (
bsw/jbe@1309:           // caret is allready breaknode
bsw/jbe@1309:           wysihtml.dom.domNode(caretInfo.caretNode).is.lineBreak() ||
bsw/jbe@1309:           // caret is textnode
bsw/jbe@1309:           (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak())) ||
bsw/jbe@1309:           // Caret is temprorary rangy selection marker
bsw/jbe@1309:           (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') &&
bsw/jbe@1309:             (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.prevNode).is.block()) &&
bsw/jbe@1309:             (!caretInfo.nextNode || wysihtml.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.nextNode).is.block())
bsw/jbe@1309:           )
bsw/jbe@1309:         ) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.commands.formatBlock = {
bsw/jbe@1309:     exec: function(composer, command, options) {
bsw/jbe@1309:       options = parseOptions(options);
bsw/jbe@1309:       var newBlockElements = [],
bsw/jbe@1309:           ranges, range, bookmark, state, closestBlockName;
bsw/jbe@1309: 
bsw/jbe@1309:       // Find if current format state is active if options.toggle is set as true
bsw/jbe@1309:       // In toggle case active state elemets are formatted instead of working directly on selection
bsw/jbe@1309:       if (options && options.toggle) {
bsw/jbe@1309:         state = this.state(composer, command, options);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (state) {
bsw/jbe@1309:         // Remove format from state nodes if toggle set and state on and selection is collapsed
bsw/jbe@1309:         bookmark = rangy.saveSelection(composer.win);
bsw/jbe@1309:         for (var j = 0, jmax = state.length; j < jmax; j++) {
bsw/jbe@1309:           removeOptionsFromElement(state[j], options, composer);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       } else {
bsw/jbe@1309:         // If selection is caret expand it to cover nearest suitable block element or row if none found
bsw/jbe@1309:         if (composer.selection.isCollapsed()) {
bsw/jbe@1309:           bookmark = rangy.saveSelection(composer.win);
bsw/jbe@1309:           if (caretIsOnEmptyLine(composer)) {
bsw/jbe@1309:             composer.selection.selectLine();
bsw/jbe@1309:           } else {
bsw/jbe@1309:             expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:         if (options) {
bsw/jbe@1309:           newBlockElements = formatSelection("apply", composer, options);
bsw/jbe@1309:         } else {
bsw/jbe@1309:           // Options == null means block formatting should be removed from selection
bsw/jbe@1309:           newBlockElements = formatSelection("remove", composer);
bsw/jbe@1309:         }
bsw/jbe@1309:         
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Remove empty block elements that may be left behind
bsw/jbe@1309:       // Also remove them from new blocks list
bsw/jbe@1309:       newBlockElements = cleanup(composer, newBlockElements);
bsw/jbe@1309:       
bsw/jbe@1309:       // Restore selection
bsw/jbe@1309:       if (bookmark) {
bsw/jbe@1309:         rangy.restoreSelection(bookmark);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         selectElements(newBlockElements, composer);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309:     
bsw/jbe@1309:     // Removes all block formatting from selection
bsw/jbe@1309:     remove: function(composer, command, options) {
bsw/jbe@1309:       options = parseOptions(options);
bsw/jbe@1309:       var newBlockElements, bookmark;
bsw/jbe@1309:       
bsw/jbe@1309:       // If selection is caret expand it to cover nearest suitable block element or row if none found
bsw/jbe@1309:       if (composer.selection.isCollapsed()) {
bsw/jbe@1309:         bookmark = rangy.saveSelection(composer.win);
bsw/jbe@1309:         expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
bsw/jbe@1309:       }
bsw/jbe@1309:       
bsw/jbe@1309:       newBlockElements = formatSelection("remove", composer);
bsw/jbe@1309:       newBlockElements = cleanup(composer, newBlockElements);
bsw/jbe@1309:       
bsw/jbe@1309:       // Restore selection
bsw/jbe@1309:       if (bookmark) {
bsw/jbe@1309:         rangy.restoreSelection(bookmark);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         selectElements(newBlockElements, composer);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // If options as null is passed returns status describing all block level elements
bsw/jbe@1309:     state: function(composer, command, options) {
bsw/jbe@1309:       options = parseOptions(options);
bsw/jbe@1309: 
bsw/jbe@1309:       var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
bsw/jbe@1309:             return wysihtml.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
bsw/jbe@1309:           }).bind(this)),
bsw/jbe@1309:           parentNodes = composer.selection.getSelectedOwnNodes(),
bsw/jbe@1309:           parent;
bsw/jbe@1309: 
bsw/jbe@1309:       // Finds matching elements that are parents of selection and adds to nodes list
bsw/jbe@1309:       for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
bsw/jbe@1309:         parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
bsw/jbe@1309:         if (parent && nodes.indexOf(parent) === -1) {
bsw/jbe@1309:           nodes.push(parent);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return (nodes.length === 0) ? false : nodes;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Unifies all inline tags additions and removals
bsw/jbe@1309:  * See https://github.com/Voog/wysihtml/pull/169 for specification of action
bsw/jbe@1309:  */
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   var defaultTag = "SPAN",
bsw/jbe@1309:       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",
bsw/jbe@1309:       queryAliasMap = {
bsw/jbe@1309:         "b": "b, strong",
bsw/jbe@1309:         "strong": "b, strong",
bsw/jbe@1309:         "em": "em, i",
bsw/jbe@1309:         "i": "em, i"
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:   function hasNoClass(element) {
bsw/jbe@1309:     return (/^\s*$/).test(element.className);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function hasNoStyle(element) {
bsw/jbe@1309:     return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style'));
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Associative arrays in javascript are really objects and do not have length defined
bsw/jbe@1309:   // Thus have to check emptyness in a different way
bsw/jbe@1309:   function hasNoAttributes(element) {
bsw/jbe@1309:     var attr = wysihtml.dom.getAttributes(element);
bsw/jbe@1309:     return wysihtml.lang.object(attr).isEmpty();
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // compares two nodes if they are semantically the same
bsw/jbe@1309:   // Used in cleanup to find consequent semantically similar elements for merge
bsw/jbe@1309:   function isSameNode(element1, element2) {
bsw/jbe@1309:     var classes1, classes2,
bsw/jbe@1309:         attr1, attr2;
bsw/jbe@1309: 
bsw/jbe@1309:     if (element1.nodeType !== 1 || element2.nodeType !== 1) {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (element1.nodeName !== element2.nodeName) {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' ');
bsw/jbe@1309:     classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' ');
bsw/jbe@1309:     if (wysihtml.lang.array(classes1).without(classes2).length > 0) {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     attr1 = wysihtml.dom.getAttributes(element1);
bsw/jbe@1309:     attr2 = wysihtml.dom.getAttributes(element2);
bsw/jbe@1309: 
bsw/jbe@1309:     if (attr1.length !== attr2.length || !wysihtml.lang.object(wysihtml.lang.object(attr1).difference(attr2)).isEmpty()) {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return true;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function createWrapNode(textNode, options) {
bsw/jbe@1309:     var nodeName = options && options.nodeName || defaultTag,
bsw/jbe@1309:         element = textNode.ownerDocument.createElement(nodeName);
bsw/jbe@1309: 
bsw/jbe@1309:     // Remove similar classes before applying className
bsw/jbe@1309:     if (options.classRegExp) {
bsw/jbe@1309:       element.className = element.className.replace(options.classRegExp, "");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.className) {
bsw/jbe@1309:       element.classList.add(options.className);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.styleProperty && typeof options.styleValue !== "undefined") {
bsw/jbe@1309:       element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.attribute) {
bsw/jbe@1309:       if (typeof options.attribute === "object") {
bsw/jbe@1309:         for (var a in options.attribute) {
bsw/jbe@1309:           if (options.attribute.hasOwnProperty(a)) {
bsw/jbe@1309:             element.setAttribute(a, options.attribute[a]);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       } else if (typeof options.attributeValue !== "undefined") {
bsw/jbe@1309:         element.setAttribute(options.attribute, options.attributeValue);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return element;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Tests if attr2 list contains all attributes present in attr1
bsw/jbe@1309:   // Note: attr 1 can have more attributes than attr2
bsw/jbe@1309:   function containsSameAttributes(attr1, attr2) {
bsw/jbe@1309:     for (var a in attr1) {
bsw/jbe@1309:       if (attr1.hasOwnProperty(a)) {
bsw/jbe@1309:         if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) {
bsw/jbe@1309:           return false;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return true;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // If attrbutes and values are the same > remove
bsw/jbe@1309:   // if attributes or values 
bsw/jbe@1309:   function updateElementAttributes(element, newAttributes, toggle) {
bsw/jbe@1309:     var attr = wysihtml.dom.getAttributes(element),
bsw/jbe@1309:         fullContain = containsSameAttributes(newAttributes, attr),
bsw/jbe@1309:         attrDifference = wysihtml.lang.object(attr).difference(newAttributes),
bsw/jbe@1309:         a, b;
bsw/jbe@1309: 
bsw/jbe@1309:     if (fullContain && toggle !== false) {
bsw/jbe@1309:       for (a in newAttributes) {
bsw/jbe@1309:         if (newAttributes.hasOwnProperty(a)) {
bsw/jbe@1309:           element.removeAttribute(a);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     } else {
bsw/jbe@1309: 
bsw/jbe@1309:       /*if (!wysihtml.lang.object(attrDifference).isEmpty()) {
bsw/jbe@1309:         for (b in attrDifference) {
bsw/jbe@1309:           if (attrDifference.hasOwnProperty(b)) {
bsw/jbe@1309:             element.removeAttribute(b);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }*/
bsw/jbe@1309: 
bsw/jbe@1309:       for (a in newAttributes) {
bsw/jbe@1309:         if (newAttributes.hasOwnProperty(a)) {
bsw/jbe@1309:           element.setAttribute(a, newAttributes[a]);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function updateFormatOfElement(element, options) {
bsw/jbe@1309:     var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch;
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.className) {
bsw/jbe@1309:       if (options.toggle !== false && element.classList.contains(options.className)) {
bsw/jbe@1309:         element.classList.remove(options.className);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         if (options.classRegExp) {
bsw/jbe@1309:           element.className = element.className.replace(options.classRegExp, '');
bsw/jbe@1309:         }
bsw/jbe@1309:         element.classList.add(options.className);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (hasNoClass(element)) {
bsw/jbe@1309:         element.removeAttribute('class');
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // change/remove style
bsw/jbe@1309:     if (options.styleProperty) {
bsw/jbe@1309:       if (options.toggle !== false && element.style[wysihtml.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) {
bsw/jbe@1309:         element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
bsw/jbe@1309:       } else {
bsw/jbe@1309:         element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     if (hasNoStyle(element)) {
bsw/jbe@1309:       element.removeAttribute('style');
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (options.attribute) {
bsw/jbe@1309:       if (typeof options.attribute === "object") {
bsw/jbe@1309:         newAttributes =  options.attribute;
bsw/jbe@1309:       } else {
bsw/jbe@1309:         newAttributes = {};
bsw/jbe@1309:         newAttributes[options.attribute] = options.attributeValue || '';
bsw/jbe@1309:       }
bsw/jbe@1309:       updateElementAttributes(element, newAttributes, options.toggle);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:     // Handle similar semantically same elements (queryAliasMap)
bsw/jbe@1309:     nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null;
bsw/jbe@1309:     nodeQueryMatch = nodeNameQuery ? wysihtml.dom.domNode(element).test({ query: nodeNameQuery }) : false;
bsw/jbe@1309:     
bsw/jbe@1309:     // Unwrap element if no attributes present and node name given
bsw/jbe@1309:     // or no attributes and if no nodename set but node is the default
bsw/jbe@1309:     if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) {
bsw/jbe@1309:       if (
bsw/jbe@1309:         ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) &&
bsw/jbe@1309:         hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)
bsw/jbe@1309:       ) {
bsw/jbe@1309:         wysihtml.dom.unwrap(element);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Fetch all textnodes in selection
bsw/jbe@1309:   // Empty textnodes are ignored except the one containing text caret
bsw/jbe@1309:   function getSelectedTextNodes(selection, splitBounds) {
bsw/jbe@1309:     var textNodes = [];
bsw/jbe@1309: 
bsw/jbe@1309:     if (!selection.isCollapsed()) {
bsw/jbe@1309:       textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) {
bsw/jbe@1309:         // Exclude empty nodes except caret node
bsw/jbe@1309:         return (!wysihtml.dom.domNode(node).is.emptyTextNode());
bsw/jbe@1309:       }, splitBounds));
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return textNodes;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function findSimilarTextNodeWrapper(textNode, options, container, exact) {
bsw/jbe@1309:     var node = textNode,
bsw/jbe@1309:         similarOptions = exact ? options : correctOptionsForSimilarityCheck(options);
bsw/jbe@1309: 
bsw/jbe@1309:     do {
bsw/jbe@1309:       if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) {
bsw/jbe@1309:         return node;
bsw/jbe@1309:       }
bsw/jbe@1309:       node = node.parentNode;
bsw/jbe@1309:     } while (node && node !== container);
bsw/jbe@1309: 
bsw/jbe@1309:     return null;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function correctOptionsForSimilarityCheck(options) {
bsw/jbe@1309:     return {
bsw/jbe@1309:       nodeName: options.nodeName || null,
bsw/jbe@1309:       className: (!options.classRegExp) ? options.className || null : null,
bsw/jbe@1309:       classRegExp: options.classRegExp || null,
bsw/jbe@1309:       styleProperty: options.styleProperty || null
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Finds inline node with similar nodeName/style/className
bsw/jbe@1309:   // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes
bsw/jbe@1309:   function isSimilarNode(node, options) {
bsw/jbe@1309:     var o;
bsw/jbe@1309:     if (options.nodeName) {
bsw/jbe@1309:       var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase();
bsw/jbe@1309:       return wysihtml.dom.domNode(node).test({ query: query });
bsw/jbe@1309:     } else {
bsw/jbe@1309:       o = wysihtml.lang.object(options).clone();
bsw/jbe@1309:       o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted
bsw/jbe@1309:       return wysihtml.dom.domNode(node).test(o);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function selectRange(composer, range) {
bsw/jbe@1309:     var d = document.documentElement || document.body,
bsw/jbe@1309:         oldScrollTop  = d.scrollTop,
bsw/jbe@1309:         oldScrollLeft = d.scrollLeft,
bsw/jbe@1309:         selection = rangy.getSelection(composer.win);
bsw/jbe@1309: 
bsw/jbe@1309:     rangy.getSelection(composer.win).removeAllRanges();
bsw/jbe@1309:     
bsw/jbe@1309:     // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again
bsw/jbe@1309:     try {
bsw/jbe@1309:       rangy.getSelection(composer.win).addRange(range);
bsw/jbe@1309:     } catch (e) {}
bsw/jbe@1309:     if (!composer.doc.activeElement || !wysihtml.dom.contains(composer.element, composer.doc.activeElement)) {
bsw/jbe@1309:       composer.element.focus();
bsw/jbe@1309:       d.scrollTop  = oldScrollTop;
bsw/jbe@1309:       d.scrollLeft = oldScrollLeft;
bsw/jbe@1309:       rangy.getSelection(composer.win).addRange(range);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function selectTextNodes(textNodes, composer) {
bsw/jbe@1309:     var range = rangy.createRange(composer.doc),
bsw/jbe@1309:         lastText = textNodes[textNodes.length - 1];
bsw/jbe@1309: 
bsw/jbe@1309:     if (textNodes[0] && lastText) {
bsw/jbe@1309:       range.setStart(textNodes[0], 0);
bsw/jbe@1309:       range.setEnd(lastText, lastText.length);
bsw/jbe@1309:       selectRange(composer, range);
bsw/jbe@1309:     }
bsw/jbe@1309:     
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function selectTextNode(composer, node, start, end) {
bsw/jbe@1309:     var range = rangy.createRange(composer.doc);
bsw/jbe@1309:     if (node) {
bsw/jbe@1309:       range.setStart(node, start);
bsw/jbe@1309:       range.setEnd(node, typeof end !== 'undefined' ? end : start);
bsw/jbe@1309:       selectRange(composer, range);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function getState(composer, options, exact) {
bsw/jbe@1309:     var searchNodes = getSelectedTextNodes(composer.selection),
bsw/jbe@1309:         nodes = [],
bsw/jbe@1309:         partial = false,
bsw/jbe@1309:         node, range, caretNode;
bsw/jbe@1309: 
bsw/jbe@1309:     if (composer.selection.isInThisEditable()) {
bsw/jbe@1309: 
bsw/jbe@1309:       if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
bsw/jbe@1309:         caretNode = composer.selection.getSelection().anchorNode;
bsw/jbe@1309:         if (!caretNode) {
bsw/jbe@1309:           // selection not in editor
bsw/jbe@1309:           return {
bsw/jbe@1309:               nodes: [],
bsw/jbe@1309:               partial: false
bsw/jbe@1309:           };
bsw/jbe@1309:         }
bsw/jbe@1309:         if (caretNode.nodeType === 3) {
bsw/jbe@1309:           searchNodes = [caretNode];
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Handle collapsed selection caret
bsw/jbe@1309:       if (!searchNodes.length) {
bsw/jbe@1309:         range = composer.selection.getOwnRanges()[0];
bsw/jbe@1309:         if (range) {
bsw/jbe@1309:           searchNodes = [range.endContainer];
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
bsw/jbe@1309:         node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
bsw/jbe@1309:         if (node) {
bsw/jbe@1309:           nodes.push(node);
bsw/jbe@1309:         } else {
bsw/jbe@1309:           partial = true;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:     }
bsw/jbe@1309:     
bsw/jbe@1309:     return {
bsw/jbe@1309:       nodes: nodes,
bsw/jbe@1309:       partial: partial
bsw/jbe@1309:     };
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Returns if caret is inside a word in textnode (not on boundary)
bsw/jbe@1309:   // If selection anchornode is not text node, returns false
bsw/jbe@1309:   function caretIsInsideWord(selection) {
bsw/jbe@1309:     var anchor, offset, beforeChar, afterChar;
bsw/jbe@1309:     if (selection) {
bsw/jbe@1309:       anchor = selection.anchorNode;
bsw/jbe@1309:       offset = selection.anchorOffset;
bsw/jbe@1309:       if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) {
bsw/jbe@1309:         beforeChar = anchor.data[offset - 1];
bsw/jbe@1309:         afterChar = anchor.data[offset];
bsw/jbe@1309:         return (/\w/).test(beforeChar) && (/\w/).test(afterChar);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Returns a range and textnode containing object from caret position covering a whole word
bsw/jbe@1309:   // wordOffsety describes the original position of caret in the new textNode 
bsw/jbe@1309:   // Caret has to be inside a textNode.
bsw/jbe@1309:   function getRangeForWord(selection) {
bsw/jbe@1309:     var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar,
bsw/jbe@1309:         txtNodes = [];
bsw/jbe@1309:     if (selection) {
bsw/jbe@1309:       anchor = selection.anchorNode;
bsw/jbe@1309:       offset = offsetStart = offsetEnd = selection.anchorOffset;
bsw/jbe@1309:       doc = anchor.ownerDocument;
bsw/jbe@1309:       range = rangy.createRange(doc);
bsw/jbe@1309: 
bsw/jbe@1309:       if (anchor && anchor.nodeType === 3) {
bsw/jbe@1309: 
bsw/jbe@1309:         while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) {
bsw/jbe@1309:           offsetStart--;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) {
bsw/jbe@1309:           offsetEnd++;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         range.setStartAndEnd(anchor, offsetStart, offsetEnd);
bsw/jbe@1309:         range.splitBoundaries();
bsw/jbe@1309:         txtNodes = range.getNodes([3], function(node) {
bsw/jbe@1309:           return (!wysihtml.dom.domNode(node).is.emptyTextNode());
bsw/jbe@1309:         });
bsw/jbe@1309: 
bsw/jbe@1309:         return {
bsw/jbe@1309:           wordOffset: offset - offsetStart,
bsw/jbe@1309:           range: range,
bsw/jbe@1309:           textNode: txtNodes[0]
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Contents of 2 elements are merged to fitst element. second element is removed as consequence
bsw/jbe@1309:   function mergeContents(element1, element2) {
bsw/jbe@1309:     while (element2.firstChild) {
bsw/jbe@1309:       element1.appendChild(element2.firstChild);
bsw/jbe@1309:     }
bsw/jbe@1309:     element2.parentNode.removeChild(element2);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function mergeConsequentSimilarElements(elements) {
bsw/jbe@1309:     for (var i = elements.length; i--;) {
bsw/jbe@1309:       
bsw/jbe@1309:       if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup
bsw/jbe@1309: 
bsw/jbe@1309:         if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) {
bsw/jbe@1309:           mergeContents(elements[i], elements[i].nextSibling);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (elements[i].previousSibling && isSameNode(elements[i]  , elements[i].previousSibling)) {
bsw/jbe@1309:           mergeContents(elements[i].previousSibling, elements[i]);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function cleanupAndSetSelection(composer, textNodes, options) {
bsw/jbe@1309:     if (textNodes.length > 0) {
bsw/jbe@1309:       selectTextNodes(textNodes, composer);
bsw/jbe@1309:     }
bsw/jbe@1309:     mergeConsequentSimilarElements(getState(composer, options).nodes);
bsw/jbe@1309:     if (textNodes.length > 0) {
bsw/jbe@1309:       selectTextNodes(textNodes, composer);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   function cleanupAndSetCaret(composer, textNode, offset, options) {
bsw/jbe@1309:     selectTextNode(composer, textNode, offset);
bsw/jbe@1309:     mergeConsequentSimilarElements(getState(composer, options).nodes);
bsw/jbe@1309:     selectTextNode(composer, textNode, offset);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Formats a textnode with given options
bsw/jbe@1309:   function formatTextNode(textNode, options) {
bsw/jbe@1309:     var wrapNode = createWrapNode(textNode, options);
bsw/jbe@1309: 
bsw/jbe@1309:     textNode.parentNode.insertBefore(wrapNode, textNode);
bsw/jbe@1309:     wrapNode.appendChild(textNode);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Changes/toggles format of a textnode
bsw/jbe@1309:   function unformatTextNode(textNode, composer, options) {
bsw/jbe@1309:     var container = composer.element,
bsw/jbe@1309:         wrapNode = findSimilarTextNodeWrapper(textNode, options, container),
bsw/jbe@1309:         newWrapNode;
bsw/jbe@1309: 
bsw/jbe@1309:     if (wrapNode) {
bsw/jbe@1309:       newWrapNode = wrapNode.cloneNode(false);
bsw/jbe@1309: 
bsw/jbe@1309:       wysihtml.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode);
bsw/jbe@1309:       updateFormatOfElement(newWrapNode, options);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Removes the format around textnode
bsw/jbe@1309:   function removeFormatFromTextNode(textNode, composer, options) {
bsw/jbe@1309:     var container = composer.element,
bsw/jbe@1309:         wrapNode = findSimilarTextNodeWrapper(textNode, options, container);
bsw/jbe@1309: 
bsw/jbe@1309:     if (wrapNode) {
bsw/jbe@1309:       wysihtml.dom.domNode(textNode).escapeParent(wrapNode);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Creates node around caret formated with options
bsw/jbe@1309:   function formatTextRange(range, composer, options) {
bsw/jbe@1309:     var wrapNode = createWrapNode(range.endContainer, options);
bsw/jbe@1309: 
bsw/jbe@1309:     range.surroundContents(wrapNode);
bsw/jbe@1309:     composer.selection.selectNode(wrapNode);
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Changes/toggles format of whole selection
bsw/jbe@1309:   function updateFormat(composer, textNodes, state, options) {
bsw/jbe@1309:     var exactState = getState(composer, options, true),
bsw/jbe@1309:         selection = composer.selection.getSelection(),
bsw/jbe@1309:         wordObj, textNode, newNode, i;
bsw/jbe@1309: 
bsw/jbe@1309:     if (!textNodes.length) {
bsw/jbe@1309:       // Selection is caret
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:       if (options.toggle !== false) {
bsw/jbe@1309:         if (caretIsInsideWord(selection)) {
bsw/jbe@1309: 
bsw/jbe@1309:           // Unformat whole word 
bsw/jbe@1309:           wordObj = getRangeForWord(selection);
bsw/jbe@1309:           textNode = wordObj.textNode;
bsw/jbe@1309:           unformatTextNode(wordObj.textNode, composer, options);
bsw/jbe@1309:           cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
bsw/jbe@1309: 
bsw/jbe@1309:         } else {
bsw/jbe@1309: 
bsw/jbe@1309:           // Escape caret out of format
bsw/jbe@1309:           textNode = composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309:           newNode = state.nodes[0].cloneNode(false);
bsw/jbe@1309:           newNode.appendChild(textNode);
bsw/jbe@1309:           composer.selection.splitElementAtCaret(state.nodes[0], newNode);
bsw/jbe@1309:           updateFormatOfElement(newNode, options);
bsw/jbe@1309:           cleanupAndSetSelection(composer, [textNode], options);
bsw/jbe@1309:           var s = composer.selection.getSelection();
bsw/jbe@1309:           if (s.anchorNode && s.focusNode) {
bsw/jbe@1309:             // Has an error in IE when collapsing selection. probably from rangy
bsw/jbe@1309:             try {
bsw/jbe@1309:               s.collapseToEnd();
bsw/jbe@1309:             } catch (e) {}
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       } else {
bsw/jbe@1309:         // In non-toggle mode the closest state element has to be found and the state updated differently
bsw/jbe@1309:         for (i = state.nodes.length; i--;) {
bsw/jbe@1309:           updateFormatOfElement(state.nodes[i], options);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:     } else {
bsw/jbe@1309: 
bsw/jbe@1309:       if (!exactState.partial && options.toggle !== false) {
bsw/jbe@1309: 
bsw/jbe@1309:         // If whole selection (all textnodes) are in the applied format
bsw/jbe@1309:         // remove the format from selection
bsw/jbe@1309:         // Non-toggle mode never removes. Remove has to be called explicitly
bsw/jbe@1309:         for (i = textNodes.length; i--;) {
bsw/jbe@1309:           unformatTextNode(textNodes[i], composer, options);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       } else {
bsw/jbe@1309:         
bsw/jbe@1309:         // Selection is partially in format
bsw/jbe@1309:         // change it to new if format if textnode allreafy in similar state
bsw/jbe@1309:         // else just apply
bsw/jbe@1309:         
bsw/jbe@1309:         for (i = textNodes.length; i--;) {
bsw/jbe@1309:           
bsw/jbe@1309:           if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
bsw/jbe@1309:             unformatTextNode(textNodes[i], composer, options);
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
bsw/jbe@1309:             formatTextNode(textNodes[i], options);
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       cleanupAndSetSelection(composer, textNodes, options);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Removes format from selection
bsw/jbe@1309:   function removeFormat(composer, textNodes, state, options) {
bsw/jbe@1309:     var textNode, textOffset, newNode, i,
bsw/jbe@1309:         selection = composer.selection.getSelection();
bsw/jbe@1309: 
bsw/jbe@1309:     if (!textNodes.length) {    
bsw/jbe@1309:       textNode = selection.anchorNode;
bsw/jbe@1309:       textOffset = selection.anchorOffset;
bsw/jbe@1309: 
bsw/jbe@1309:       for (i = state.nodes.length; i--;) {
bsw/jbe@1309:         wysihtml.dom.unwrap(state.nodes[i]);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       cleanupAndSetCaret(composer, textNode, textOffset, options);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       for (i = textNodes.length; i--;) {
bsw/jbe@1309:         removeFormatFromTextNode(textNodes[i], composer, options);
bsw/jbe@1309:       }
bsw/jbe@1309:       cleanupAndSetSelection(composer, textNodes, options);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   // Adds format to selection
bsw/jbe@1309:   function applyFormat(composer, textNodes, options) {
bsw/jbe@1309:     var wordObj, i,
bsw/jbe@1309:         selection = composer.selection.getSelection();
bsw/jbe@1309:  
bsw/jbe@1309:     if (!textNodes.length) {
bsw/jbe@1309:       // Handle collapsed selection caret and return
bsw/jbe@1309:       if (caretIsInsideWord(selection)) {
bsw/jbe@1309: 
bsw/jbe@1309:         wordObj = getRangeForWord(selection);
bsw/jbe@1309:         formatTextNode(wordObj.textNode, options);
bsw/jbe@1309:         cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
bsw/jbe@1309: 
bsw/jbe@1309:       } else {
bsw/jbe@1309:         var r = composer.selection.getOwnRanges()[0];
bsw/jbe@1309:         if (r) {
bsw/jbe@1309:           formatTextRange(r, composer, options);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       
bsw/jbe@1309:     } else {
bsw/jbe@1309:       // Handle textnodes in selection and apply format
bsw/jbe@1309:       for (i = textNodes.length; i--;) {
bsw/jbe@1309:         formatTextNode(textNodes[i], options);
bsw/jbe@1309:       }
bsw/jbe@1309:       cleanupAndSetSelection(composer, textNodes, options);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309:   
bsw/jbe@1309:   // If properties is passed as a string, correct options with that nodeName
bsw/jbe@1309:   function fixOptions(options) {
bsw/jbe@1309:     options = (typeof options === "string") ? { nodeName: options } : options;
bsw/jbe@1309:     if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); }
bsw/jbe@1309:     return options;
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.commands.formatInline = {
bsw/jbe@1309: 
bsw/jbe@1309:     // Basics:
bsw/jbe@1309:     // In case of plain text or inline state not set wrap all non-empty textnodes with
bsw/jbe@1309:     // 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)
bsw/jbe@1309:     //    In case of changing mode every textnode is addressed separatly
bsw/jbe@1309:     exec: function(composer, command, options) {
bsw/jbe@1309:       options = fixOptions(options);
bsw/jbe@1309: 
bsw/jbe@1309:       // Join adjactent textnodes first
bsw/jbe@1309:       composer.element.normalize();
bsw/jbe@1309: 
bsw/jbe@1309:       var textNodes = getSelectedTextNodes(composer.selection, true),
bsw/jbe@1309:           state = getState(composer, options);
bsw/jbe@1309:       if (state.nodes.length > 0) {
bsw/jbe@1309:         // Text allready has the format applied
bsw/jbe@1309:         updateFormat(composer, textNodes, state, options);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         // Selection is not in the applied format
bsw/jbe@1309:         applyFormat(composer, textNodes, options);
bsw/jbe@1309:       }
bsw/jbe@1309:       composer.element.normalize();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     remove: function(composer, command, options) {
bsw/jbe@1309:       options = fixOptions(options);
bsw/jbe@1309:       composer.element.normalize();
bsw/jbe@1309: 
bsw/jbe@1309:       var textNodes = getSelectedTextNodes(composer.selection, true),
bsw/jbe@1309:           state = getState(composer, options);
bsw/jbe@1309: 
bsw/jbe@1309:       if (state.nodes.length > 0) {
bsw/jbe@1309:         // Text allready has the format applied
bsw/jbe@1309:         removeFormat(composer, textNodes, state, options);
bsw/jbe@1309:       }
bsw/jbe@1309:       
bsw/jbe@1309:       composer.element.normalize();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer, command, options) {
bsw/jbe@1309:       options = fixOptions(options);
bsw/jbe@1309:       var nodes = getState(composer, options, true).nodes;
bsw/jbe@1309:       return (nodes.length === 0) ? false : nodes;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml){
bsw/jbe@1309:   wysihtml.commands.indentList = {
bsw/jbe@1309:     exec: function(composer, command, value) {
bsw/jbe@1309:       var listEls = composer.selection.getSelectionParentsByTag('LI');
bsw/jbe@1309:       if (listEls) {
bsw/jbe@1309:         return this.tryToPushLiLevel(listEls, composer.selection);
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer, command) {
bsw/jbe@1309:         return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     tryToPushLiLevel: function(liNodes, selection) {
bsw/jbe@1309:       var listTag, list, prevLi, liNode, prevLiList,
bsw/jbe@1309:           found = false;
bsw/jbe@1309: 
bsw/jbe@1309:       selection.executeAndRestoreRangy(function() {
bsw/jbe@1309: 
bsw/jbe@1309:         for (var i = liNodes.length; i--;) {
bsw/jbe@1309:           liNode = liNodes[i];
bsw/jbe@1309:           listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL';
bsw/jbe@1309:           list = liNode.ownerDocument.createElement(listTag);
bsw/jbe@1309:           prevLi = wysihtml.dom.domNode(liNode).prev({nodeTypes: [wysihtml.ELEMENT_NODE]});
bsw/jbe@1309:           prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null;
bsw/jbe@1309: 
bsw/jbe@1309:           if (prevLi) {
bsw/jbe@1309:             if (prevLiList) {
bsw/jbe@1309:               prevLiList.appendChild(liNode);
bsw/jbe@1309:             } else {
bsw/jbe@1309:               list.appendChild(liNode);
bsw/jbe@1309:               prevLi.appendChild(list);
bsw/jbe@1309:             }
bsw/jbe@1309:             found = true;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       });
bsw/jbe@1309:       return found;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: }(wysihtml));
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml){
bsw/jbe@1309:   wysihtml.commands.insertHTML = {
bsw/jbe@1309:     exec: function(composer, command, html) {
bsw/jbe@1309:         composer.selection.insertHTML(html);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function() {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: }(wysihtml));
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var LINE_BREAK = "
" + (wysihtml.browser.needsSpaceAfterLineBreak() ? " " : "");
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.commands.insertLineBreak = {
bsw/jbe@1309:     exec: function(composer, command) {
bsw/jbe@1309:       composer.selection.insertHTML(LINE_BREAK);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function() {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.commands.insertList = (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   var isNode = function(node, name) {
bsw/jbe@1309:     if (node && node.nodeName) {
bsw/jbe@1309:       if (typeof name === 'string') {
bsw/jbe@1309:         name = [name];
bsw/jbe@1309:       }
bsw/jbe@1309:       for (var n = name.length; n--;) {
bsw/jbe@1309:         if (node.nodeName === name[n]) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:     return false;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var findListEl = function(node, nodeName, composer) {
bsw/jbe@1309:     var ret = {
bsw/jbe@1309:           el: null,
bsw/jbe@1309:           other: false
bsw/jbe@1309:         };
bsw/jbe@1309: 
bsw/jbe@1309:     if (node) {
bsw/jbe@1309:       var parentLi = wysihtml.dom.getParentElement(node, { query: "li" }, false, composer.element),
bsw/jbe@1309:           otherNodeName = (nodeName === "UL") ? "OL" : "UL";
bsw/jbe@1309: 
bsw/jbe@1309:       if (isNode(node, nodeName)) {
bsw/jbe@1309:         ret.el = node;
bsw/jbe@1309:       } else if (isNode(node, otherNodeName)) {
bsw/jbe@1309:         ret = {
bsw/jbe@1309:           el: node,
bsw/jbe@1309:           other: true
bsw/jbe@1309:         };
bsw/jbe@1309:       } else if (parentLi) {
bsw/jbe@1309:         if (isNode(parentLi.parentNode, nodeName)) {
bsw/jbe@1309:           ret.el = parentLi.parentNode;
bsw/jbe@1309:         } else if (isNode(parentLi.parentNode, otherNodeName)) {
bsw/jbe@1309:           ret = {
bsw/jbe@1309:             el : parentLi.parentNode,
bsw/jbe@1309:             other: true
bsw/jbe@1309:           };
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // do not count list elements outside of composer
bsw/jbe@1309:     if (ret.el && !composer.element.contains(ret.el)) {
bsw/jbe@1309:       ret.el = null;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     return ret;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleSameTypeList = function(el, nodeName, composer) {
bsw/jbe@1309:     var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
bsw/jbe@1309:         otherLists, innerLists;
bsw/jbe@1309:     // Unwrap list
bsw/jbe@1309:     // - foo
- bar
bsw/jbe@1309:     // becomes:
bsw/jbe@1309:     // foo
bar
bsw/jbe@1309: 
bsw/jbe@1309:     composer.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309:       otherLists = getListsInSelection(otherNodeName, composer);
bsw/jbe@1309:       if (otherLists.length) {
bsw/jbe@1309:         for (var l = otherLists.length; l--;) {
bsw/jbe@1309:           wysihtml.dom.renameElement(otherLists[l], nodeName.toLowerCase());
bsw/jbe@1309:         }
bsw/jbe@1309:       } else {
bsw/jbe@1309:         innerLists = getListsInSelection(['OL', 'UL'], composer);
bsw/jbe@1309:         for (var i = innerLists.length; i--;) {
bsw/jbe@1309:           wysihtml.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
bsw/jbe@1309:         }
bsw/jbe@1309:         if (innerLists.length === 0) {
bsw/jbe@1309:           wysihtml.dom.resolveList(el, composer.config.useLineBreaks);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     });
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleOtherTypeList =  function(el, nodeName, composer) {
bsw/jbe@1309:     var otherNodeName = (nodeName === "UL") ? "OL" : "UL";
bsw/jbe@1309:     // Turn an ordered list into an unordered list
bsw/jbe@1309:     // - foo
- bar
bsw/jbe@1309:     // becomes:
bsw/jbe@1309:     //- foo
- bar
bsw/jbe@1309:     // Also rename other lists in selection
bsw/jbe@1309:     composer.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309:       var renameLists = [el].concat(getListsInSelection(otherNodeName, composer));
bsw/jbe@1309: 
bsw/jbe@1309:       // All selection inner lists get renamed too
bsw/jbe@1309:       for (var l = renameLists.length; l--;) {
bsw/jbe@1309:         wysihtml.dom.renameElement(renameLists[l], nodeName.toLowerCase());
bsw/jbe@1309:       }
bsw/jbe@1309:     });
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var getListsInSelection = function(nodeName, composer) {
bsw/jbe@1309:       var ranges = composer.selection.getOwnRanges(),
bsw/jbe@1309:           renameLists = [];
bsw/jbe@1309: 
bsw/jbe@1309:       for (var r = ranges.length; r--;) {
bsw/jbe@1309:         renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
bsw/jbe@1309:           return isNode(node, nodeName);
bsw/jbe@1309:         }));
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       return renameLists;
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var createListFallback = function(nodeName, composer) {
bsw/jbe@1309:     var sel = rangy.saveSelection(composer.win);
bsw/jbe@1309: 
bsw/jbe@1309:     // Fallback for Create list
bsw/jbe@1309:     var tempClassName =  "_wysihtml-temp-" + new Date().getTime(),
bsw/jbe@1309:         isEmpty, list;
bsw/jbe@1309: 
bsw/jbe@1309:     composer.commands.exec("formatBlock", {
bsw/jbe@1309:       "nodeName": "div",
bsw/jbe@1309:       "className": tempClassName
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     var tempElement = composer.element.querySelector("." + tempClassName);
bsw/jbe@1309: 
bsw/jbe@1309:     // This space causes new lists to never break on enter
bsw/jbe@1309:     var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
bsw/jbe@1309:     tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309:     if (tempElement) {
bsw/jbe@1309:       isEmpty = (/^(\s|(
))+$/i).test(tempElement.innerHTML);
bsw/jbe@1309:       list = wysihtml.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer);
bsw/jbe@1309:       if (sel) {
bsw/jbe@1309:         rangy.restoreSelection(sel);
bsw/jbe@1309:       }
bsw/jbe@1309:       if (isEmpty) {
bsw/jbe@1309:         composer.selection.selectNode(list.querySelector("li"), true);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   return {
bsw/jbe@1309:     exec: function(composer, command, nodeName) {
bsw/jbe@1309:       var doc           = composer.doc,
bsw/jbe@1309:           cmd           = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList",
bsw/jbe@1309:           s = composer.selection.getSelection(),
bsw/jbe@1309:           anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
bsw/jbe@1309:           fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode,
bsw/jbe@1309:           selectedNode, list;
bsw/jbe@1309: 
bsw/jbe@1309:       if (s.isBackwards()) {
bsw/jbe@1309:         // swap variables
bsw/jbe@1309:         anode = [fnode, fnode = anode][0];
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (wysihtml.dom.domNode(fnode).is.emptyTextNode(true) && fnode) {
bsw/jbe@1309:         fnode = wysihtml.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:       }
bsw/jbe@1309:       if (wysihtml.dom.domNode(anode).is.emptyTextNode(true) && anode) {
bsw/jbe@1309:         anode = wysihtml.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (anode && fnode) {
bsw/jbe@1309:         if (anode === fnode) {
bsw/jbe@1309:           selectedNode = anode;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           selectedNode = wysihtml.dom.domNode(anode).commonAncestor(fnode, composer.element);
bsw/jbe@1309:         }
bsw/jbe@1309:       } else {
bsw/jbe@1309:         selectedNode  = composer.selection.getSelectedNode();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       list = findListEl(selectedNode, nodeName, composer);
bsw/jbe@1309: 
bsw/jbe@1309:       if (!list.el) {
bsw/jbe@1309:         if (composer.commands.support(cmd)) {
bsw/jbe@1309:           doc.execCommand(cmd, false, null);
bsw/jbe@1309:         } else {
bsw/jbe@1309:           createListFallback(nodeName, composer);
bsw/jbe@1309:         }
bsw/jbe@1309:       } else if (list.other) {
bsw/jbe@1309:         handleOtherTypeList(list.el, nodeName, composer);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         handleSameTypeList(list.el, nodeName, composer);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer, command, nodeName) {
bsw/jbe@1309:       var selectedNode = composer.selection.getSelectedNode(),
bsw/jbe@1309:           list         = findListEl(selectedNode, nodeName, composer);
bsw/jbe@1309: 
bsw/jbe@1309:       return (list.el && !list.other) ? list.el : false;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml){
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.commands.outdentList = {
bsw/jbe@1309:     exec: function(composer, command, value) {
bsw/jbe@1309:       var listEls = composer.selection.getSelectionParentsByTag('LI');
bsw/jbe@1309:       if (listEls) {
bsw/jbe@1309:         return this.tryToPullLiLevel(listEls, composer);
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer, command) {
bsw/jbe@1309:         return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     tryToPullLiLevel: function(liNodes, composer) {
bsw/jbe@1309:       var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
bsw/jbe@1309:           found = false,
bsw/jbe@1309:           that = this;
bsw/jbe@1309: 
bsw/jbe@1309:       composer.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309: 
bsw/jbe@1309:         for (var i = liNodes.length; i--;) {
bsw/jbe@1309:           liNode = liNodes[i];
bsw/jbe@1309:           if (liNode.parentNode) {
bsw/jbe@1309:             listNode = liNode.parentNode;
bsw/jbe@1309: 
bsw/jbe@1309:             if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
bsw/jbe@1309:               found = true;
bsw/jbe@1309: 
bsw/jbe@1309:               outerListNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element);
bsw/jbe@1309:               outerLiNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element);
bsw/jbe@1309: 
bsw/jbe@1309:               if (outerListNode && outerLiNode) {
bsw/jbe@1309: 
bsw/jbe@1309:                 if (liNode.nextSibling) {
bsw/jbe@1309:                   afterList = that.getAfterList(listNode, liNode);
bsw/jbe@1309:                   liNode.appendChild(afterList);
bsw/jbe@1309:                 }
bsw/jbe@1309:                 outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
bsw/jbe@1309: 
bsw/jbe@1309:               } else {
bsw/jbe@1309: 
bsw/jbe@1309:                 if (liNode.nextSibling) {
bsw/jbe@1309:                   afterList = that.getAfterList(listNode, liNode);
bsw/jbe@1309:                   liNode.appendChild(afterList);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 for (var j = liNode.childNodes.length; j--;) {
bsw/jbe@1309:                   listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
bsw/jbe@1309:                 }
bsw/jbe@1309: 
bsw/jbe@1309:                 listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
bsw/jbe@1309:                 liNode.parentNode.removeChild(liNode);
bsw/jbe@1309: 
bsw/jbe@1309:               }
bsw/jbe@1309: 
bsw/jbe@1309:               // cleanup
bsw/jbe@1309:               if (listNode.childNodes.length === 0) {
bsw/jbe@1309:                   listNode.parentNode.removeChild(listNode);
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:       });
bsw/jbe@1309:       return found;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getAfterList: function(listNode, liNode) {
bsw/jbe@1309:       var nodeName = listNode.nodeName,
bsw/jbe@1309:           newList = document.createElement(nodeName);
bsw/jbe@1309: 
bsw/jbe@1309:       while (liNode.nextSibling) {
bsw/jbe@1309:         newList.appendChild(liNode.nextSibling);
bsw/jbe@1309:       }
bsw/jbe@1309:       return newList;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:   };
bsw/jbe@1309: }(wysihtml));
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml){
bsw/jbe@1309:   wysihtml.commands.redo = {
bsw/jbe@1309:     exec: function(composer) {
bsw/jbe@1309:       return composer.undoManager.redo();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer) {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: }(wysihtml));
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   var nodeOptions = {
bsw/jbe@1309:     nodeName: "A"
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.commands.removeLink = {
bsw/jbe@1309:     exec: function(composer, command) {
bsw/jbe@1309:       wysihtml.commands.formatInline.remove(composer, command, nodeOptions);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer, command) {
bsw/jbe@1309:       return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml){
bsw/jbe@1309:   wysihtml.commands.undo = {
bsw/jbe@1309:     exec: function(composer) {
bsw/jbe@1309:       return composer.undoManager.undo();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     state: function(composer) {
bsw/jbe@1309:       return false;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: }(wysihtml));
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Undo Manager for wysihtml
bsw/jbe@1309:  * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var Z_KEY               = 90,
bsw/jbe@1309:       Y_KEY               = 89,
bsw/jbe@1309:       BACKSPACE_KEY       = 8,
bsw/jbe@1309:       DELETE_KEY          = 46,
bsw/jbe@1309:       MAX_HISTORY_ENTRIES = 25,
bsw/jbe@1309:       DATA_ATTR_NODE      = "data-wysihtml-selection-node",
bsw/jbe@1309:       DATA_ATTR_OFFSET    = "data-wysihtml-selection-offset",
bsw/jbe@1309:       UNDO_HTML           = '' + wysihtml.INVISIBLE_SPACE + '',
bsw/jbe@1309:       REDO_HTML           = '' + wysihtml.INVISIBLE_SPACE + '',
bsw/jbe@1309:       dom                 = wysihtml.dom;
bsw/jbe@1309: 
bsw/jbe@1309:   function cleanTempElements(doc) {
bsw/jbe@1309:     var tempElement;
bsw/jbe@1309:     while (tempElement = doc.querySelector("._wysihtml-temp")) {
bsw/jbe@1309:       tempElement.parentNode.removeChild(tempElement);
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.UndoManager = wysihtml.lang.Dispatcher.extend(
bsw/jbe@1309:     /** @scope wysihtml.UndoManager.prototype */ {
bsw/jbe@1309:     constructor: function(editor) {
bsw/jbe@1309:       this.editor = editor;
bsw/jbe@1309:       this.composer = editor.composer;
bsw/jbe@1309:       this.element = this.composer.element;
bsw/jbe@1309: 
bsw/jbe@1309:       this.position = 0;
bsw/jbe@1309:       this.historyStr = [];
bsw/jbe@1309:       this.historyDom = [];
bsw/jbe@1309: 
bsw/jbe@1309:       this.transact();
bsw/jbe@1309: 
bsw/jbe@1309:       this._observe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _observe: function() {
bsw/jbe@1309:       var that      = this,
bsw/jbe@1309:           doc       = this.composer.sandbox.getDocument(),
bsw/jbe@1309:           lastKey;
bsw/jbe@1309: 
bsw/jbe@1309:       // Catch CTRL+Z and CTRL+Y
bsw/jbe@1309:       dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309:         if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
bsw/jbe@1309:           return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var keyCode = event.keyCode,
bsw/jbe@1309:             isUndo = keyCode === Z_KEY && !event.shiftKey,
bsw/jbe@1309:             isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
bsw/jbe@1309: 
bsw/jbe@1309:         if (isUndo) {
bsw/jbe@1309:           that.undo();
bsw/jbe@1309:           event.preventDefault();
bsw/jbe@1309:         } else if (isRedo) {
bsw/jbe@1309:           that.redo();
bsw/jbe@1309:           event.preventDefault();
bsw/jbe@1309:         }
bsw/jbe@1309:       });
bsw/jbe@1309: 
bsw/jbe@1309:       // Catch delete and backspace
bsw/jbe@1309:       dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309:         var keyCode = event.keyCode;
bsw/jbe@1309:         if (keyCode === lastKey) {
bsw/jbe@1309:           return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         lastKey = keyCode;
bsw/jbe@1309: 
bsw/jbe@1309:         if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
bsw/jbe@1309:           that.transact();
bsw/jbe@1309:         }
bsw/jbe@1309:       });
bsw/jbe@1309: 
bsw/jbe@1309:       this.editor
bsw/jbe@1309:         .on("newword:composer", function() {
bsw/jbe@1309:           that.transact();
bsw/jbe@1309:         })
bsw/jbe@1309: 
bsw/jbe@1309:         .on("beforecommand:composer", function() {
bsw/jbe@1309:           that.transact();
bsw/jbe@1309:         });
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     transact: function() {
bsw/jbe@1309:       var previousHtml      = this.historyStr[this.position - 1],
bsw/jbe@1309:           currentHtml       = this.composer.getValue(false, false),
bsw/jbe@1309:           composerIsVisible   = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
bsw/jbe@1309:           range, node, offset, element, position;
bsw/jbe@1309: 
bsw/jbe@1309:       if (currentHtml === previousHtml) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var length = this.historyStr.length = this.historyDom.length = this.position;
bsw/jbe@1309:       if (length > MAX_HISTORY_ENTRIES) {
bsw/jbe@1309:         this.historyStr.shift();
bsw/jbe@1309:         this.historyDom.shift();
bsw/jbe@1309:         this.position--;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.position++;
bsw/jbe@1309: 
bsw/jbe@1309:       if (composerIsVisible) {
bsw/jbe@1309:         // Do not start saving selection if composer is not visible
bsw/jbe@1309:         range   = this.composer.selection.getRange();
bsw/jbe@1309:         node    = (range && range.startContainer) ? range.startContainer : this.element;
bsw/jbe@1309:         offset  = (range && range.startOffset) ? range.startOffset : 0;
bsw/jbe@1309: 
bsw/jbe@1309:         if (node.nodeType === wysihtml.ELEMENT_NODE) {
bsw/jbe@1309:           element = node;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           element  = node.parentNode;
bsw/jbe@1309:           position = this.getChildNodeIndex(element, node);
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         element.setAttribute(DATA_ATTR_OFFSET, offset);
bsw/jbe@1309:         if (typeof(position) !== "undefined") {
bsw/jbe@1309:           element.setAttribute(DATA_ATTR_NODE, position);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       var clone = this.element.cloneNode(!!currentHtml);
bsw/jbe@1309:       this.historyDom.push(clone);
bsw/jbe@1309:       this.historyStr.push(currentHtml);
bsw/jbe@1309: 
bsw/jbe@1309:       if (element) {
bsw/jbe@1309:         element.removeAttribute(DATA_ATTR_OFFSET);
bsw/jbe@1309:         element.removeAttribute(DATA_ATTR_NODE);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     undo: function() {
bsw/jbe@1309:       this.transact();
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.undoPossible()) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.set(this.historyDom[--this.position - 1]);
bsw/jbe@1309:       this.editor.fire("undo:composer");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     redo: function() {
bsw/jbe@1309:       if (!this.redoPossible()) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.set(this.historyDom[++this.position - 1]);
bsw/jbe@1309:       this.editor.fire("redo:composer");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     undoPossible: function() {
bsw/jbe@1309:       return this.position > 1;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     redoPossible: function() {
bsw/jbe@1309:       return this.position < this.historyStr.length;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     set: function(historyEntry) {
bsw/jbe@1309:       this.element.innerHTML = "";
bsw/jbe@1309: 
bsw/jbe@1309:       var i = 0,
bsw/jbe@1309:           childNodes = historyEntry.childNodes,
bsw/jbe@1309:           length = historyEntry.childNodes.length;
bsw/jbe@1309: 
bsw/jbe@1309:       for (; i";
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getValue: function(parse, clearInternals) {
bsw/jbe@1309:       var value = this.isEmpty() ? "" : wysihtml.quirks.getCorrectInnerHTML(this.element);
bsw/jbe@1309:       if (parse !== false) {
bsw/jbe@1309:         value = this.parent.parse(value, (clearInternals === false) ? false : true);
bsw/jbe@1309:       }
bsw/jbe@1309:       return value;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     setValue: function(html, parse) {
bsw/jbe@1309:       if (parse !== false) {
bsw/jbe@1309:         html = this.parent.parse(html);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       try {
bsw/jbe@1309:         this.element.innerHTML = html;
bsw/jbe@1309:       } catch (e) {
bsw/jbe@1309:         this.element.innerText = html;
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     cleanUp: function(rules) {
bsw/jbe@1309:       var bookmark;
bsw/jbe@1309:       if (this.selection && this.selection.isInThisEditable()) {
bsw/jbe@1309:         bookmark = rangy.saveSelection(this.win);
bsw/jbe@1309:       }
bsw/jbe@1309:       this.parent.parse(this.element, undefined, rules);
bsw/jbe@1309:       if (bookmark) {
bsw/jbe@1309:         rangy.restoreSelection(bookmark);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     show: function() {
bsw/jbe@1309:       this.editableArea.style.display = this._displayStyle || "";
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.config.noTextarea && !this.textarea.element.disabled) {
bsw/jbe@1309:         // Firefox needs this, otherwise contentEditable becomes uneditable
bsw/jbe@1309:         this.disable();
bsw/jbe@1309:         this.enable();
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     hide: function() {
bsw/jbe@1309:       this._displayStyle = dom.getStyle("display").from(this.editableArea);
bsw/jbe@1309:       if (this._displayStyle === "none") {
bsw/jbe@1309:         this._displayStyle = null;
bsw/jbe@1309:       }
bsw/jbe@1309:       this.editableArea.style.display = "none";
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     disable: function() {
bsw/jbe@1309:       this.parent.fire("disable:composer");
bsw/jbe@1309:       this.element.removeAttribute("contentEditable");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     enable: function() {
bsw/jbe@1309:       this.parent.fire("enable:composer");
bsw/jbe@1309:       this.element.setAttribute("contentEditable", "true");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     focus: function(setToEnd) {
bsw/jbe@1309:       // IE 8 fires the focus event after .focus()
bsw/jbe@1309:       // This is needed by our simulate_placeholder.js to work
bsw/jbe@1309:       // therefore we clear it ourselves this time
bsw/jbe@1309:       if (wysihtml.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
bsw/jbe@1309:         this.clear();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.base();
bsw/jbe@1309: 
bsw/jbe@1309:       var lastChild = this.element.lastChild;
bsw/jbe@1309:       if (setToEnd && lastChild && this.selection) {
bsw/jbe@1309:         if (lastChild.nodeName === "BR") {
bsw/jbe@1309:           this.selection.setBefore(this.element.lastChild);
bsw/jbe@1309:         } else {
bsw/jbe@1309:           this.selection.setAfter(this.element.lastChild);
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getScrollPos: function() {
bsw/jbe@1309:       if (this.doc && this.win) {
bsw/jbe@1309:         var pos = {};
bsw/jbe@1309: 
bsw/jbe@1309:         if (typeof this.win.pageYOffset !== "undefined") {
bsw/jbe@1309:           pos.y = this.win.pageYOffset;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           pos.y = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollTop;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (typeof this.win.pageXOffset !== "undefined") {
bsw/jbe@1309:           pos.x = this.win.pageXOffset;
bsw/jbe@1309:         } else {
bsw/jbe@1309:           pos.x = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollLeft;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         return pos;
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     setScrollPos: function(pos) {
bsw/jbe@1309:       if (pos && typeof pos.x !== "undefined" && typeof pos.y !== "undefined") {
bsw/jbe@1309:         this.win.scrollTo(pos.x, pos.y);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getTextContent: function() {
bsw/jbe@1309:       return dom.getTextContent(this.element);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     hasPlaceholderSet: function() {
bsw/jbe@1309:       return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     isEmpty: function() {
bsw/jbe@1309:       var innerHTML = this.element.innerHTML.toLowerCase();
bsw/jbe@1309:       return (/^(\s|
|<\/br>||<\/p>)*$/i).test(innerHTML)  ||
bsw/jbe@1309:              innerHTML === ""            ||
bsw/jbe@1309:              innerHTML === "
"        ||
bsw/jbe@1309:              innerHTML === "
"     ||
bsw/jbe@1309:              innerHTML === "
" ||
bsw/jbe@1309:              this.hasPlaceholderSet();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _initContentEditableArea: function() {
bsw/jbe@1309:         var that = this;
bsw/jbe@1309:         if (this.config.noTextarea) {
bsw/jbe@1309:             this.sandbox = new dom.ContentEditableArea(function() {
bsw/jbe@1309:                 that._create();
bsw/jbe@1309:             }, {
bsw/jbe@1309:               className: this.config.classNames.sandbox
bsw/jbe@1309:             }, this.editableArea);
bsw/jbe@1309:         } else {
bsw/jbe@1309:             this.sandbox = new dom.ContentEditableArea(function() {
bsw/jbe@1309:                 that._create();
bsw/jbe@1309:             }, {
bsw/jbe@1309:               className: this.config.classNames.sandbox
bsw/jbe@1309:             });
bsw/jbe@1309:             this.editableArea = this.sandbox.getContentEditable();
bsw/jbe@1309:             dom.insert(this.editableArea).after(this.textarea.element);
bsw/jbe@1309:             this._createWysiwygFormField();
bsw/jbe@1309:         }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _initSandbox: function() {
bsw/jbe@1309:       var that = this;
bsw/jbe@1309:       this.sandbox = new dom.Sandbox(function() {
bsw/jbe@1309:         that._create();
bsw/jbe@1309:       }, {
bsw/jbe@1309:         stylesheets:  this.config.stylesheets,
bsw/jbe@1309:         className: this.config.classNames.sandbox
bsw/jbe@1309:       });
bsw/jbe@1309:       this.editableArea  = this.sandbox.getIframe();
bsw/jbe@1309: 
bsw/jbe@1309:       var textareaElement = this.textarea.element;
bsw/jbe@1309:       dom.insert(this.editableArea).after(textareaElement);
bsw/jbe@1309: 
bsw/jbe@1309:       this._createWysiwygFormField();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor
bsw/jbe@1309:     _createWysiwygFormField: function() {
bsw/jbe@1309:         if (this.textarea.element.form) {
bsw/jbe@1309:           var hiddenField = document.createElement("input");
bsw/jbe@1309:           hiddenField.type   = "hidden";
bsw/jbe@1309:           hiddenField.name   = "_wysihtml_mode";
bsw/jbe@1309:           hiddenField.value  = 1;
bsw/jbe@1309:           dom.insert(hiddenField).after(this.textarea.element);
bsw/jbe@1309:         }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _create: function() {
bsw/jbe@1309:       var that = this;
bsw/jbe@1309:       this.doc                = this.sandbox.getDocument();
bsw/jbe@1309:       this.win                = this.sandbox.getWindow();
bsw/jbe@1309:       this.element            = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body;
bsw/jbe@1309:       if (!this.config.noTextarea) {
bsw/jbe@1309:           this.textarea           = this.parent.textarea;
bsw/jbe@1309:           this.element.innerHTML  = this.textarea.getValue(true, false);
bsw/jbe@1309:       } else {
bsw/jbe@1309:           this.cleanUp(); // cleans contenteditable on initiation as it may contain html
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Make sure our selection handler is ready
bsw/jbe@1309:       this.selection = new wysihtml.Selection(this.parent, this.element, this.config.classNames.uneditableContainer);
bsw/jbe@1309: 
bsw/jbe@1309:       // Make sure commands dispatcher is ready
bsw/jbe@1309:       this.commands  = new wysihtml.Commands(this.parent);
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.config.noTextarea) {
bsw/jbe@1309:           dom.copyAttributes([
bsw/jbe@1309:               "className", "spellcheck", "title", "lang", "dir", "accessKey"
bsw/jbe@1309:           ]).from(this.textarea.element).to(this.element);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this._initAutoLinking();
bsw/jbe@1309: 
bsw/jbe@1309:       dom.addClass(this.element, this.config.classNames.composer);
bsw/jbe@1309:       //
bsw/jbe@1309:       // Make the editor look like the original textarea, by syncing styles
bsw/jbe@1309:       if (this.config.style && !this.config.contentEditableMode) {
bsw/jbe@1309:         this.style();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.observe();
bsw/jbe@1309: 
bsw/jbe@1309:       var name = this.config.name;
bsw/jbe@1309:       if (name) {
bsw/jbe@1309:         dom.addClass(this.element, name);
bsw/jbe@1309:         if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.enable();
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.config.noTextarea && this.textarea.element.disabled) {
bsw/jbe@1309:         this.disable();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Simulate html5 placeholder attribute on contentEditable element
bsw/jbe@1309:       var placeholderText = typeof(this.config.placeholder) === "string"
bsw/jbe@1309:         ? this.config.placeholder
bsw/jbe@1309:         : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder"));
bsw/jbe@1309:       if (placeholderText) {
bsw/jbe@1309:         dom.simulatePlaceholder(this.parent, this, placeholderText, this.config.classNames.placeholder);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Make sure that the browser avoids using inline styles whenever possible
bsw/jbe@1309:       this.commands.exec("styleWithCSS", false);
bsw/jbe@1309: 
bsw/jbe@1309:       this._initObjectResizing();
bsw/jbe@1309:       this._initUndoManager();
bsw/jbe@1309:       this._initLineBreaking();
bsw/jbe@1309: 
bsw/jbe@1309:       // Simulate html5 autofocus on contentEditable element
bsw/jbe@1309:       // This doesn't work on IOS (5.1.1)
bsw/jbe@1309:       if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
bsw/jbe@1309:         setTimeout(function() { that.focus(true); }, 100);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // IE sometimes leaves a single paragraph, which can't be removed by the user
bsw/jbe@1309:       if (!browser.clearsContentEditableCorrectly()) {
bsw/jbe@1309:         wysihtml.quirks.ensureProperClearing(this);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Set up a sync that makes sure that textarea and editor have the same content
bsw/jbe@1309:       if (this.initSync && this.config.sync) {
bsw/jbe@1309:         this.initSync();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Okay hide the textarea, we are ready to go
bsw/jbe@1309:       if (!this.config.noTextarea) { this.textarea.hide(); }
bsw/jbe@1309: 
bsw/jbe@1309:       // Fire global (before-)load event
bsw/jbe@1309:       this.parent.fire("beforeload").fire("load");
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _initAutoLinking: function() {
bsw/jbe@1309:       var that                           = this,
bsw/jbe@1309:           supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
bsw/jbe@1309:           supportsAutoLinking            = browser.doesAutoLinkingInContentEditable();
bsw/jbe@1309: 
bsw/jbe@1309:       if (supportsDisablingOfAutoLinking) {
bsw/jbe@1309:         this.commands.exec("AutoUrlDetect", false, false);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.config.autoLink) {
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Only do the auto linking by ourselves when the browser doesn't support auto linking
bsw/jbe@1309:       // OR when he supports auto linking but we were able to turn it off (IE9+)
bsw/jbe@1309:       if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
bsw/jbe@1309:         this.parent.on("newword:composer", function() {
bsw/jbe@1309:           if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
bsw/jbe@1309:             var nodeWithSelection = that.selection.getSelectedNode(),
bsw/jbe@1309:                 uneditables = that.element.querySelectorAll("." + that.config.classNames.uneditableContainer),
bsw/jbe@1309:                 isInUneditable = false;
bsw/jbe@1309: 
bsw/jbe@1309:             for (var i = uneditables.length; i--;) {
bsw/jbe@1309:               if (wysihtml.dom.contains(uneditables[i], nodeWithSelection)) {
bsw/jbe@1309:                 isInUneditable = true;
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (!isInUneditable) dom.autoLink(nodeWithSelection, [that.config.classNames.uneditableContainer]);
bsw/jbe@1309:           }
bsw/jbe@1309:         });
bsw/jbe@1309: 
bsw/jbe@1309:         dom.observe(this.element, "blur", function() {
bsw/jbe@1309:           dom.autoLink(that.element, [that.config.classNames.uneditableContainer]);
bsw/jbe@1309:         });
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Assuming we have the following:
bsw/jbe@1309:       //  http://www.google.de
bsw/jbe@1309:       // If a user now changes the url in the innerHTML we want to make sure that
bsw/jbe@1309:       // it's synchronized with the href attribute (as long as the innerHTML is still a url)
bsw/jbe@1309:       var // Use a live NodeList to check whether there are any links in the document
bsw/jbe@1309:           links           = this.sandbox.getDocument().getElementsByTagName("a"),
bsw/jbe@1309:           // The autoLink helper method reveals a reg exp to detect correct urls
bsw/jbe@1309:           urlRegExp       = dom.autoLink.URL_REG_EXP,
bsw/jbe@1309:           getTextContent  = function(element) {
bsw/jbe@1309:             var textContent = wysihtml.lang.string(dom.getTextContent(element)).trim();
bsw/jbe@1309:             if (textContent.substr(0, 4) === "www.") {
bsw/jbe@1309:               textContent = "http://" + textContent;
bsw/jbe@1309:             }
bsw/jbe@1309:             return textContent;
bsw/jbe@1309:           };
bsw/jbe@1309: 
bsw/jbe@1309:       dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309:         if (!links.length) {
bsw/jbe@1309:           return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
bsw/jbe@1309:             link         = dom.getParentElement(selectedNode, { query: "a" }, 4),
bsw/jbe@1309:             textContent;
bsw/jbe@1309: 
bsw/jbe@1309:         if (!link) {
bsw/jbe@1309:           return;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         textContent = getTextContent(link);
bsw/jbe@1309:         // keydown is fired before the actual content is changed
bsw/jbe@1309:         // therefore we set a timeout to change the href
bsw/jbe@1309:         setTimeout(function() {
bsw/jbe@1309:           var newTextContent = getTextContent(link);
bsw/jbe@1309:           if (newTextContent === textContent) {
bsw/jbe@1309:             return;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           // Only set href when new href looks like a valid url
bsw/jbe@1309:           if (newTextContent.match(urlRegExp)) {
bsw/jbe@1309:             link.setAttribute("href", newTextContent);
bsw/jbe@1309:           }
bsw/jbe@1309:         }, 0);
bsw/jbe@1309:       });
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _initObjectResizing: function() {
bsw/jbe@1309:       this.commands.exec("enableObjectResizing", true);
bsw/jbe@1309: 
bsw/jbe@1309:       // IE sets inline styles after resizing objects
bsw/jbe@1309:       // The following lines make sure that the width/height css properties
bsw/jbe@1309:       // are copied over to the width/height attributes
bsw/jbe@1309:       if (browser.supportsEvent("resizeend")) {
bsw/jbe@1309:         var properties        = ["width", "height"],
bsw/jbe@1309:             propertiesLength  = properties.length,
bsw/jbe@1309:             element           = this.element;
bsw/jbe@1309: 
bsw/jbe@1309:         dom.observe(element, "resizeend", function(event) {
bsw/jbe@1309:           var target = event.target || event.srcElement,
bsw/jbe@1309:               style  = target.style,
bsw/jbe@1309:               i      = 0,
bsw/jbe@1309:               property;
bsw/jbe@1309: 
bsw/jbe@1309:           if (target.nodeName !== "IMG") {
bsw/jbe@1309:             return;
bsw/jbe@1309:           }
bsw/jbe@1309: 
bsw/jbe@1309:           for (; i p:first-child { margin-top: 0; }",
bsw/jbe@1309:         "._wysihtml-temp     { display: none; }",
bsw/jbe@1309:         wysihtml.browser.isGecko ?
bsw/jbe@1309:           "body.placeholder { color: graytext !important; }" :
bsw/jbe@1309:           "body.placeholder { color: #a9a9a9 !important; }",
bsw/jbe@1309:         // Ensure that user see's broken images and can delete them
bsw/jbe@1309:         "img:-moz-broken      { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
bsw/jbe@1309:       ];
bsw/jbe@1309: 
bsw/jbe@1309:   /**
bsw/jbe@1309:    * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
bsw/jbe@1309:    * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
bsw/jbe@1309:    *
bsw/jbe@1309:    * Other browsers need a more hacky way: (pssst don't tell my mama)
bsw/jbe@1309:    * In order to prevent the element being scrolled into view when focusing it, we simply
bsw/jbe@1309:    * move it out of the scrollable area, focus it, and reset it's position
bsw/jbe@1309:    */
bsw/jbe@1309:   var focusWithoutScrolling = function(element) {
bsw/jbe@1309:     if (element.setActive) {
bsw/jbe@1309:       // Following line could cause a js error when the textarea is invisible
bsw/jbe@1309:       // See https://github.com/xing/wysihtml5/issues/9
bsw/jbe@1309:       try { element.setActive(); } catch(e) {}
bsw/jbe@1309:     } else {
bsw/jbe@1309:       var elementStyle = element.style,
bsw/jbe@1309:           originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
bsw/jbe@1309:           originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
bsw/jbe@1309:           originalStyles = {
bsw/jbe@1309:             position:         elementStyle.position,
bsw/jbe@1309:             top:              elementStyle.top,
bsw/jbe@1309:             left:             elementStyle.left,
bsw/jbe@1309:             WebkitUserSelect: elementStyle.WebkitUserSelect
bsw/jbe@1309:           };
bsw/jbe@1309: 
bsw/jbe@1309:       dom.setStyles({
bsw/jbe@1309:         position:         "absolute",
bsw/jbe@1309:         top:              "-99999px",
bsw/jbe@1309:         left:             "-99999px",
bsw/jbe@1309:         // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
bsw/jbe@1309:         WebkitUserSelect: "none"
bsw/jbe@1309:       }).on(element);
bsw/jbe@1309: 
bsw/jbe@1309:       element.focus();
bsw/jbe@1309: 
bsw/jbe@1309:       dom.setStyles(originalStyles).on(element);
bsw/jbe@1309: 
bsw/jbe@1309:       if (win.scrollTo) {
bsw/jbe@1309:         // Some browser extensions unset this method to prevent annoyances
bsw/jbe@1309:         // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
bsw/jbe@1309:         // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
bsw/jbe@1309:         win.scrollTo(originalScrollLeft, originalScrollTop);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.Composer.prototype.style = function() {
bsw/jbe@1309:     var that                  = this,
bsw/jbe@1309:         originalActiveElement = doc.querySelector(":focus"),
bsw/jbe@1309:         textareaElement       = this.textarea.element,
bsw/jbe@1309:         hasPlaceholder        = textareaElement.hasAttribute("placeholder"),
bsw/jbe@1309:         originalPlaceholder   = hasPlaceholder && textareaElement.getAttribute("placeholder"),
bsw/jbe@1309:         originalDisplayValue  = textareaElement.style.display,
bsw/jbe@1309:         originalDisabled      = textareaElement.disabled,
bsw/jbe@1309:         displayValueForCopying;
bsw/jbe@1309: 
bsw/jbe@1309:     this.focusStylesHost      = HOST_TEMPLATE.cloneNode(false);
bsw/jbe@1309:     this.blurStylesHost       = HOST_TEMPLATE.cloneNode(false);
bsw/jbe@1309:     this.disabledStylesHost   = HOST_TEMPLATE.cloneNode(false);
bsw/jbe@1309: 
bsw/jbe@1309:     // Remove placeholder before copying (as the placeholder has an affect on the computed style)
bsw/jbe@1309:     if (hasPlaceholder) {
bsw/jbe@1309:       textareaElement.removeAttribute("placeholder");
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (textareaElement === originalActiveElement) {
bsw/jbe@1309:       textareaElement.blur();
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // enable for copying styles
bsw/jbe@1309:     textareaElement.disabled = false;
bsw/jbe@1309: 
bsw/jbe@1309:     // set textarea to display="none" to get cascaded styles via getComputedStyle
bsw/jbe@1309:     textareaElement.style.display = displayValueForCopying = "none";
bsw/jbe@1309: 
bsw/jbe@1309:     if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
bsw/jbe@1309:         (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
bsw/jbe@1309:       textareaElement.style.display = displayValueForCopying = originalDisplayValue;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
bsw/jbe@1309:     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost);
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- editor styles ---------
bsw/jbe@1309:     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- apply standard rules ---------
bsw/jbe@1309:     dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- :disabled styles ---------
bsw/jbe@1309:     textareaElement.disabled = true;
bsw/jbe@1309:     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
bsw/jbe@1309:     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
bsw/jbe@1309:     textareaElement.disabled = originalDisabled;
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- :focus styles ---------
bsw/jbe@1309:     textareaElement.style.display = originalDisplayValue;
bsw/jbe@1309:     focusWithoutScrolling(textareaElement);
bsw/jbe@1309:     textareaElement.style.display = displayValueForCopying;
bsw/jbe@1309: 
bsw/jbe@1309:     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
bsw/jbe@1309:     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
bsw/jbe@1309: 
bsw/jbe@1309:     // reset textarea
bsw/jbe@1309:     textareaElement.style.display = originalDisplayValue;
bsw/jbe@1309: 
bsw/jbe@1309:     dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
bsw/jbe@1309: 
bsw/jbe@1309:     // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
bsw/jbe@1309:     // this is needed for when the change_view event is fired where the iframe is hidden and then
bsw/jbe@1309:     // the blur event fires and re-displays it
bsw/jbe@1309:     var boxFormattingStyles = wysihtml.lang.array(BOX_FORMATTING).without(["display"]);
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- restore focus ---------
bsw/jbe@1309:     if (originalActiveElement) {
bsw/jbe@1309:       focusWithoutScrolling(originalActiveElement);
bsw/jbe@1309:     } else {
bsw/jbe@1309:       textareaElement.blur();
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- restore placeholder ---------
bsw/jbe@1309:     if (hasPlaceholder) {
bsw/jbe@1309:       textareaElement.setAttribute("placeholder", originalPlaceholder);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- Sync focus/blur styles ---------
bsw/jbe@1309:     this.parent.on("focus:composer", function() {
bsw/jbe@1309:       dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea);
bsw/jbe@1309:       dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     this.parent.on("blur:composer", function() {
bsw/jbe@1309:       dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
bsw/jbe@1309:       dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     this.parent.observe("disable:composer", function() {
bsw/jbe@1309:       dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea);
bsw/jbe@1309:       dom.copyStyles(TEXT_FORMATTING)     .from(that.disabledStylesHost).to(that.element);
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     this.parent.observe("enable:composer", function() {
bsw/jbe@1309:       dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
bsw/jbe@1309:       dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
bsw/jbe@1309:     });
bsw/jbe@1309: 
bsw/jbe@1309:     return this;
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Taking care of events
bsw/jbe@1309:  *  - Simulating 'change' event on contentEditable element
bsw/jbe@1309:  *  - Handling drag & drop logic
bsw/jbe@1309:  *  - Catch paste events
bsw/jbe@1309:  *  - Dispatch proprietary newword:composer event
bsw/jbe@1309:  *  - Keyboard shortcuts
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var dom       = wysihtml.dom,
bsw/jbe@1309:       domNode = dom.domNode,
bsw/jbe@1309:       browser   = wysihtml.browser,
bsw/jbe@1309:       /**
bsw/jbe@1309:        * Map keyCodes to query commands
bsw/jbe@1309:        */
bsw/jbe@1309:       shortcuts = {
bsw/jbe@1309:         "66": "bold",     // B
bsw/jbe@1309:         "73": "italic",   // I
bsw/jbe@1309:         "85": "underline" // U
bsw/jbe@1309:       };
bsw/jbe@1309: 
bsw/jbe@1309:   var actions = {
bsw/jbe@1309: 
bsw/jbe@1309:     // Adds multiple eventlisteners to target, bound to one callback
bsw/jbe@1309:     // TODO: If needed elsewhere make it part of wysihtml.dom or sth
bsw/jbe@1309:     addListeners: function (target, events, callback) {
bsw/jbe@1309:       for(var i = 0, max = events.length; i < max; i++) {
bsw/jbe@1309:         target.addEventListener(events[i], callback, false);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Removes multiple eventlisteners from target, bound to one callback
bsw/jbe@1309:     // TODO: If needed elsewhere make it part of wysihtml.dom or sth
bsw/jbe@1309:     removeListeners: function (target, events, callback) {
bsw/jbe@1309:       for(var i = 0, max = events.length; i < max; i++) {
bsw/jbe@1309:         target.removeEventListener(events[i], callback, false);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Override for giving user ability to delete last line break in table cell
bsw/jbe@1309:     fixLastBrDeletionInTable: function(composer, force) {
bsw/jbe@1309:       if (composer.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309:         var sel = composer.selection.getSelection(),
bsw/jbe@1309:             aNode = sel.anchorNode;
bsw/jbe@1309:         if (aNode && aNode.nodeType === 1 && (wysihtml.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) {
bsw/jbe@1309:           var nextNode = aNode.childNodes[sel.anchorOffset];
bsw/jbe@1309:           if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") {
bsw/jbe@1309:             nextNode.parentNode.removeChild(nextNode);
bsw/jbe@1309:             return true;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // If found an uneditable before caret then notify it before deletion
bsw/jbe@1309:     handleUneditableDeletion: function(composer) {
bsw/jbe@1309:       var before = composer.selection.getBeforeSelection(true);
bsw/jbe@1309:       if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) {
bsw/jbe@1309:         if (actions.fixLastBrDeletionInTable(composer, true)) {
bsw/jbe@1309:           return true;
bsw/jbe@1309:         }
bsw/jbe@1309:         try {
bsw/jbe@1309:           var ev = new CustomEvent("wysihtml:uneditable:delete", {bubbles: true, cancelable: false});
bsw/jbe@1309:           before.node.dispatchEvent(ev);
bsw/jbe@1309:         } catch (err) {}
bsw/jbe@1309:         before.node.parentNode.removeChild(before.node);
bsw/jbe@1309:         return true;
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Deletion with caret in the beginning of headings and other block elvel elements needs special attention
bsw/jbe@1309:     // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
bsw/jbe@1309:     fixDeleteInTheBeginningOfBlock: function(composer) {
bsw/jbe@1309:       var selection = composer.selection,
bsw/jbe@1309:           prevNode = selection.getPreviousNode();
bsw/jbe@1309: 
bsw/jbe@1309:       if (selection.caretIsFirstInSelection(wysihtml.browser.usesControlRanges()) && prevNode) {
bsw/jbe@1309:         if (prevNode.nodeType === 1 &&
bsw/jbe@1309:             wysihtml.dom.domNode(prevNode).is.block() &&
bsw/jbe@1309:             !domNode(prevNode).test({
bsw/jbe@1309:               query: "ol, ul, table, tr, dl"
bsw/jbe@1309:             })
bsw/jbe@1309:         ) {
bsw/jbe@1309:           if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
bsw/jbe@1309:             // If heading is empty remove the heading node
bsw/jbe@1309:             prevNode.parentNode.removeChild(prevNode);
bsw/jbe@1309:             return true;
bsw/jbe@1309:           } else {
bsw/jbe@1309:             if (prevNode.lastChild) {
bsw/jbe@1309:               var selNode = prevNode.lastChild,
bsw/jbe@1309:                   selectedNode = selection.getSelectedNode(),
bsw/jbe@1309:                   commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element),
bsw/jbe@1309:                   curNode = wysihtml.dom.getParentElement(selectedNode, {
bsw/jbe@1309:                     query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"
bsw/jbe@1309:                   }, false, commonAncestorNode || composer.element);
bsw/jbe@1309: 
bsw/jbe@1309:               if (curNode) {
bsw/jbe@1309:                 domNode(curNode).transferContentTo(prevNode, true);
bsw/jbe@1309:                 selection.setAfter(selNode);
bsw/jbe@1309:                 return true;
bsw/jbe@1309:               } else if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309:                 selectedNode = selection.getCaretNode();
bsw/jbe@1309:                 domNode(selectedNode).transferContentTo(prevNode, true);
bsw/jbe@1309:                 selection.setAfter(selNode);
bsw/jbe@1309:                 return true;
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */
bsw/jbe@1309:     /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */
bsw/jbe@1309:     fixDeleteInTheBeginningOfLi: function(composer) {
bsw/jbe@1309:       if (wysihtml.browser.hasLiDeletingProblem()) {
bsw/jbe@1309:         var selection = composer.selection.getSelection(),
bsw/jbe@1309:             aNode = selection.anchorNode,
bsw/jbe@1309:             listNode, prevNode, firstNode,
bsw/jbe@1309:             isInBeginnig = composer.selection.caretIsFirstInSelection(),
bsw/jbe@1309:             prevNode,
bsw/jbe@1309:             intermediaryNode;
bsw/jbe@1309: 
bsw/jbe@1309:         // Fix caret at the beginnig of first textNode in LI
bsw/jbe@1309:         if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) {
bsw/jbe@1309:           aNode = aNode.parentNode;
bsw/jbe@1309:           isInBeginnig = true;
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") {
bsw/jbe@1309:           prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:           if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) {
bsw/jbe@1309:             prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309:             intermediaryNode = aNode.parentNode;
bsw/jbe@1309:           }
bsw/jbe@1309:           if (prevNode) {
bsw/jbe@1309:             firstNode = aNode.firstChild;
bsw/jbe@1309:             domNode(aNode).transferContentTo(prevNode, true);
bsw/jbe@1309: 
bsw/jbe@1309:             if (intermediaryNode && intermediaryNode.children.length === 0){
bsw/jbe@1309:               intermediaryNode.remove();
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:             if (firstNode) {
bsw/jbe@1309:               composer.selection.setBefore(firstNode);
bsw/jbe@1309:             } else if (prevNode) {
bsw/jbe@1309:               if (prevNode.nodeType === 1) {
bsw/jbe@1309:                 if (prevNode.lastChild) {
bsw/jbe@1309:                   composer.selection.setAfter(prevNode.lastChild);
bsw/jbe@1309:                 } else {
bsw/jbe@1309:                   composer.selection.selectNode(prevNode);
bsw/jbe@1309:                 }
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 composer.selection.setAfter(prevNode);
bsw/jbe@1309:               }
bsw/jbe@1309:             }
bsw/jbe@1309:             return true;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     fixDeleteInTheBeginningOfControlSelection: function(composer) {
bsw/jbe@1309:       var selection = composer.selection,
bsw/jbe@1309:           prevNode = selection.getPreviousNode(),
bsw/jbe@1309:           selectedNode = selection.getSelectedNode(),
bsw/jbe@1309:           afterCaretNode;
bsw/jbe@1309: 
bsw/jbe@1309:       if (selection.caretIsFirstInSelection()) {
bsw/jbe@1309:         if (selectedNode.nodeType === 3) {
bsw/jbe@1309:           selectedNode = selectedNode.parentNode;
bsw/jbe@1309:         }
bsw/jbe@1309:         afterCaretNode = selectedNode.firstChild;
bsw/jbe@1309:         domNode(selectedNode).transferContentTo(prevNode, true);
bsw/jbe@1309:         if (afterCaretNode) {
bsw/jbe@1309:           composer.selection.setBefore(afterCaretNode);
bsw/jbe@1309:         }
bsw/jbe@1309:         return true;
bsw/jbe@1309:       }
bsw/jbe@1309:       return false;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
bsw/jbe@1309:     // Returns true if some corrections is applied so events know when to prevent default
bsw/jbe@1309:     doLineBreaksModeEnterWithCaret: function(composer) {
bsw/jbe@1309:       var breakNodes = "p, pre, div, blockquote",
bsw/jbe@1309:           caretInfo, parent, txtNode,
bsw/jbe@1309:           ret = false;
bsw/jbe@1309: 
bsw/jbe@1309:       caretInfo = composer.selection.getNodesNearCaret();
bsw/jbe@1309:       if (caretInfo) {
bsw/jbe@1309: 
bsw/jbe@1309:         if (caretInfo.caretNode || caretInfo.nextNode) {
bsw/jbe@1309:           parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2);
bsw/jbe@1309:           if (parent === composer.element) {
bsw/jbe@1309:             parent = undefined;
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309: 
bsw/jbe@1309:         if (parent && caretInfo.caretNode) {
bsw/jbe@1309:           if (domNode(caretInfo.caretNode).is.lineBreak()) {
bsw/jbe@1309: 
bsw/jbe@1309:             if (composer.config.doubleLineBreakEscapesBlock) {
bsw/jbe@1309:               // Double enter (enter on blank line) exits block element in useLineBreaks mode.
bsw/jbe@1309:               ret = true;
bsw/jbe@1309:               caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode);
bsw/jbe@1309: 
bsw/jbe@1309:               // Ensure surplous line breaks are not added to preceding element
bsw/jbe@1309:               if (domNode(caretInfo.nextNode).is.lineBreak()) {
bsw/jbe@1309:                 caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode);
bsw/jbe@1309:               }
bsw/jbe@1309: 
bsw/jbe@1309:               var brNode = composer.doc.createElement('br');
bsw/jbe@1309:               if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) {
bsw/jbe@1309:                 parent.parentNode.insertBefore(brNode, parent.nextSibling);
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 composer.selection.splitElementAtCaret(parent, brNode);
bsw/jbe@1309:               }
bsw/jbe@1309: 
bsw/jbe@1309:               // Ensure surplous blank lines are not added to preceding element
bsw/jbe@1309:               if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) {
bsw/jbe@1309:                 // Replaces blank lines at the beginning of textnode
bsw/jbe@1309:                 caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, '');
bsw/jbe@1309:               }
bsw/jbe@1309:               composer.selection.setBefore(brNode);
bsw/jbe@1309:             }
bsw/jbe@1309: 
bsw/jbe@1309:           } else if (caretInfo.caretNode.nodeType === 3 && wysihtml.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) {
bsw/jbe@1309: 
bsw/jbe@1309:             // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens.
bsw/jbe@1309:             // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br
bsw/jbe@1309:             ret = true;
bsw/jbe@1309:             var br1 = composer.doc.createElement('br'),
bsw/jbe@1309:                 br2 = composer.doc.createElement('br'),
bsw/jbe@1309:                 f = composer.doc.createDocumentFragment();
bsw/jbe@1309:             f.appendChild(br1);
bsw/jbe@1309:             f.appendChild(br2);
bsw/jbe@1309:             composer.selection.insertNode(f);
bsw/jbe@1309:             composer.selection.setBefore(br2);
bsw/jbe@1309: 
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:       return ret;
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleDeleteKeyPress = function(event, composer) {
bsw/jbe@1309:     var selection = composer.selection,
bsw/jbe@1309:         element = composer.element;
bsw/jbe@1309: 
bsw/jbe@1309:     if (selection.isCollapsed()) {
bsw/jbe@1309:       /**
bsw/jbe@1309:        * when the editor is empty in useLineBreaks = false mode, preserve
bsw/jbe@1309:        * the default value in it which is 
bsw/jbe@1309:        */
bsw/jbe@1309:       if (composer.isEmpty() && !composer.config.useLineBreaks) {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (actions.handleUneditableDeletion(composer)) {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (actions.fixDeleteInTheBeginningOfLi(composer)) {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (actions.fixDeleteInTheBeginningOfBlock(composer)) {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (actions.fixLastBrDeletionInTable(composer)) {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309:         if (actions.fixDeleteInTheBeginningOfControlSelection(composer)) {
bsw/jbe@1309:           event.preventDefault();
bsw/jbe@1309:           return;
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     } else {
bsw/jbe@1309:       if (selection.containsUneditable()) {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         selection.deleteContents();
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleEnterKeyPress = function(event, composer) {
bsw/jbe@1309:     if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) {
bsw/jbe@1309:       // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
bsw/jbe@1309: 
bsw/jbe@1309:       var breakNodes = "p, pre, div, blockquote",
bsw/jbe@1309:           caretInfo, parent, txtNode;
bsw/jbe@1309: 
bsw/jbe@1309:       if (composer.selection.isCollapsed()) {
bsw/jbe@1309:         if (actions.doLineBreaksModeEnterWithCaret(composer)) {
bsw/jbe@1309:           event.preventDefault();
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (browser.hasCaretAtLinkEndInsertionProblems() && composer.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309:       var target = composer.selection.getSelectedNode(true),
bsw/jbe@1309:           targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309:           invisibleSpace, space;
bsw/jbe@1309: 
bsw/jbe@1309:       if (targetEl && targetEl.closest('a') && target.nodeType === 3 && target === targetEl.lastChild) {
bsw/jbe@1309:         // Seems like enter was pressed and caret was at the end of link node
bsw/jbe@1309:         // This means user wants to escape the link now (caret is last in link node too).
bsw/jbe@1309:         composer.selection.setAfter(targetEl);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleTabKeyDown = function(composer, element, shiftKey) {
bsw/jbe@1309:     if (!composer.selection.isCollapsed()) {
bsw/jbe@1309:       composer.selection.deleteContents();
bsw/jbe@1309:     } else if (composer.selection.caretIsInTheBeginnig('li')) {
bsw/jbe@1309:       if (shiftKey) {
bsw/jbe@1309:         if (composer.commands.exec('outdentList')) return;
bsw/jbe@1309:       } else {
bsw/jbe@1309:         if (composer.commands.exec('indentList')) return;
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Is   close enough to tab. Could not find enough counter arguments for now.
bsw/jbe@1309:     composer.commands.exec("insertHTML", " ");
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleDomNodeRemoved = function(event) {
bsw/jbe@1309:       if (this.domNodeRemovedInterval) {
bsw/jbe@1309:         clearInterval(domNodeRemovedInterval);
bsw/jbe@1309:       }
bsw/jbe@1309:       this.parent.fire("destroy:composer");
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires
bsw/jbe@1309:   var handleUserInteraction = function (event) {
bsw/jbe@1309:     this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event);
bsw/jbe@1309:     setTimeout((function() {
bsw/jbe@1309:       this.parent.fire("interaction", event).fire("interaction:composer", event);
bsw/jbe@1309:     }).bind(this), 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleFocus = function(event) {
bsw/jbe@1309:     this.parent.fire("focus", event).fire("focus:composer", event);
bsw/jbe@1309: 
bsw/jbe@1309:     // Delay storing of state until all focus handler are fired
bsw/jbe@1309:     // especially the one which resets the placeholder
bsw/jbe@1309:     setTimeout((function() {
bsw/jbe@1309:       this.focusState = this.getValue(false, false);
bsw/jbe@1309:     }).bind(this), 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleBlur = function(event) {
bsw/jbe@1309:     if (this.focusState !== this.getValue(false, false)) {
bsw/jbe@1309:       //create change event if supported (all except IE8)
bsw/jbe@1309:       var changeevent = event;
bsw/jbe@1309:       if(typeof Object.create == 'function') {
bsw/jbe@1309:         changeevent = Object.create(event, { type: { value: 'change' } });
bsw/jbe@1309:       }
bsw/jbe@1309:       this.parent.fire("change", changeevent).fire("change:composer", changeevent);
bsw/jbe@1309:     }
bsw/jbe@1309:     this.parent.fire("blur", event).fire("blur:composer", event);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handlePaste = function(event) {
bsw/jbe@1309:     this.parent.fire(event.type, event).fire(event.type + ":composer", event);
bsw/jbe@1309:     if (event.type === "paste") {
bsw/jbe@1309:       setTimeout((function() {
bsw/jbe@1309:         this.parent.fire("newword:composer");
bsw/jbe@1309:       }).bind(this), 0);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleCopy = function(event) {
bsw/jbe@1309:     if (this.config.copyedFromMarking) {
bsw/jbe@1309:       // If supported the copied source can be based directly on selection
bsw/jbe@1309:       // 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.
bsw/jbe@1309:       if (wysihtml.browser.supportsModernPaste()) {
bsw/jbe@1309:         event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml());
bsw/jbe@1309:         event.clipboardData.setData("text/plain", this.selection.getPlainText());
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:       }
bsw/jbe@1309:       this.parent.fire(event.type, event).fire(event.type + ":composer", event);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleKeyUp = function(event) {
bsw/jbe@1309:     var keyCode = event.keyCode;
bsw/jbe@1309:     if (keyCode === wysihtml.SPACE_KEY || keyCode === wysihtml.ENTER_KEY) {
bsw/jbe@1309:       this.parent.fire("newword:composer");
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleMouseDown = function(event) {
bsw/jbe@1309:     if (!browser.canSelectImagesInContentEditable()) {
bsw/jbe@1309:       // Make sure that images are selected when clicking on them
bsw/jbe@1309:       var target = event.target,
bsw/jbe@1309:           allImages = this.element.querySelectorAll('img'),
bsw/jbe@1309:           notMyImages = this.element.querySelectorAll('.' + this.config.classNames.uneditableContainer + ' img'),
bsw/jbe@1309:           myImages = wysihtml.lang.array(allImages).without(notMyImages);
bsw/jbe@1309: 
bsw/jbe@1309:       if (target.nodeName === "IMG" && wysihtml.lang.array(myImages).contains(target)) {
bsw/jbe@1309:         this.selection.selectNode(target);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Saves mousedown position for IE controlSelect fix
bsw/jbe@1309:     if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309:       this.selection.lastMouseDownPos = {x: event.clientX, y: event.clientY};
bsw/jbe@1309:       setTimeout(function() {
bsw/jbe@1309:         delete this.selection.lastMouseDownPos;
bsw/jbe@1309:       }.bind(this), 0);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // IE has this madness of control selects of overflowed and some other elements (weird box around element on selection and second click selects text)
bsw/jbe@1309:   // This fix handles the second click problem by adding cursor to the right position under cursor inside when controlSelection is made
bsw/jbe@1309:   var handleIEControlSelect = function(event) {
bsw/jbe@1309:     var target = event.target,
bsw/jbe@1309:         pos = this.selection.lastMouseDownPos;
bsw/jbe@1309:     if (pos) {
bsw/jbe@1309:       var caretPosition = document.body.createTextRange();
bsw/jbe@1309:         setTimeout(function() {
bsw/jbe@1309:           try {
bsw/jbe@1309:             caretPosition.moveToPoint(pos.x, pos.y);
bsw/jbe@1309:             caretPosition.select();
bsw/jbe@1309:           } catch (e) {}
bsw/jbe@1309:         }.bind(this), 0);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleClick = function(event) {
bsw/jbe@1309:     if (this.config.classNames.uneditableContainer) {
bsw/jbe@1309:       // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text)
bsw/jbe@1309:       // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour
bsw/jbe@1309:       var uneditable = wysihtml.dom.getParentElement(event.target, { query: "." + this.config.classNames.uneditableContainer }, false, this.element);
bsw/jbe@1309:       if (uneditable) {
bsw/jbe@1309:         this.selection.setAfter(uneditable);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleDrop = function(event) {
bsw/jbe@1309:     if (!browser.canSelectImagesInContentEditable()) {
bsw/jbe@1309:       // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case
bsw/jbe@1309:       setTimeout((function() {
bsw/jbe@1309:         this.selection.getSelection().removeAllRanges();
bsw/jbe@1309:       }).bind(this), 0);
bsw/jbe@1309:     }
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleKeyDown = function(event) {
bsw/jbe@1309:     var keyCode = event.keyCode,
bsw/jbe@1309:         command = shortcuts[keyCode],
bsw/jbe@1309:         target = this.selection.getSelectedNode(true),
bsw/jbe@1309:         targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309:         parent;
bsw/jbe@1309: 
bsw/jbe@1309:     // Select all (meta/ctrl + a)
bsw/jbe@1309:     if ((event.ctrlKey || event.metaKey) && !event.altKey && keyCode === 65) {
bsw/jbe@1309:       this.selection.selectAll();
bsw/jbe@1309:       event.preventDefault();
bsw/jbe@1309:       return;
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Shortcut logic
bsw/jbe@1309:     if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
bsw/jbe@1309:       this.commands.exec(command);
bsw/jbe@1309:       event.preventDefault();
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (keyCode === wysihtml.BACKSPACE_KEY) {
bsw/jbe@1309:       // Delete key override for special cases
bsw/jbe@1309:       handleDeleteKeyPress(event, this);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor
bsw/jbe@1309:     if (keyCode === wysihtml.BACKSPACE_KEY || keyCode === wysihtml.DELETE_KEY) {
bsw/jbe@1309:       if (target && target.nodeName === "IMG") {
bsw/jbe@1309:         event.preventDefault();
bsw/jbe@1309:         parent = target.parentNode;
bsw/jbe@1309:         parent.removeChild(target);// delete the ![]() bsw/jbe@1309:         // And it's parent  too if it hasn't got any other child nodes
bsw/jbe@1309:         if (parent.nodeName === "A" && !parent.firstChild) {
bsw/jbe@1309:           parent.parentNode.removeChild(parent);
bsw/jbe@1309:         }
bsw/jbe@1309:         setTimeout((function() {
bsw/jbe@1309:           wysihtml.quirks.redraw(this.element);
bsw/jbe@1309:         }).bind(this), 0);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (this.config.handleTabKey && keyCode === wysihtml.TAB_KEY) {
bsw/jbe@1309:       // TAB key handling
bsw/jbe@1309:       event.preventDefault();
bsw/jbe@1309:       handleTabKeyDown(this, this.element, event.shiftKey);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (keyCode === wysihtml.ENTER_KEY) {
bsw/jbe@1309:       handleEnterKeyPress(event, this);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleKeyPress = function(event) {
bsw/jbe@1309: 
bsw/jbe@1309:     // This block should run only if some character is inserted (nor command keys like delete, backspace, enter, etc.)
bsw/jbe@1309:     if (event.which !== 0) {
bsw/jbe@1309: 
bsw/jbe@1309:       // Test if caret is last in a link in webkit and try to fix webkit problem,
bsw/jbe@1309:       // that all inserted content is added outside of link.
bsw/jbe@1309:       // This issue was added as a not thought through fix for getting caret after link in contenteditable if it is last in editable area.
bsw/jbe@1309:       // Allthough it fixes this minor case it actually introduces a cascade of problems when editing links.
bsw/jbe@1309:       // The standard approachi in other wysiwygs seems as a step backwards - introducing a separate modal for managing links content text.
bsw/jbe@1309:       // I find it to be too big of a tradeoff in terms of expected simple UI flow, thus trying to fight against it.
bsw/jbe@1309:       // Also adds link escaping by double space with caret at the end of link for all browsers
bsw/jbe@1309: 
bsw/jbe@1309:       if (this.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309:         var target = this.selection.getSelectedNode(true),
bsw/jbe@1309:             targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309:             invisibleSpace, space;
bsw/jbe@1309: 
bsw/jbe@1309:         if (targetEl && targetEl.closest('a') && target === targetEl.lastChild) {
bsw/jbe@1309: 
bsw/jbe@1309:           if (event.which !== 32 || this.selection.caretIsInTheEndOfNode(true) && browser.hasCaretAtLinkEndInsertionProblems()) {
bsw/jbe@1309:             // Executed if there is no whitespace before caret in textnode in case of pressing space.
bsw/jbe@1309:             // Whitespace before marks that user wants to escape the node by pressing double space.
bsw/jbe@1309:             // Otherwise insert the character in the link not out as it would like to go natively
bsw/jbe@1309: 
bsw/jbe@1309:             invisibleSpace = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309:             this.selection.insertNode(invisibleSpace);
bsw/jbe@1309:             this.selection.setBefore(invisibleSpace);
bsw/jbe@1309:             setTimeout(function() {
bsw/jbe@1309: 
bsw/jbe@1309:               if (invisibleSpace.textContent.length > 1) {
bsw/jbe@1309:                 invisibleSpace.textContent = invisibleSpace.textContent.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, '');
bsw/jbe@1309:                 this.selection.setAfter(invisibleSpace);
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 invisibleSpace.remove();
bsw/jbe@1309:               }
bsw/jbe@1309: 
bsw/jbe@1309:             }.bind(this), 0);
bsw/jbe@1309:           } else if (event.which === 32) {
bsw/jbe@1309:             // Seems like space was pressed and there was a space before the caret allready
bsw/jbe@1309:             // 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.
bsw/jbe@1309:             // But lets move the trailing space too out of link if present
bsw/jbe@1309: 
bsw/jbe@1309:             if (target.nodeType === 3 && (/[\u00A0 ]$/).test(target.textContent)) {
bsw/jbe@1309: 
bsw/jbe@1309:               target.textContent = target.textContent.replace(/[\u00A0 ]$/, '');
bsw/jbe@1309:               space = this.doc.createTextNode(' ');
bsw/jbe@1309:               targetEl.parentNode.insertBefore(space, targetEl.nextSibling);
bsw/jbe@1309:               this.selection.setAfter(space, false);
bsw/jbe@1309:               event.preventDefault();
bsw/jbe@1309: 
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   var handleIframeFocus = function(event) {
bsw/jbe@1309:     setTimeout((function() {
bsw/jbe@1309:       if (this.doc.querySelector(":focus") !== this.element) {
bsw/jbe@1309:         this.focus();
bsw/jbe@1309:       }
bsw/jbe@1309:     }).bind(this), 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleIframeBlur = function(event) {
bsw/jbe@1309:     setTimeout((function() {
bsw/jbe@1309:       this.selection.getSelection().removeAllRanges();
bsw/jbe@1309:     }).bind(this), 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // Testing requires actions to be accessible from out of scope
bsw/jbe@1309:   wysihtml.views.Composer.prototype.observeActions = actions;
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.Composer.prototype.observe = function() {
bsw/jbe@1309:     var that                = this,
bsw/jbe@1309:         container           = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
bsw/jbe@1309:         element             = this.element,
bsw/jbe@1309:         focusBlurElement    = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow();
bsw/jbe@1309: 
bsw/jbe@1309:     this.focusState = this.getValue(false, false);
bsw/jbe@1309:     this.actions = actions;
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- destroy:composer event ---------
bsw/jbe@1309:     container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:     // DOMNodeRemoved event is not supported in IE 8
bsw/jbe@1309:     // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed
bsw/jbe@1309:     if (!browser.supportsMutationEvents()) {
bsw/jbe@1309:       this.domNodeRemovedInterval = setInterval(function() {
bsw/jbe@1309:         if (!dom.contains(document.documentElement, container)) {
bsw/jbe@1309:           handleDomNodeRemoved.call(this);
bsw/jbe@1309:         }
bsw/jbe@1309:       }, 250);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     actions.addListeners(focusBlurElement, ['drop', 'paste', 'mouseup', 'focus', 'keyup'], handleUserInteraction.bind(this));
bsw/jbe@1309:     focusBlurElement.addEventListener('focus', handleFocus.bind(this), false);
bsw/jbe@1309:     focusBlurElement.addEventListener('blur',  handleBlur.bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:     actions.addListeners(this.element, ['drop', 'paste', 'beforepaste'], handlePaste.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('copy',       handleCopy.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('mousedown',  handleMouseDown.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('click',      handleClick.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('drop',       handleDrop.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('keyup',      handleKeyUp.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('keydown',    handleKeyDown.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('keypress',   handleKeyPress.bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:     // IE controlselect madness fix
bsw/jbe@1309:     if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309:       this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     this.element.addEventListener("dragenter", (function() {
bsw/jbe@1309:       this.parent.fire("unset_placeholder");
bsw/jbe@1309:     }).bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Class that takes care that the value of the composer and the textarea is always in sync
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var INTERVAL = 400;
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.Synchronizer = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.views.Synchronizer.prototype */ {
bsw/jbe@1309: 
bsw/jbe@1309:     constructor: function(editor, textarea, composer) {
bsw/jbe@1309:       this.editor   = editor;
bsw/jbe@1309:       this.textarea = textarea;
bsw/jbe@1309:       this.composer = composer;
bsw/jbe@1309: 
bsw/jbe@1309:       this._observe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Sync html from composer to textarea
bsw/jbe@1309:      * Takes care of placeholders
bsw/jbe@1309:      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
bsw/jbe@1309:      */
bsw/jbe@1309:     fromComposerToTextarea: function(shouldParseHtml) {
bsw/jbe@1309:       this.textarea.setValue(wysihtml.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Sync value of textarea to composer
bsw/jbe@1309:      * Takes care of placeholders
bsw/jbe@1309:      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
bsw/jbe@1309:      */
bsw/jbe@1309:     fromTextareaToComposer: function(shouldParseHtml) {
bsw/jbe@1309:       var textareaValue = this.textarea.getValue(false, false);
bsw/jbe@1309:       if (textareaValue) {
bsw/jbe@1309:         this.composer.setValue(textareaValue, shouldParseHtml);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.composer.clear();
bsw/jbe@1309:         this.editor.fire("set_placeholder");
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Invoke syncing based on view state
bsw/jbe@1309:      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
bsw/jbe@1309:      */
bsw/jbe@1309:     sync: function(shouldParseHtml) {
bsw/jbe@1309:       if (this.editor.currentView.name === "textarea") {
bsw/jbe@1309:         this.fromTextareaToComposer(shouldParseHtml);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.fromComposerToTextarea(shouldParseHtml);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Initializes interval-based syncing
bsw/jbe@1309:      * also makes sure that on-submit the composer's content is synced with the textarea
bsw/jbe@1309:      * immediately when the form gets submitted
bsw/jbe@1309:      */
bsw/jbe@1309:     _observe: function() {
bsw/jbe@1309:       var interval,
bsw/jbe@1309:           that          = this,
bsw/jbe@1309:           form          = this.textarea.element.form,
bsw/jbe@1309:           startInterval = function() {
bsw/jbe@1309:             interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
bsw/jbe@1309:           },
bsw/jbe@1309:           stopInterval  = function() {
bsw/jbe@1309:             clearInterval(interval);
bsw/jbe@1309:             interval = null;
bsw/jbe@1309:           };
bsw/jbe@1309: 
bsw/jbe@1309:       startInterval();
bsw/jbe@1309: 
bsw/jbe@1309:       if (form) {
bsw/jbe@1309:         // If the textarea is in a form make sure that after onreset and onsubmit the composer
bsw/jbe@1309:         // has the correct state
bsw/jbe@1309:         wysihtml.dom.observe(form, "submit", function() {
bsw/jbe@1309:           that.sync(true);
bsw/jbe@1309:         });
bsw/jbe@1309:         wysihtml.dom.observe(form, "reset", function() {
bsw/jbe@1309:           setTimeout(function() { that.fromTextareaToComposer(); }, 0);
bsw/jbe@1309:         });
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.editor.on("change_view", function(view) {
bsw/jbe@1309:         if (view === "composer" && !interval) {
bsw/jbe@1309:           that.fromTextareaToComposer(true);
bsw/jbe@1309:           startInterval();
bsw/jbe@1309:         } else if (view === "textarea") {
bsw/jbe@1309:           that.fromComposerToTextarea(true);
bsw/jbe@1309:           stopInterval();
bsw/jbe@1309:         }
bsw/jbe@1309:       });
bsw/jbe@1309: 
bsw/jbe@1309:       this.editor.on("destroy:composer", stopInterval);
bsw/jbe@1309:     }
bsw/jbe@1309:   });
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.SourceView = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.views.SourceView.prototype */ {
bsw/jbe@1309: 
bsw/jbe@1309:     constructor: function(editor, composer) {
bsw/jbe@1309:       this.editor   = editor;
bsw/jbe@1309:       this.composer = composer;
bsw/jbe@1309: 
bsw/jbe@1309:       this._observe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     switchToTextarea: function(shouldParseHtml) {
bsw/jbe@1309:       var composerStyles = this.composer.win.getComputedStyle(this.composer.element),
bsw/jbe@1309:           width = parseFloat(composerStyles.width),
bsw/jbe@1309:           height = Math.max(parseFloat(composerStyles.height), 100);
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.textarea) {
bsw/jbe@1309:         this.textarea = this.composer.doc.createElement('textarea');
bsw/jbe@1309:         this.textarea.className = "wysihtml-source-view";
bsw/jbe@1309:       }
bsw/jbe@1309:       this.textarea.style.width = width + 'px';
bsw/jbe@1309:       this.textarea.style.height = height + 'px';
bsw/jbe@1309:       this.textarea.value = this.editor.getValue(shouldParseHtml, true);
bsw/jbe@1309:       this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element);
bsw/jbe@1309:       this.editor.currentView = "source";
bsw/jbe@1309:       this.composer.element.style.display = 'none';
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     switchToComposer: function(shouldParseHtml) {
bsw/jbe@1309:       var textareaValue = this.textarea.value;
bsw/jbe@1309:       if (textareaValue) {
bsw/jbe@1309:         this.composer.setValue(textareaValue, shouldParseHtml);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.composer.clear();
bsw/jbe@1309:         this.editor.fire("set_placeholder");
bsw/jbe@1309:       }
bsw/jbe@1309:       this.textarea.parentNode.removeChild(this.textarea);
bsw/jbe@1309:       this.editor.currentView = this.composer;
bsw/jbe@1309:       this.composer.element.style.display = '';
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _observe: function() {
bsw/jbe@1309:       this.editor.on("change_view", function(view) {
bsw/jbe@1309:         if (view === "composer") {
bsw/jbe@1309:           this.switchToComposer(true);
bsw/jbe@1309:         } else if (view === "textarea") {
bsw/jbe@1309:           this.switchToTextarea(true);
bsw/jbe@1309:         }
bsw/jbe@1309:       }.bind(this));
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:   });
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.views.Textarea = wysihtml.views.View.extend(
bsw/jbe@1309:   /** @scope wysihtml.views.Textarea.prototype */ {
bsw/jbe@1309:   name: "textarea",
bsw/jbe@1309: 
bsw/jbe@1309:   constructor: function(parent, textareaElement, config) {
bsw/jbe@1309:     this.base(parent, textareaElement, config);
bsw/jbe@1309: 
bsw/jbe@1309:     this._observe();
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   clear: function() {
bsw/jbe@1309:     this.element.value = "";
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   getValue: function(parse) {
bsw/jbe@1309:     var value = this.isEmpty() ? "" : this.element.value;
bsw/jbe@1309:     if (parse !== false) {
bsw/jbe@1309:       value = this.parent.parse(value);
bsw/jbe@1309:     }
bsw/jbe@1309:     return value;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   setValue: function(html, parse) {
bsw/jbe@1309:     if (parse !== false) {
bsw/jbe@1309:       html = this.parent.parse(html);
bsw/jbe@1309:     }
bsw/jbe@1309:     this.element.value = html;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   cleanUp: function(rules) {
bsw/jbe@1309:       var html = this.parent.parse(this.element.value, undefined, rules);
bsw/jbe@1309:       this.element.value = html;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   hasPlaceholderSet: function() {
bsw/jbe@1309:     var supportsPlaceholder = wysihtml.browser.supportsPlaceholderAttributeOn(this.element),
bsw/jbe@1309:         placeholderText     = this.element.getAttribute("placeholder") || null,
bsw/jbe@1309:         value               = this.element.value,
bsw/jbe@1309:         isEmpty             = !value;
bsw/jbe@1309:     return (supportsPlaceholder && isEmpty) || (value === placeholderText);
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   isEmpty: function() {
bsw/jbe@1309:     return !wysihtml.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   _observe: function() {
bsw/jbe@1309:     var element = this.element,
bsw/jbe@1309:         parent  = this.parent,
bsw/jbe@1309:         eventMapping = {
bsw/jbe@1309:           focusin:  "focus",
bsw/jbe@1309:           focusout: "blur"
bsw/jbe@1309:         },
bsw/jbe@1309:         /**
bsw/jbe@1309:          * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
bsw/jbe@1309:          * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
bsw/jbe@1309:          */
bsw/jbe@1309:         events = wysihtml.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
bsw/jbe@1309: 
bsw/jbe@1309:     parent.on("beforeload", function() {
bsw/jbe@1309:       wysihtml.dom.observe(element, events, function(event) {
bsw/jbe@1309:         var eventName = eventMapping[event.type] || event.type;
bsw/jbe@1309:         parent.fire(eventName).fire(eventName + ":textarea");
bsw/jbe@1309:       });
bsw/jbe@1309: 
bsw/jbe@1309:       wysihtml.dom.observe(element, ["paste", "drop"], function() {
bsw/jbe@1309:         setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
bsw/jbe@1309:       });
bsw/jbe@1309:     });
bsw/jbe@1309:   }
bsw/jbe@1309: });
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * WYSIHTML Editor
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
bsw/jbe@1309:  * @param {Object} [config] See defaults object below for explanation of each individual config option
bsw/jbe@1309:  *
bsw/jbe@1309:  * @events
bsw/jbe@1309:  *    load
bsw/jbe@1309:  *    beforeload (for internal use only)
bsw/jbe@1309:  *    focus
bsw/jbe@1309:  *    focus:composer
bsw/jbe@1309:  *    focus:textarea
bsw/jbe@1309:  *    blur
bsw/jbe@1309:  *    blur:composer
bsw/jbe@1309:  *    blur:textarea
bsw/jbe@1309:  *    change
bsw/jbe@1309:  *    change:composer
bsw/jbe@1309:  *    change:textarea
bsw/jbe@1309:  *    paste
bsw/jbe@1309:  *    paste:composer
bsw/jbe@1309:  *    paste:textarea
bsw/jbe@1309:  *    newword:composer
bsw/jbe@1309:  *    destroy:composer
bsw/jbe@1309:  *    undo:composer
bsw/jbe@1309:  *    redo:composer
bsw/jbe@1309:  *    beforecommand:composer
bsw/jbe@1309:  *    aftercommand:composer
bsw/jbe@1309:  *    enable:composer
bsw/jbe@1309:  *    disable:composer
bsw/jbe@1309:  *    change_view
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var undef;
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.Editor = wysihtml.lang.Dispatcher.extend({
bsw/jbe@1309:     /** @scope wysihtml.Editor.prototype */
bsw/jbe@1309:     defaults: {
bsw/jbe@1309:       // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
bsw/jbe@1309:       name:                 undef,
bsw/jbe@1309:       // Whether the editor should look like the textarea (by adopting styles)
bsw/jbe@1309:       style:                true,
bsw/jbe@1309:       // Whether urls, entered by the user should automatically become clickable-links
bsw/jbe@1309:       autoLink:             true,
bsw/jbe@1309:       // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
bsw/jbe@1309:       handleTabKey:         true,
bsw/jbe@1309:       // Object which includes parser rules to apply when html gets cleaned
bsw/jbe@1309:       // See parser_rules/*.js for examples
bsw/jbe@1309:       parserRules:          { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} },
bsw/jbe@1309:       // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
bsw/jbe@1309:       pasteParserRulesets: null,
bsw/jbe@1309:       // Parser method to use when the user inserts content
bsw/jbe@1309:       parser:               wysihtml.dom.parse,
bsw/jbe@1309:       // By default wysihtml will insert a
bsw/jbe@1309:         // And it's parent  too if it hasn't got any other child nodes
bsw/jbe@1309:         if (parent.nodeName === "A" && !parent.firstChild) {
bsw/jbe@1309:           parent.parentNode.removeChild(parent);
bsw/jbe@1309:         }
bsw/jbe@1309:         setTimeout((function() {
bsw/jbe@1309:           wysihtml.quirks.redraw(this.element);
bsw/jbe@1309:         }).bind(this), 0);
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (this.config.handleTabKey && keyCode === wysihtml.TAB_KEY) {
bsw/jbe@1309:       // TAB key handling
bsw/jbe@1309:       event.preventDefault();
bsw/jbe@1309:       handleTabKeyDown(this, this.element, event.shiftKey);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     if (keyCode === wysihtml.ENTER_KEY) {
bsw/jbe@1309:       handleEnterKeyPress(event, this);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleKeyPress = function(event) {
bsw/jbe@1309: 
bsw/jbe@1309:     // This block should run only if some character is inserted (nor command keys like delete, backspace, enter, etc.)
bsw/jbe@1309:     if (event.which !== 0) {
bsw/jbe@1309: 
bsw/jbe@1309:       // Test if caret is last in a link in webkit and try to fix webkit problem,
bsw/jbe@1309:       // that all inserted content is added outside of link.
bsw/jbe@1309:       // This issue was added as a not thought through fix for getting caret after link in contenteditable if it is last in editable area.
bsw/jbe@1309:       // Allthough it fixes this minor case it actually introduces a cascade of problems when editing links.
bsw/jbe@1309:       // The standard approachi in other wysiwygs seems as a step backwards - introducing a separate modal for managing links content text.
bsw/jbe@1309:       // I find it to be too big of a tradeoff in terms of expected simple UI flow, thus trying to fight against it.
bsw/jbe@1309:       // Also adds link escaping by double space with caret at the end of link for all browsers
bsw/jbe@1309: 
bsw/jbe@1309:       if (this.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309:         var target = this.selection.getSelectedNode(true),
bsw/jbe@1309:             targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309:             invisibleSpace, space;
bsw/jbe@1309: 
bsw/jbe@1309:         if (targetEl && targetEl.closest('a') && target === targetEl.lastChild) {
bsw/jbe@1309: 
bsw/jbe@1309:           if (event.which !== 32 || this.selection.caretIsInTheEndOfNode(true) && browser.hasCaretAtLinkEndInsertionProblems()) {
bsw/jbe@1309:             // Executed if there is no whitespace before caret in textnode in case of pressing space.
bsw/jbe@1309:             // Whitespace before marks that user wants to escape the node by pressing double space.
bsw/jbe@1309:             // Otherwise insert the character in the link not out as it would like to go natively
bsw/jbe@1309: 
bsw/jbe@1309:             invisibleSpace = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309:             this.selection.insertNode(invisibleSpace);
bsw/jbe@1309:             this.selection.setBefore(invisibleSpace);
bsw/jbe@1309:             setTimeout(function() {
bsw/jbe@1309: 
bsw/jbe@1309:               if (invisibleSpace.textContent.length > 1) {
bsw/jbe@1309:                 invisibleSpace.textContent = invisibleSpace.textContent.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, '');
bsw/jbe@1309:                 this.selection.setAfter(invisibleSpace);
bsw/jbe@1309:               } else {
bsw/jbe@1309:                 invisibleSpace.remove();
bsw/jbe@1309:               }
bsw/jbe@1309: 
bsw/jbe@1309:             }.bind(this), 0);
bsw/jbe@1309:           } else if (event.which === 32) {
bsw/jbe@1309:             // Seems like space was pressed and there was a space before the caret allready
bsw/jbe@1309:             // 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.
bsw/jbe@1309:             // But lets move the trailing space too out of link if present
bsw/jbe@1309: 
bsw/jbe@1309:             if (target.nodeType === 3 && (/[\u00A0 ]$/).test(target.textContent)) {
bsw/jbe@1309: 
bsw/jbe@1309:               target.textContent = target.textContent.replace(/[\u00A0 ]$/, '');
bsw/jbe@1309:               space = this.doc.createTextNode(' ');
bsw/jbe@1309:               targetEl.parentNode.insertBefore(space, targetEl.nextSibling);
bsw/jbe@1309:               this.selection.setAfter(space, false);
bsw/jbe@1309:               event.preventDefault();
bsw/jbe@1309: 
bsw/jbe@1309:             }
bsw/jbe@1309:           }
bsw/jbe@1309:         }
bsw/jbe@1309:       }
bsw/jbe@1309:     }
bsw/jbe@1309:   }
bsw/jbe@1309: 
bsw/jbe@1309:   var handleIframeFocus = function(event) {
bsw/jbe@1309:     setTimeout((function() {
bsw/jbe@1309:       if (this.doc.querySelector(":focus") !== this.element) {
bsw/jbe@1309:         this.focus();
bsw/jbe@1309:       }
bsw/jbe@1309:     }).bind(this), 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   var handleIframeBlur = function(event) {
bsw/jbe@1309:     setTimeout((function() {
bsw/jbe@1309:       this.selection.getSelection().removeAllRanges();
bsw/jbe@1309:     }).bind(this), 0);
bsw/jbe@1309:   };
bsw/jbe@1309: 
bsw/jbe@1309:   // Testing requires actions to be accessible from out of scope
bsw/jbe@1309:   wysihtml.views.Composer.prototype.observeActions = actions;
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.Composer.prototype.observe = function() {
bsw/jbe@1309:     var that                = this,
bsw/jbe@1309:         container           = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
bsw/jbe@1309:         element             = this.element,
bsw/jbe@1309:         focusBlurElement    = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow();
bsw/jbe@1309: 
bsw/jbe@1309:     this.focusState = this.getValue(false, false);
bsw/jbe@1309:     this.actions = actions;
bsw/jbe@1309: 
bsw/jbe@1309:     // --------- destroy:composer event ---------
bsw/jbe@1309:     container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:     // DOMNodeRemoved event is not supported in IE 8
bsw/jbe@1309:     // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed
bsw/jbe@1309:     if (!browser.supportsMutationEvents()) {
bsw/jbe@1309:       this.domNodeRemovedInterval = setInterval(function() {
bsw/jbe@1309:         if (!dom.contains(document.documentElement, container)) {
bsw/jbe@1309:           handleDomNodeRemoved.call(this);
bsw/jbe@1309:         }
bsw/jbe@1309:       }, 250);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     actions.addListeners(focusBlurElement, ['drop', 'paste', 'mouseup', 'focus', 'keyup'], handleUserInteraction.bind(this));
bsw/jbe@1309:     focusBlurElement.addEventListener('focus', handleFocus.bind(this), false);
bsw/jbe@1309:     focusBlurElement.addEventListener('blur',  handleBlur.bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:     actions.addListeners(this.element, ['drop', 'paste', 'beforepaste'], handlePaste.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('copy',       handleCopy.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('mousedown',  handleMouseDown.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('click',      handleClick.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('drop',       handleDrop.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('keyup',      handleKeyUp.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('keydown',    handleKeyDown.bind(this), false);
bsw/jbe@1309:     this.element.addEventListener('keypress',   handleKeyPress.bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:     // IE controlselect madness fix
bsw/jbe@1309:     if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309:       this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false);
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:     this.element.addEventListener("dragenter", (function() {
bsw/jbe@1309:       this.parent.fire("unset_placeholder");
bsw/jbe@1309:     }).bind(this), false);
bsw/jbe@1309: 
bsw/jbe@1309:   };
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * Class that takes care that the value of the composer and the textarea is always in sync
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var INTERVAL = 400;
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.Synchronizer = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.views.Synchronizer.prototype */ {
bsw/jbe@1309: 
bsw/jbe@1309:     constructor: function(editor, textarea, composer) {
bsw/jbe@1309:       this.editor   = editor;
bsw/jbe@1309:       this.textarea = textarea;
bsw/jbe@1309:       this.composer = composer;
bsw/jbe@1309: 
bsw/jbe@1309:       this._observe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Sync html from composer to textarea
bsw/jbe@1309:      * Takes care of placeholders
bsw/jbe@1309:      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
bsw/jbe@1309:      */
bsw/jbe@1309:     fromComposerToTextarea: function(shouldParseHtml) {
bsw/jbe@1309:       this.textarea.setValue(wysihtml.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Sync value of textarea to composer
bsw/jbe@1309:      * Takes care of placeholders
bsw/jbe@1309:      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
bsw/jbe@1309:      */
bsw/jbe@1309:     fromTextareaToComposer: function(shouldParseHtml) {
bsw/jbe@1309:       var textareaValue = this.textarea.getValue(false, false);
bsw/jbe@1309:       if (textareaValue) {
bsw/jbe@1309:         this.composer.setValue(textareaValue, shouldParseHtml);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.composer.clear();
bsw/jbe@1309:         this.editor.fire("set_placeholder");
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Invoke syncing based on view state
bsw/jbe@1309:      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
bsw/jbe@1309:      */
bsw/jbe@1309:     sync: function(shouldParseHtml) {
bsw/jbe@1309:       if (this.editor.currentView.name === "textarea") {
bsw/jbe@1309:         this.fromTextareaToComposer(shouldParseHtml);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.fromComposerToTextarea(shouldParseHtml);
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Initializes interval-based syncing
bsw/jbe@1309:      * also makes sure that on-submit the composer's content is synced with the textarea
bsw/jbe@1309:      * immediately when the form gets submitted
bsw/jbe@1309:      */
bsw/jbe@1309:     _observe: function() {
bsw/jbe@1309:       var interval,
bsw/jbe@1309:           that          = this,
bsw/jbe@1309:           form          = this.textarea.element.form,
bsw/jbe@1309:           startInterval = function() {
bsw/jbe@1309:             interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
bsw/jbe@1309:           },
bsw/jbe@1309:           stopInterval  = function() {
bsw/jbe@1309:             clearInterval(interval);
bsw/jbe@1309:             interval = null;
bsw/jbe@1309:           };
bsw/jbe@1309: 
bsw/jbe@1309:       startInterval();
bsw/jbe@1309: 
bsw/jbe@1309:       if (form) {
bsw/jbe@1309:         // If the textarea is in a form make sure that after onreset and onsubmit the composer
bsw/jbe@1309:         // has the correct state
bsw/jbe@1309:         wysihtml.dom.observe(form, "submit", function() {
bsw/jbe@1309:           that.sync(true);
bsw/jbe@1309:         });
bsw/jbe@1309:         wysihtml.dom.observe(form, "reset", function() {
bsw/jbe@1309:           setTimeout(function() { that.fromTextareaToComposer(); }, 0);
bsw/jbe@1309:         });
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.editor.on("change_view", function(view) {
bsw/jbe@1309:         if (view === "composer" && !interval) {
bsw/jbe@1309:           that.fromTextareaToComposer(true);
bsw/jbe@1309:           startInterval();
bsw/jbe@1309:         } else if (view === "textarea") {
bsw/jbe@1309:           that.fromComposerToTextarea(true);
bsw/jbe@1309:           stopInterval();
bsw/jbe@1309:         }
bsw/jbe@1309:       });
bsw/jbe@1309: 
bsw/jbe@1309:       this.editor.on("destroy:composer", stopInterval);
bsw/jbe@1309:     }
bsw/jbe@1309:   });
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.views.SourceView = Base.extend(
bsw/jbe@1309:     /** @scope wysihtml.views.SourceView.prototype */ {
bsw/jbe@1309: 
bsw/jbe@1309:     constructor: function(editor, composer) {
bsw/jbe@1309:       this.editor   = editor;
bsw/jbe@1309:       this.composer = composer;
bsw/jbe@1309: 
bsw/jbe@1309:       this._observe();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     switchToTextarea: function(shouldParseHtml) {
bsw/jbe@1309:       var composerStyles = this.composer.win.getComputedStyle(this.composer.element),
bsw/jbe@1309:           width = parseFloat(composerStyles.width),
bsw/jbe@1309:           height = Math.max(parseFloat(composerStyles.height), 100);
bsw/jbe@1309: 
bsw/jbe@1309:       if (!this.textarea) {
bsw/jbe@1309:         this.textarea = this.composer.doc.createElement('textarea');
bsw/jbe@1309:         this.textarea.className = "wysihtml-source-view";
bsw/jbe@1309:       }
bsw/jbe@1309:       this.textarea.style.width = width + 'px';
bsw/jbe@1309:       this.textarea.style.height = height + 'px';
bsw/jbe@1309:       this.textarea.value = this.editor.getValue(shouldParseHtml, true);
bsw/jbe@1309:       this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element);
bsw/jbe@1309:       this.editor.currentView = "source";
bsw/jbe@1309:       this.composer.element.style.display = 'none';
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     switchToComposer: function(shouldParseHtml) {
bsw/jbe@1309:       var textareaValue = this.textarea.value;
bsw/jbe@1309:       if (textareaValue) {
bsw/jbe@1309:         this.composer.setValue(textareaValue, shouldParseHtml);
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.composer.clear();
bsw/jbe@1309:         this.editor.fire("set_placeholder");
bsw/jbe@1309:       }
bsw/jbe@1309:       this.textarea.parentNode.removeChild(this.textarea);
bsw/jbe@1309:       this.editor.currentView = this.composer;
bsw/jbe@1309:       this.composer.element.style.display = '';
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _observe: function() {
bsw/jbe@1309:       this.editor.on("change_view", function(view) {
bsw/jbe@1309:         if (view === "composer") {
bsw/jbe@1309:           this.switchToComposer(true);
bsw/jbe@1309:         } else if (view === "textarea") {
bsw/jbe@1309:           this.switchToTextarea(true);
bsw/jbe@1309:         }
bsw/jbe@1309:       }.bind(this));
bsw/jbe@1309:     }
bsw/jbe@1309: 
bsw/jbe@1309:   });
bsw/jbe@1309: 
bsw/jbe@1309: })(wysihtml);
bsw/jbe@1309: 
bsw/jbe@1309: wysihtml.views.Textarea = wysihtml.views.View.extend(
bsw/jbe@1309:   /** @scope wysihtml.views.Textarea.prototype */ {
bsw/jbe@1309:   name: "textarea",
bsw/jbe@1309: 
bsw/jbe@1309:   constructor: function(parent, textareaElement, config) {
bsw/jbe@1309:     this.base(parent, textareaElement, config);
bsw/jbe@1309: 
bsw/jbe@1309:     this._observe();
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   clear: function() {
bsw/jbe@1309:     this.element.value = "";
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   getValue: function(parse) {
bsw/jbe@1309:     var value = this.isEmpty() ? "" : this.element.value;
bsw/jbe@1309:     if (parse !== false) {
bsw/jbe@1309:       value = this.parent.parse(value);
bsw/jbe@1309:     }
bsw/jbe@1309:     return value;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   setValue: function(html, parse) {
bsw/jbe@1309:     if (parse !== false) {
bsw/jbe@1309:       html = this.parent.parse(html);
bsw/jbe@1309:     }
bsw/jbe@1309:     this.element.value = html;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   cleanUp: function(rules) {
bsw/jbe@1309:       var html = this.parent.parse(this.element.value, undefined, rules);
bsw/jbe@1309:       this.element.value = html;
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   hasPlaceholderSet: function() {
bsw/jbe@1309:     var supportsPlaceholder = wysihtml.browser.supportsPlaceholderAttributeOn(this.element),
bsw/jbe@1309:         placeholderText     = this.element.getAttribute("placeholder") || null,
bsw/jbe@1309:         value               = this.element.value,
bsw/jbe@1309:         isEmpty             = !value;
bsw/jbe@1309:     return (supportsPlaceholder && isEmpty) || (value === placeholderText);
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   isEmpty: function() {
bsw/jbe@1309:     return !wysihtml.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
bsw/jbe@1309:   },
bsw/jbe@1309: 
bsw/jbe@1309:   _observe: function() {
bsw/jbe@1309:     var element = this.element,
bsw/jbe@1309:         parent  = this.parent,
bsw/jbe@1309:         eventMapping = {
bsw/jbe@1309:           focusin:  "focus",
bsw/jbe@1309:           focusout: "blur"
bsw/jbe@1309:         },
bsw/jbe@1309:         /**
bsw/jbe@1309:          * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
bsw/jbe@1309:          * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
bsw/jbe@1309:          */
bsw/jbe@1309:         events = wysihtml.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
bsw/jbe@1309: 
bsw/jbe@1309:     parent.on("beforeload", function() {
bsw/jbe@1309:       wysihtml.dom.observe(element, events, function(event) {
bsw/jbe@1309:         var eventName = eventMapping[event.type] || event.type;
bsw/jbe@1309:         parent.fire(eventName).fire(eventName + ":textarea");
bsw/jbe@1309:       });
bsw/jbe@1309: 
bsw/jbe@1309:       wysihtml.dom.observe(element, ["paste", "drop"], function() {
bsw/jbe@1309:         setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
bsw/jbe@1309:       });
bsw/jbe@1309:     });
bsw/jbe@1309:   }
bsw/jbe@1309: });
bsw/jbe@1309: 
bsw/jbe@1309: /**
bsw/jbe@1309:  * WYSIHTML Editor
bsw/jbe@1309:  *
bsw/jbe@1309:  * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
bsw/jbe@1309:  * @param {Object} [config] See defaults object below for explanation of each individual config option
bsw/jbe@1309:  *
bsw/jbe@1309:  * @events
bsw/jbe@1309:  *    load
bsw/jbe@1309:  *    beforeload (for internal use only)
bsw/jbe@1309:  *    focus
bsw/jbe@1309:  *    focus:composer
bsw/jbe@1309:  *    focus:textarea
bsw/jbe@1309:  *    blur
bsw/jbe@1309:  *    blur:composer
bsw/jbe@1309:  *    blur:textarea
bsw/jbe@1309:  *    change
bsw/jbe@1309:  *    change:composer
bsw/jbe@1309:  *    change:textarea
bsw/jbe@1309:  *    paste
bsw/jbe@1309:  *    paste:composer
bsw/jbe@1309:  *    paste:textarea
bsw/jbe@1309:  *    newword:composer
bsw/jbe@1309:  *    destroy:composer
bsw/jbe@1309:  *    undo:composer
bsw/jbe@1309:  *    redo:composer
bsw/jbe@1309:  *    beforecommand:composer
bsw/jbe@1309:  *    aftercommand:composer
bsw/jbe@1309:  *    enable:composer
bsw/jbe@1309:  *    disable:composer
bsw/jbe@1309:  *    change_view
bsw/jbe@1309:  */
bsw/jbe@1309: (function(wysihtml) {
bsw/jbe@1309:   var undef;
bsw/jbe@1309: 
bsw/jbe@1309:   wysihtml.Editor = wysihtml.lang.Dispatcher.extend({
bsw/jbe@1309:     /** @scope wysihtml.Editor.prototype */
bsw/jbe@1309:     defaults: {
bsw/jbe@1309:       // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
bsw/jbe@1309:       name:                 undef,
bsw/jbe@1309:       // Whether the editor should look like the textarea (by adopting styles)
bsw/jbe@1309:       style:                true,
bsw/jbe@1309:       // Whether urls, entered by the user should automatically become clickable-links
bsw/jbe@1309:       autoLink:             true,
bsw/jbe@1309:       // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
bsw/jbe@1309:       handleTabKey:         true,
bsw/jbe@1309:       // Object which includes parser rules to apply when html gets cleaned
bsw/jbe@1309:       // See parser_rules/*.js for examples
bsw/jbe@1309:       parserRules:          { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} },
bsw/jbe@1309:       // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
bsw/jbe@1309:       pasteParserRulesets: null,
bsw/jbe@1309:       // Parser method to use when the user inserts content
bsw/jbe@1309:       parser:               wysihtml.dom.parse,
bsw/jbe@1309:       // By default wysihtml will insert a 
 for line breaks, set this to false to use 
bsw/jbe@1309:       useLineBreaks:        true,
bsw/jbe@1309:       // Double enter (enter on blank line) exits block element in useLineBreaks mode.
bsw/jbe@1309:       // It enables a way of escaping out of block elements and splitting block elements
bsw/jbe@1309:       doubleLineBreakEscapesBlock: true,
bsw/jbe@1309:       // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
bsw/jbe@1309:       stylesheets:          [],
bsw/jbe@1309:       // Placeholder text to use, defaults to the placeholder attribute on the textarea element
bsw/jbe@1309:       placeholderText:      undef,
bsw/jbe@1309:       // Whether the rich text editor should be rendered on touch devices (wysihtml >= 0.3.0 comes with basic support for iOS 5)
bsw/jbe@1309:       supportTouchDevices:  true,
bsw/jbe@1309:       // Whether senseless  elements (empty or without attributes) should be removed/replaced with their content
bsw/jbe@1309:       cleanUp:              true,
bsw/jbe@1309:       // Whether to use div instead of secure iframe
bsw/jbe@1309:       contentEditableMode: false,
bsw/jbe@1309:       classNames: {
bsw/jbe@1309:         // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
bsw/jbe@1309:         composer: "wysihtml-editor",
bsw/jbe@1309:         // Class name to add to the body when the wysihtml editor is supported
bsw/jbe@1309:         body: "wysihtml-supported",
bsw/jbe@1309:         // classname added to editable area element (iframe/div) on creation
bsw/jbe@1309:         sandbox: "wysihtml-sandbox",
bsw/jbe@1309:         // class on editable area with placeholder
bsw/jbe@1309:         placeholder: "wysihtml-placeholder",
bsw/jbe@1309:         // Classname of container that editor should not touch and pass through
bsw/jbe@1309:         uneditableContainer: "wysihtml-uneditable-container"
bsw/jbe@1309:       },
bsw/jbe@1309:       // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste)
bsw/jbe@1309:       // Also copied source is based directly on selection - 
bsw/jbe@1309:       // (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).
bsw/jbe@1309:       // If falsy value is passed source override is also disabled
bsw/jbe@1309:       copyedFromMarking: ''
bsw/jbe@1309:     },
bsw/jbe@1309:     
bsw/jbe@1309:     constructor: function(editableElement, config) {
bsw/jbe@1309:       this.editableElement  = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement;
bsw/jbe@1309:       this.config           = wysihtml.lang.object({}).merge(this.defaults).merge(config).get();
bsw/jbe@1309:       this._isCompatible    = wysihtml.browser.supported();
bsw/jbe@1309: 
bsw/jbe@1309:       // merge classNames
bsw/jbe@1309:       if (config && config.classNames) {
bsw/jbe@1309:         wysihtml.lang.object(this.config.classNames).merge(config.classNames);
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       if (this.editableElement.nodeName.toLowerCase() != "textarea") {
bsw/jbe@1309:           this.config.contentEditableMode = true;
bsw/jbe@1309:           this.config.noTextarea = true;
bsw/jbe@1309:       }
bsw/jbe@1309:       if (!this.config.noTextarea) {
bsw/jbe@1309:           this.textarea         = new wysihtml.views.Textarea(this, this.editableElement, this.config);
bsw/jbe@1309:           this.currentView      = this.textarea;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Sort out unsupported/unwanted browsers here
bsw/jbe@1309:       if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml.browser.isTouchDevice())) {
bsw/jbe@1309:         var that = this;
bsw/jbe@1309:         setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
bsw/jbe@1309:         return;
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       // Add class name to body, to indicate that the editor is supported
bsw/jbe@1309:       wysihtml.dom.addClass(document.body, this.config.classNames.body);
bsw/jbe@1309: 
bsw/jbe@1309:       this.composer = new wysihtml.views.Composer(this, this.editableElement, this.config);
bsw/jbe@1309:       this.currentView = this.composer;
bsw/jbe@1309: 
bsw/jbe@1309:       if (typeof(this.config.parser) === "function") {
bsw/jbe@1309:         this._initParser();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.on("beforeload", this.handleBeforeLoad);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     handleBeforeLoad: function() {
bsw/jbe@1309:         if (!this.config.noTextarea) {
bsw/jbe@1309:           this.synchronizer = new wysihtml.views.Synchronizer(this, this.textarea, this.composer);
bsw/jbe@1309:         } else {
bsw/jbe@1309:           this.sourceView = new wysihtml.views.SourceView(this, this.composer);
bsw/jbe@1309:         }
bsw/jbe@1309:         this.runEditorExtenders();
bsw/jbe@1309:     },
bsw/jbe@1309:     
bsw/jbe@1309:     runEditorExtenders: function() {
bsw/jbe@1309:       wysihtml.editorExtenders.forEach(function(extender) {
bsw/jbe@1309:         extender(this);
bsw/jbe@1309:       }.bind(this));
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     isCompatible: function() {
bsw/jbe@1309:       return this._isCompatible;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     clear: function() {
bsw/jbe@1309:       this.currentView.clear();
bsw/jbe@1309:       return this;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     getValue: function(parse, clearInternals) {
bsw/jbe@1309:       return this.currentView.getValue(parse, clearInternals);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     setValue: function(html, parse) {
bsw/jbe@1309:       this.fire("unset_placeholder");
bsw/jbe@1309: 
bsw/jbe@1309:       if (!html) {
bsw/jbe@1309:         return this.clear();
bsw/jbe@1309:       }
bsw/jbe@1309: 
bsw/jbe@1309:       this.currentView.setValue(html, parse);
bsw/jbe@1309:       return this;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     cleanUp: function(rules) {
bsw/jbe@1309:         this.currentView.cleanUp(rules);
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     focus: function(setToEnd) {
bsw/jbe@1309:       this.currentView.focus(setToEnd);
bsw/jbe@1309:       return this;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Deactivate editor (make it readonly)
bsw/jbe@1309:      */
bsw/jbe@1309:     disable: function() {
bsw/jbe@1309:       this.currentView.disable();
bsw/jbe@1309:       return this;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Activate editor
bsw/jbe@1309:      */
bsw/jbe@1309:     enable: function() {
bsw/jbe@1309:       this.currentView.enable();
bsw/jbe@1309:       return this;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     isEmpty: function() {
bsw/jbe@1309:       return this.currentView.isEmpty();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     hasPlaceholderSet: function() {
bsw/jbe@1309:       return this.currentView.hasPlaceholderSet();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     destroy: function() {
bsw/jbe@1309:       if (this.composer && this.composer.sandbox) {
bsw/jbe@1309:         this.composer.sandbox.destroy();
bsw/jbe@1309:       }
bsw/jbe@1309:       this.fire("destroy:composer");
bsw/jbe@1309:       this.off();
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     parse: function(htmlOrElement, clearInternals, customRules) {
bsw/jbe@1309:       var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null);
bsw/jbe@1309:       var returnValue = this.config.parser(htmlOrElement, {
bsw/jbe@1309:         "rules": customRules || this.config.parserRules,
bsw/jbe@1309:         "cleanUp": this.config.cleanUp,
bsw/jbe@1309:         "context": parseContext,
bsw/jbe@1309:         "uneditableClass": this.config.classNames.uneditableContainer,
bsw/jbe@1309:         "clearInternals" : clearInternals
bsw/jbe@1309:       });
bsw/jbe@1309:       if (typeof(htmlOrElement) === "object") {
bsw/jbe@1309:         wysihtml.quirks.redraw(htmlOrElement);
bsw/jbe@1309:       }
bsw/jbe@1309:       return returnValue;
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     /**
bsw/jbe@1309:      * Prepare html parser logic
bsw/jbe@1309:      *  - Observes for paste and drop
bsw/jbe@1309:      */
bsw/jbe@1309:     _initParser: function() {
bsw/jbe@1309:       var oldHtml;
bsw/jbe@1309: 
bsw/jbe@1309:       if (wysihtml.browser.supportsModernPaste()) {
bsw/jbe@1309:         this.on("paste:composer", function(event) {
bsw/jbe@1309:           event.preventDefault();
bsw/jbe@1309:           oldHtml = wysihtml.dom.getPastedHtml(event);
bsw/jbe@1309:           if (oldHtml) {
bsw/jbe@1309:             this._cleanAndPaste(oldHtml);
bsw/jbe@1309:           }
bsw/jbe@1309:         }.bind(this));
bsw/jbe@1309: 
bsw/jbe@1309:       } else {
bsw/jbe@1309:         this.on("beforepaste:composer", function(event) {
bsw/jbe@1309:           event.preventDefault();
bsw/jbe@1309:           var scrollPos = this.composer.getScrollPos();
bsw/jbe@1309: 
bsw/jbe@1309:           wysihtml.dom.getPastedHtmlWithDiv(this.composer, function(pastedHTML) {
bsw/jbe@1309:             if (pastedHTML) {
bsw/jbe@1309:               this._cleanAndPaste(pastedHTML);
bsw/jbe@1309:             }
bsw/jbe@1309:             this.composer.setScrollPos(scrollPos);
bsw/jbe@1309:           }.bind(this));
bsw/jbe@1309: 
bsw/jbe@1309:         }.bind(this));
bsw/jbe@1309:       }
bsw/jbe@1309:     },
bsw/jbe@1309: 
bsw/jbe@1309:     _cleanAndPaste: function (oldHtml) {
bsw/jbe@1309:       var cleanHtml = wysihtml.quirks.cleanPastedHTML(oldHtml, {
bsw/jbe@1309:         "referenceNode": this.composer.element,
bsw/jbe@1309:         "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}],
bsw/jbe@1309:         "uneditableClass": this.config.classNames.uneditableContainer
bsw/jbe@1309:       });
bsw/jbe@1309:       this.composer.selection.deleteContents();
bsw/jbe@1309:       this.composer.selection.insertHTML(cleanHtml);
bsw/jbe@1309:     }
bsw/jbe@1309:   });
bsw/jbe@1309: })(wysihtml);