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: bsw/jbe@1309: bsw/jbe@1309: var range = document.selection.createRange(); bsw/jbe@1309: alert(range.parentElement().id); // Should alert "ul" but alerts "b" bsw/jbe@1309: bsw/jbe@1309: This method returns the common ancestor node of the following: bsw/jbe@1309: - the parentElement() of the textRange bsw/jbe@1309: - the parentElement() of the textRange after calling collapse(true) bsw/jbe@1309: - the parentElement() of the textRange after calling collapse(false) bsw/jbe@1309: */ bsw/jbe@1309: var getTextRangeContainerElement = function(textRange) { bsw/jbe@1309: var parentEl = textRange.parentElement(); bsw/jbe@1309: var range = textRange.duplicate(); bsw/jbe@1309: range.collapse(true); bsw/jbe@1309: var startEl = range.parentElement(); bsw/jbe@1309: range = textRange.duplicate(); bsw/jbe@1309: range.collapse(false); bsw/jbe@1309: var endEl = range.parentElement(); bsw/jbe@1309: var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); bsw/jbe@1309: bsw/jbe@1309: return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); bsw/jbe@1309: }; bsw/jbe@1309: bsw/jbe@1309: var textRangeIsCollapsed = function(textRange) { bsw/jbe@1309: return textRange.compareEndPoints("StartToEnd", textRange) == 0; bsw/jbe@1309: }; bsw/jbe@1309: bsw/jbe@1309: // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started bsw/jbe@1309: // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) bsw/jbe@1309: // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bsw/jbe@1309: // bugs, handling for inputs and images, plus optimizations. bsw/jbe@1309: var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { bsw/jbe@1309: var workingRange = textRange.duplicate(); bsw/jbe@1309: workingRange.collapse(isStart); bsw/jbe@1309: var containerElement = workingRange.parentElement(); bsw/jbe@1309: bsw/jbe@1309: // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so bsw/jbe@1309: // check for that bsw/jbe@1309: if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { bsw/jbe@1309: containerElement = wholeRangeContainerElement; bsw/jbe@1309: } bsw/jbe@1309: bsw/jbe@1309: bsw/jbe@1309: // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and bsw/jbe@1309: // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx bsw/jbe@1309: if (!containerElement.canHaveHTML) { bsw/jbe@1309: var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); bsw/jbe@1309: return { bsw/jbe@1309: boundaryPosition: pos, bsw/jbe@1309: nodeInfo: { bsw/jbe@1309: nodeIndex: pos.offset, bsw/jbe@1309: containerElement: pos.node bsw/jbe@1309: } bsw/jbe@1309: }; bsw/jbe@1309: } bsw/jbe@1309: bsw/jbe@1309: var workingNode = dom.getDocument(containerElement).createElement("span"); bsw/jbe@1309: bsw/jbe@1309: // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 bsw/jbe@1309: // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 bsw/jbe@1309: if (workingNode.parentNode) { bsw/jbe@1309: dom.removeNode(workingNode); bsw/jbe@1309: } bsw/jbe@1309: bsw/jbe@1309: var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; bsw/jbe@1309: var previousNode, nextNode, boundaryPosition, boundaryNode; bsw/jbe@1309: var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; bsw/jbe@1309: var childNodeCount = containerElement.childNodes.length; bsw/jbe@1309: var end = childNodeCount; bsw/jbe@1309: bsw/jbe@1309: // Check end first. Code within the loop assumes that the endth child node of the container is definitely bsw/jbe@1309: // after the range boundary. bsw/jbe@1309: var nodeIndex = end; bsw/jbe@1309: bsw/jbe@1309: while (true) { bsw/jbe@1309: if (nodeIndex == childNodeCount) { bsw/jbe@1309: containerElement.appendChild(workingNode); bsw/jbe@1309: } else { bsw/jbe@1309: containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); bsw/jbe@1309: } bsw/jbe@1309: workingRange.moveToElementText(workingNode); bsw/jbe@1309: comparison = workingRange.compareEndPoints(workingComparisonType, textRange); bsw/jbe@1309: if (comparison == 0 || start == end) { bsw/jbe@1309: break; bsw/jbe@1309: } else if (comparison == -1) { bsw/jbe@1309: if (end == start + 1) { bsw/jbe@1309: // We know the endth child node is after the range boundary, so we must be done. bsw/jbe@1309: break; bsw/jbe@1309: } else { bsw/jbe@1309: start = nodeIndex; bsw/jbe@1309: } bsw/jbe@1309: } else { bsw/jbe@1309: end = (end == start + 1) ? start : nodeIndex; bsw/jbe@1309: } bsw/jbe@1309: nodeIndex = Math.floor((start + end) / 2); bsw/jbe@1309: containerElement.removeChild(workingNode); bsw/jbe@1309: } bsw/jbe@1309: bsw/jbe@1309: bsw/jbe@1309: // We've now reached or gone past the boundary of the text range we're interested in bsw/jbe@1309: // so have identified the node we want bsw/jbe@1309: boundaryNode = workingNode.nextSibling; bsw/jbe@1309: bsw/jbe@1309: if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { bsw/jbe@1309: // This is a character data node (text, comment, cdata). The working range is collapsed at the start of bsw/jbe@1309: // the node containing the text range's boundary, so we move the end of the working range to the bsw/jbe@1309: // boundary point and measure the length of its text to get the boundary's offset within the node. bsw/jbe@1309: workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); bsw/jbe@1309: bsw/jbe@1309: var offset; bsw/jbe@1309: bsw/jbe@1309: if (/[\r\n]/.test(boundaryNode.data)) { bsw/jbe@1309: /* bsw/jbe@1309: For the particular case of a boundary within a text node containing rendered line breaks (within a 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: *
  • eminem
  • bsw/jbe@1309: *
  • dr. dre
  • bsw/jbe@1309: *
  • 50 Cent
  • bsw/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 = '
foo
bar
'; 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 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: *
  • 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: *
  1. eminem
  2. bsw/jbe@1309: *
  3. dr. dre
  4. bsw/jbe@1309: *
  5. 50 Cent
  6. bsw/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: *
  • 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: * 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: 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: //
  1. foo
  2. 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
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);