From 76216dcbe05449c9cbeceef4810b790653176e28 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Mon, 21 Oct 2024 16:25:44 +0200 Subject: [PATCH 1/2] Vendor wysihtml rich text editor Original project is unmaintained. We need to apply fixes for current Chrome versions. --- lib/pageflow/engine.rb | 1 - pageflow.gemspec | 3 - vendor/assets/javascripts/wysihtml-toolbar.js | 19308 ++++++++++++++++ 3 files changed, 19308 insertions(+), 4 deletions(-) create mode 100644 vendor/assets/javascripts/wysihtml-toolbar.js diff --git a/lib/pageflow/engine.rb b/lib/pageflow/engine.rb index a158a26578..a4611d508c 100644 --- a/lib/pageflow/engine.rb +++ b/lib/pageflow/engine.rb @@ -23,7 +23,6 @@ require 'marionette-rails' require 'jquery-fileupload-rails' require 'jquery-minicolors-rails' -require 'wysihtml/rails' require 'i18n-js' require 'http_accept_language' require 'pageflow-public-i18n' diff --git a/pageflow.gemspec b/pageflow.gemspec index 12d9df1c76..3cb336e94b 100644 --- a/pageflow.gemspec +++ b/pageflow.gemspec @@ -116,9 +116,6 @@ Gem::Specification.new do |s| # Using translations from rails locales in javascript code. s.add_dependency 'i18n-js', '~> 2.1' - # WYSIWYG editor - s.add_dependency 'wysihtml-rails', '0.5.5' - s.add_dependency 'bourbon', '~> 3.1.8' # Pretty URLs diff --git a/vendor/assets/javascripts/wysihtml-toolbar.js b/vendor/assets/javascripts/wysihtml-toolbar.js new file mode 100644 index 0000000000..f1fcca7be3 --- /dev/null +++ b/vendor/assets/javascripts/wysihtml-toolbar.js @@ -0,0 +1,19308 @@ +/** + * @license wysihtml v0.5.5 + * https://github.com/Voog/wysihtml + * + * Author: Christopher Blum (https://github.com/tiff) + * Secondary author of extended features: Oliver Pulges (https://github.com/pulges) + * + * Copyright (C) 2012 XING AG + * Licensed under the MIT license (MIT) + * + */ +var wysihtml5 = { + version: "0.5.5", + + // namespaces + commands: {}, + dom: {}, + quirks: {}, + toolbar: {}, + lang: {}, + selection: {}, + views: {}, + + INVISIBLE_SPACE: "\uFEFF", + INVISIBLE_SPACE_REG_EXP: /\uFEFF/g, + + VOID_ELEMENTS: "area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr", + + EMPTY_FUNCTION: function() {}, + + ELEMENT_NODE: 1, + TEXT_NODE: 3, + + BACKSPACE_KEY: 8, + ENTER_KEY: 13, + ESCAPE_KEY: 27, + SPACE_KEY: 32, + TAB_KEY: 9, + DELETE_KEY: 46 +}; +;wysihtml5.polyfills = function(win, doc) { + + // TODO: in future try to replace most inline compability checks with polyfills for code readability + + // IE8 SUPPORT BLOCK + // You can compile without all this if IE8 is not needed + + // String trim for ie8 + if (!String.prototype.trim) { + (function() { + // Make sure we trim BOM and NBSP + var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + String.prototype.trim = function() { + return this.replace(rtrim, ''); + }; + })(); + } + + // addEventListener, removeEventListener + (function() { + var s_add = 'addEventListener', + s_rem = 'removeEventListener'; + if( doc[s_add] ) return; + win.Element.prototype[ s_add ] = win[ s_add ] = doc[ s_add ] = function( on, fn, self ) { + return (self = this).attachEvent( 'on' + on, function(e){ + var e = e || win.event; + e.target = e.target || e.srcElement; + e.preventDefault = e.preventDefault || function(){e.returnValue = false}; + e.stopPropagation = e.stopPropagation || function(){e.cancelBubble = true}; + e.which = e.button ? ( e.button === 2 ? 3 : e.button === 4 ? 2 : e.button ) : e.keyCode; + fn.call(self, e); + }); + }; + win.Element.prototype[ s_rem ] = win[ s_rem ] = doc[ s_rem ] = function( on, fn ) { + return this.detachEvent( 'on' + on, fn ); + }; + })(); + + // element.textContent polyfill. + if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(win.Element.prototype, "textContent").get) { + (function() { + var innerText = Object.getOwnPropertyDescriptor(win.Element.prototype, "innerText"); + Object.defineProperty(win.Element.prototype, "textContent", + { + get: function() { + return innerText.get.call(this); + }, + set: function(s) { + return innerText.set.call(this, s); + } + } + ); + })(); + } + + // isArray polyfill for ie8 + if(!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + // Array indexOf for ie8 + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(a,f) { + for(var c=this.length,r=-1,d=f>>>0; ~(c-d); r=this[--c]===a?c:r); + return r; + }; + } + + // Function.prototype.bind() + // TODO: clean the code from variable 'that' as it can be confusing + if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; + } + + // closest and matches polyfill + // https://github.com/jonathantneal/closest + (function (ELEMENT) { + ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) { + var + element = this, + elements = (element.document || element.ownerDocument).querySelectorAll(selector), + index = 0; + + while (elements[index] && elements[index] !== element) { + ++index; + } + + return elements[index] ? true : false; + }; + + ELEMENT.closest = ELEMENT.closest || function closest(selector) { + var element = this; + + while (element) { + if (element.matches(selector)) { + break; + } + + element = element.parentElement; + } + + return element; + }; + }(Element.prototype)); + + // Element.classList for ie8-9 (toggle all IE) + // source http://purl.eligrey.com/github/classList.js/blob/master/classList.js + + if ("document" in win) { + // Full polyfill for browsers with no classList support + if (!("classList" in doc.createElement("_"))) { + (function(view) { + "use strict"; + if (!('Element' in view)) return; + + var + classListProp = "classList", + protoProp = "prototype", + elemCtrProto = view.Element[protoProp], + objCtr = Object, + strTrim = String[protoProp].trim || function() { + return this.replace(/^\s+|\s+$/g, ""); + }, + arrIndexOf = Array[protoProp].indexOf || function(item) { + var + i = 0, + len = this.length; + for (; i < len; i++) { + if (i in this && this[i] === item) { + return i; + } + } + return -1; + }, // Vendors: please allow content code to instantiate DOMExceptions + DOMEx = function(type, message) { + this.name = type; + this.code = DOMException[type]; + this.message = message; + }, + checkTokenAndGetIndex = function(classList, token) { + if (token === "") { + throw new DOMEx( + "SYNTAX_ERR", "An invalid or illegal string was specified" + ); + } + if (/\s/.test(token)) { + throw new DOMEx( + "INVALID_CHARACTER_ERR", "String contains an invalid character" + ); + } + return arrIndexOf.call(classList, token); + }, + ClassList = function(elem) { + var + trimmedClasses = strTrim.call(elem.getAttribute("class") || ""), + classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [], + i = 0, + len = classes.length; + for (; i < len; i++) { + this.push(classes[i]); + } + this._updateClassName = function() { + elem.setAttribute("class", this.toString()); + }; + }, + classListProto = ClassList[protoProp] = [], + classListGetter = function() { + return new ClassList(this); + }; + // Most DOMException implementations don't allow calling DOMException's toString() + // on non-DOMExceptions. Error's toString() is sufficient here. + DOMEx[protoProp] = Error[protoProp]; + classListProto.item = function(i) { + return this[i] || null; + }; + classListProto.contains = function(token) { + token += ""; + return checkTokenAndGetIndex(this, token) !== -1; + }; + classListProto.add = function() { + var + tokens = arguments, + i = 0, + l = tokens.length, + token, updated = false; + do { + token = tokens[i] + ""; + if (checkTokenAndGetIndex(this, token) === -1) { + this.push(token); + updated = true; + } + } + while (++i < l); + + if (updated) { + this._updateClassName(); + } + }; + classListProto.remove = function() { + var + tokens = arguments, + i = 0, + l = tokens.length, + token, updated = false, + index; + do { + token = tokens[i] + ""; + index = checkTokenAndGetIndex(this, token); + while (index !== -1) { + this.splice(index, 1); + updated = true; + index = checkTokenAndGetIndex(this, token); + } + } + while (++i < l); + + if (updated) { + this._updateClassName(); + } + }; + classListProto.toggle = function(token, force) { + token += ""; + + var + result = this.contains(token), + method = result ? + force !== true && "remove" : + force !== false && "add"; + + if (method) { + this[method](token); + } + + if (force === true || force === false) { + return force; + } else { + return !result; + } + }; + classListProto.toString = function() { + return this.join(" "); + }; + + if (objCtr.defineProperty) { + var classListPropDesc = { + get: classListGetter, + enumerable: true, + configurable: true + }; + try { + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } catch (ex) { // IE 8 doesn't support enumerable:true + if (ex.number === -0x7FF5EC54) { + classListPropDesc.enumerable = false; + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } + } + } else if (objCtr[protoProp].__defineGetter__) { + elemCtrProto.__defineGetter__(classListProp, classListGetter); + } + + }(win)); + + } else if ("DOMTokenList" in win) { + // There is full or partial native classList support, so just check if we need + // to normalize the add/remove and toggle APIs. + // DOMTokenList is expected to exist (removes conflicts with multiple polyfills present on site) + + (function() { + "use strict"; + + var testElement = doc.createElement("_"); + + testElement.classList.add("c1", "c2"); + + // Polyfill for IE 10/11 and Firefox <26, where classList.add and + // classList.remove exist but support only one argument at a time. + if (!testElement.classList.contains("c2")) { + var createMethod = function(method) { + var original = win.DOMTokenList.prototype[method]; + + win.DOMTokenList.prototype[method] = function(token) { + var i, len = arguments.length; + + for (i = 0; i < len; i++) { + token = arguments[i]; + original.call(this, token); + } + }; + }; + createMethod('add'); + createMethod('remove'); + } + + testElement.classList.toggle("c3", false); + + // Polyfill for IE 10 and Firefox <24, where classList.toggle does not + // support the second argument. + if (testElement.classList.contains("c3")) { + var _toggle = win.DOMTokenList.prototype.toggle; + + win.DOMTokenList.prototype.toggle = function(token, force) { + if (1 in arguments && !this.contains(token) === !force) { + return force; + } else { + return _toggle.call(this, token); + } + }; + + } + + testElement = null; + }()); + + } + + } + + // Safary has a bug of not restoring selection after node.normalize correctly. + // Detects the misbegaviour and patches it + var normalizeHasCaretError = function() { + if ("createRange" in document && "getSelection" in window) { + var e = document.createElement('div'), + t1 = document.createTextNode('a'), + t2 = document.createTextNode('a'), + t3 = document.createTextNode('a'), + r = document.createRange(), + s, ret; + + e.setAttribute('contenteditable', 'true'); + e.appendChild(t1); + e.appendChild(t2); + e.appendChild(t3); + document.body.appendChild(e); + r.setStart(t2, 1); + r.setEnd(t2, 1); + + s = window.getSelection(); + s.removeAllRanges(); + s.addRange(r); + e.normalize(); + s = window.getSelection(); + + ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2); + e.parentNode.removeChild(e); + s.removeAllRanges(); + return ret; + } + }; + + var getTextNodes = function(node){ + var all = []; + for (node=node.firstChild;node;node=node.nextSibling){ + if (node.nodeType == 3) { + all.push(node); + } else { + all = all.concat(getTextNodes(node)); + } + } + return all; + }; + + var isInDom = function(node) { + var doc = node.ownerDocument, + n = node; + + do { + if (n === doc) { + return true; + } + n = n.parentNode; + } while(n); + + return false; + }; + + var normalizeFix = function() { + var f = Node.prototype.normalize; + var nf = function() { + var texts = getTextNodes(this), + s = this.ownerDocument.defaultView.getSelection(), + anode = s.anchorNode, + aoffset = s.anchorOffset, + aelement = anode && anode.nodeType === 1 && anode.childNodes.length > 0 ? anode.childNodes[aoffset] : undefined, + fnode = s.focusNode, + foffset = s.focusOffset, + felement = fnode && fnode.nodeType === 1 && foffset > 0 ? fnode.childNodes[foffset -1] : undefined, + r = this.ownerDocument.createRange(), + prevTxt = texts.shift(), + curText = prevTxt ? texts.shift() : null; + + if (felement && felement.nodeType === 3) { + fnode = felement; + foffset = felement.nodeValue.length; + felement = undefined; + } + + if (aelement && aelement.nodeType === 3) { + anode = aelement; + aoffset = 0; + aelement = undefined; + } + + if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & Node.DOCUMENT_POSITION_CONTAINS))) { + fnode = [anode, anode = fnode][0]; + foffset = [aoffset, aoffset = foffset][0]; + } + + while(prevTxt && curText) { + if (curText.previousSibling && curText.previousSibling === prevTxt) { + if (anode === curText) { + anode = prevTxt; + aoffset = prevTxt.nodeValue.length + aoffset; + } + if (fnode === curText) { + fnode = prevTxt; + foffset = prevTxt.nodeValue.length + foffset; + } + prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue; + curText.parentNode.removeChild(curText); + curText = texts.shift(); + } else { + prevTxt = curText; + curText = texts.shift(); + } + } + + if (felement) { + foffset = Array.prototype.indexOf.call(felement.parentNode.childNodes, felement) + 1; + } + + if (aelement) { + aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement); + } + + if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) { + r.setStart(anode, aoffset); + r.setEnd(fnode, foffset); + s.removeAllRanges(); + s.addRange(r); + } + }; + Node.prototype.normalize = nf; + }; + + var F = function() { + window.removeEventListener("load", F); + if ("Node" in window && "normalize" in Node.prototype && normalizeHasCaretError()) { + normalizeFix(); + } + }; + + if (doc.readyState !== "complete") { + window.addEventListener("load", F); + } else { + F(); + } + + // CustomEvent for ie9 and up + function nativeCustomEventSupported() { + try { + var p = new CustomEvent('cat', {detail: {foo: 'bar'}}); + return 'cat' === p.type && 'bar' === p.detail.foo; + } catch (e) {} + return false; + } + var customEventSupported = nativeCustomEventSupported(); + + // Polyfills CustomEvent object for IE9 and up + (function() { + if (!customEventSupported && "CustomEvent" in window) { + function CustomEvent(event, params) { + params = params || {bubbles: false, cancelable: false, detail: undefined}; + var evt = doc.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } + CustomEvent.prototype = win.Event.prototype; + win.CustomEvent = CustomEvent; + customEventSupported = true; + } + })(); +}; + +wysihtml5.polyfills(window, document); +;/** + * Rangy, a cross-browser JavaScript range and selection library + * https://github.com/timdown/rangy + * + * Copyright 2015, Tim Down + * Licensed under the MIT license. + * Version: 1.3.0 + * Build date: 10 May 2015 + */ + +(function(factory, root) { + if (typeof define == "function" && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } else if (typeof module != "undefined" && typeof exports == "object") { + // Node/CommonJS style + module.exports = factory(); + } else { + // No AMD or CommonJS support so we place Rangy in (probably) the global variable + root.rangy = factory(); + } +})(function() { + + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; + + // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START + // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. + var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + // Minimal set of methods required for DOM Level 2 Range compliance + var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", + "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", + "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; + + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; + + // Subset of TextRange's full set of methods that we're interested in + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", + "setEndPoint", "getBoundingClientRect"]; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Trio of functions taken from Peter Michaux's article: + // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting + function isHostMethod(o, p) { + var t = typeof o[p]; + return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; + } + + function isHostObject(o, p) { + return !!(typeof o[p] == OBJECT && o[p]); + } + + function isHostProperty(o, p) { + return typeof o[p] != UNDEFINED; + } + + // Creates a convenience function to save verbose repeated calls to tests functions + function createMultiplePropertyTest(testFunc) { + return function(o, props) { + var i = props.length; + while (i--) { + if (!testFunc(o, props[i])) { + return false; + } + } + return true; + }; + } + + // Next trio of functions are a convenience to save verbose repeated calls to previous two functions + var areHostMethods = createMultiplePropertyTest(isHostMethod); + var areHostObjects = createMultiplePropertyTest(isHostObject); + var areHostProperties = createMultiplePropertyTest(isHostProperty); + + function isTextRange(range) { + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); + } + + function getBody(doc) { + return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; + } + + var forEach = [].forEach ? + function(arr, func) { + arr.forEach(func); + } : + function(arr, func) { + for (var i = 0, len = arr.length; i < len; ++i) { + func(arr[i], i); + } + }; + + var modules = {}; + + var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); + + var util = { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange, + getBody: getBody, + forEach: forEach + }; + + var api = { + version: "1.3.0", + initialized: false, + isBrowser: isBrowser, + supported: true, + util: util, + features: {}, + modules: modules, + config: { + alertOnFail: false, + alertOnWarn: false, + preferTextRange: false, + autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize + } + }; + + function consoleLog(msg) { + if (typeof console != UNDEFINED && isHostMethod(console, "log")) { + console.log(msg); + } + } + + function alertOrLog(msg, shouldAlert) { + if (isBrowser && shouldAlert) { + alert(msg); + } else { + consoleLog(msg); + } + } + + function fail(reason) { + api.initialized = true; + api.supported = false; + alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); + } + + api.fail = fail; + + function warn(msg) { + alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); + } + + api.warn = warn; + + // Add utility extend() method + var extend; + if ({}.hasOwnProperty) { + util.extend = extend = function(obj, props, deep) { + var o, p; + for (var i in props) { + if (props.hasOwnProperty(i)) { + o = obj[i]; + p = props[i]; + if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { + extend(o, p, true); + } + obj[i] = p; + } + } + // Special case for toString, which does not show up in for...in loops in IE <= 8 + if (props.hasOwnProperty("toString")) { + obj.toString = props.toString; + } + return obj; + }; + + util.createOptions = function(optionsParam, defaults) { + var options = {}; + extend(options, defaults); + if (optionsParam) { + extend(options, optionsParam); + } + return options; + }; + } else { + fail("hasOwnProperty not supported"); + } + + // Test whether we're in a browser and bail out if not + if (!isBrowser) { + fail("Rangy can only run in a browser"); + } + + // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not + (function() { + var toArray; + + if (isBrowser) { + var el = document.createElement("div"); + el.appendChild(document.createElement("span")); + var slice = [].slice; + try { + if (slice.call(el.childNodes, 0)[0].nodeType == 1) { + toArray = function(arrayLike) { + return slice.call(arrayLike, 0); + }; + } + } catch (e) {} + } + + if (!toArray) { + toArray = function(arrayLike) { + var arr = []; + for (var i = 0, len = arrayLike.length; i < len; ++i) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + util.toArray = toArray; + })(); + + // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or + // normalization of event properties + var addListener; + if (isBrowser) { + if (isHostMethod(document, "addEventListener")) { + addListener = function(obj, eventType, listener) { + obj.addEventListener(eventType, listener, false); + }; + } else if (isHostMethod(document, "attachEvent")) { + addListener = function(obj, eventType, listener) { + obj.attachEvent("on" + eventType, listener); + }; + } else { + fail("Document does not have required addEventListener or attachEvent method"); + } + + util.addListener = addListener; + } + + var initListeners = []; + + function getErrorDesc(ex) { + return ex.message || ex.description || String(ex); + } + + // Initialization + function init() { + if (!isBrowser || api.initialized) { + return; + } + var testRange; + var implementsDomRange = false, implementsTextRange = false; + + // First, perform basic feature tests + + if (isHostMethod(document, "createRange")) { + testRange = document.createRange(); + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { + implementsDomRange = true; + } + } + + var body = getBody(document); + if (!body || body.nodeName.toLowerCase() != "body") { + fail("No body element found"); + return; + } + + if (body && isHostMethod(body, "createTextRange")) { + testRange = body.createTextRange(); + if (isTextRange(testRange)) { + implementsTextRange = true; + } + } + + if (!implementsDomRange && !implementsTextRange) { + fail("Neither Range nor TextRange are available"); + return; + } + + api.initialized = true; + api.features = { + implementsDomRange: implementsDomRange, + implementsTextRange: implementsTextRange + }; + + // Initialize modules + var module, errorMessage; + for (var moduleName in modules) { + if ( (module = modules[moduleName]) instanceof Module ) { + module.init(module, api); + } + } + + // Call init listeners + for (var i = 0, len = initListeners.length; i < len; ++i) { + try { + initListeners[i](api); + } catch (ex) { + errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); + consoleLog(errorMessage); + } + } + } + + function deprecationNotice(deprecated, replacement, module) { + if (module) { + deprecated += " in module " + module.name; + } + api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " + + replacement + " instead."); + } + + function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) { + owner[deprecated] = function() { + deprecationNotice(deprecated, replacement, module); + return owner[replacement].apply(owner, util.toArray(arguments)); + }; + } + + util.deprecationNotice = deprecationNotice; + util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod; + + // Allow external scripts to initialize this library in case it's loaded after the document has loaded + api.init = init; + + // Execute listener immediately if already initialized + api.addInitListener = function(listener) { + if (api.initialized) { + listener(api); + } else { + initListeners.push(listener); + } + }; + + var shimListeners = []; + + api.addShimListener = function(listener) { + shimListeners.push(listener); + }; + + function shim(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = shimListeners.length; i < len; ++i) { + shimListeners[i](win); + } + } + + if (isBrowser) { + api.shim = api.createMissingNativeApi = shim; + createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); + } + + function Module(name, dependencies, initializer) { + this.name = name; + this.dependencies = dependencies; + this.initialized = false; + this.supported = false; + this.initializer = initializer; + } + + Module.prototype = { + init: function() { + var requiredModuleNames = this.dependencies || []; + for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { + moduleName = requiredModuleNames[i]; + + requiredModule = modules[moduleName]; + if (!requiredModule || !(requiredModule instanceof Module)) { + throw new Error("required module '" + moduleName + "' not found"); + } + + requiredModule.init(); + + if (!requiredModule.supported) { + throw new Error("required module '" + moduleName + "' not supported"); + } + } + + // Now run initializer + this.initializer(this); + }, + + fail: function(reason) { + this.initialized = true; + this.supported = false; + throw new Error(reason); + }, + + warn: function(msg) { + api.warn("Module " + this.name + ": " + msg); + }, + + deprecationNotice: function(deprecated, replacement) { + api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " + + replacement + " instead"); + }, + + createError: function(msg) { + return new Error("Error in Rangy " + this.name + " module: " + msg); + } + }; + + function createModule(name, dependencies, initFunc) { + var newModule = new Module(name, dependencies, function(module) { + if (!module.initialized) { + module.initialized = true; + try { + initFunc(api, module); + module.supported = true; + } catch (ex) { + var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); + consoleLog(errorMessage); + if (ex.stack) { + consoleLog(ex.stack); + } + } + } + }); + modules[name] = newModule; + return newModule; + } + + api.createModule = function(name) { + // Allow 2 or 3 arguments (second argument is an optional array of dependencies) + var initFunc, dependencies; + if (arguments.length == 2) { + initFunc = arguments[1]; + dependencies = []; + } else { + initFunc = arguments[2]; + dependencies = arguments[1]; + } + + var module = createModule(name, dependencies, initFunc); + + // Initialize the module immediately if the core is already initialized + if (api.initialized && api.supported) { + module.init(); + } + }; + + api.createCoreModule = function(name, dependencies, initFunc) { + createModule(name, dependencies, initFunc); + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately + + function RangePrototype() {} + api.RangePrototype = RangePrototype; + api.rangePrototype = new RangePrototype(); + + function SelectionPrototype() {} + api.selectionPrototype = new SelectionPrototype(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // DOM utility methods used by Rangy + api.createCoreModule("DomUtil", [], function(api, module) { + var UNDEF = "undefined"; + var util = api.util; + var getBody = util.getBody; + + // Perform feature tests + if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { + module.fail("document missing a Node creation method"); + } + + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); + } + + var el = document.createElement("div"); + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { + module.fail("Incomplete Element implementation"); + } + + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } + + var textNode = document.createTextNode("test"); + if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || + !util.areHostProperties(textNode, ["data"]))) { + module.fail("Incomplete Text Node implementation"); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been + // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that + // contains just the document as a single element and the value searched for is the document. + var arrayContains = /*Array.prototype.indexOf ? + function(arr, val) { + return arr.indexOf(val) > -1; + }:*/ + + function(arr, val) { + var i = arr.length; + while (i--) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI + function isHtmlNamespace(node) { + var ns; + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); + } + + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } + + function getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + ++i; + } + return i; + } + + function getNodeLength(node) { + switch (node.nodeType) { + case 7: + case 10: + return 0; + case 3: + case 8: + return node.length; + default: + return node.childNodes.length; + } + } + + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); + } + + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; + } + } + + return null; + } + + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; + } + } + return false; + } + + function isOrIsAncestorOf(ancestor, descendant) { + return isAncestorOf(ancestor, descendant, true); + } + + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; + } + n = p; + } + return null; + } + + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + } + + function isTextOrCommentNode(node) { + if (!node) { + return false; + } + var t = node.nodeType; + return t == 3 || t == 8 ; // Text or Comment + } + + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } + + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index, positionsToPreserve) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + + // Preserve positions + if (positionsToPreserve) { + for (var i = 0, position; position = positionsToPreserve[i++]; ) { + // Handle case where position was inside the portion of node after the split point + if (position.node == node && position.offset > index) { + position.node = newNode; + position.offset -= index; + } + // Handle the case where the position is a node offset within node's parent + else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { + ++position.offset; + } + } + } + return newNode; + } + + function getDocument(node) { + if (node.nodeType == 9) { + return node; + } else if (typeof node.ownerDocument != UNDEF) { + return node.ownerDocument; + } else if (typeof node.document != UNDEF) { + return node.document; + } else if (node.parentNode) { + return getDocument(node.parentNode); + } else { + throw module.createError("getDocument: no document found for node"); + } + } + + function getWindow(node) { + var doc = getDocument(node); + if (typeof doc.defaultView != UNDEF) { + return doc.defaultView; + } else if (typeof doc.parentWindow != UNDEF) { + return doc.parentWindow; + } else { + throw module.createError("Cannot get a window object for node"); + } + } + + function getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw module.createError("getIframeDocument: No Document object found for iframe element"); + } + } + + function getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw module.createError("getIframeWindow: No Window object found for iframe element"); + } + } + + // This looks bad. Is it worth it? + function isWindow(obj) { + return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); + } + + function getContentDocument(obj, module, methodName) { + var doc; + + if (!obj) { + doc = document; + } + + // Test if a DOM node has been passed and obtain a document object for it if so + else if (util.isHostProperty(obj, "nodeType")) { + doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ? + getIframeDocument(obj) : getDocument(obj); + } + + // Test if the doc parameter appears to be a Window object + else if (isWindow(obj)) { + doc = obj.document; + } + + if (!doc) { + throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); + } + + return doc; + } + + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + + function comparePoints(nodeA, offsetA, nodeB, offsetB) { + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing + var nodeC, root, childA, childB, n; + if (nodeA == nodeB) { + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { + // Case 2: node C (container B or an ancestor) is a child node of A + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { + root = getCommonAncestor(nodeA, nodeB); + if (!root) { + throw new Error("comparePoints error: nodes have no common ancestor"); + } + + // Case 4: containers are siblings or descendants of siblings + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible + throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); + } else { + n = root.firstChild; + while (n) { + if (n === childA) { + return -1; + } else if (n === childB) { + return 1; + } + n = n.nextSibling; + } + } + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried + var crashyTextNodes = false; + + function isBrokenNode(node) { + var n; + try { + n = node.parentNode; + return false; + } catch (e) { + return true; + } + } + + (function() { + var el = document.createElement("b"); + el.innerHTML = "1"; + var textNode = el.firstChild; + el.innerHTML = "
"; + crashyTextNodes = isBrokenNode(textNode); + + api.features.crashyTextNodes = crashyTextNodes; + })(); + + /*----------------------------------------------------------------------------------------------------------------*/ + + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (crashyTextNodes && isBrokenNode(node)) { + return "[Broken node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } + if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; + } + return node.nodeName; + } + + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + + var getComputedStyleProperty; + if (typeof window.getComputedStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return getWindow(el).getComputedStyle(el, null)[propName]; + }; + } else if (typeof document.documentElement.currentStyle != UNDEF) { + getComputedStyleProperty = function(el, propName) { + return el.currentStyle ? el.currentStyle[propName] : ""; + }; + } else { + module.fail("No means of obtaining computed style properties found"); + } + + function createTestElement(doc, html, contentEditable) { + var body = getBody(doc); + var el = doc.createElement("div"); + el.contentEditable = "" + !!contentEditable; + if (html) { + el.innerHTML = html; + } + + // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292) + var bodyFirstChild = body.firstChild; + if (bodyFirstChild) { + body.insertBefore(el, bodyFirstChild); + } else { + body.appendChild(el); + } + + return el; + } + + function removeNode(node) { + return node.parentNode.removeChild(node); + } + + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + var n = this._current = this._next; + var child, next; + if (this._current) { + child = n.firstChild; + if (child) { + this._next = child; + } else { + next = null; + while ((n !== this.root) && !(next = n.nextSibling)) { + n = n.parentNode; + } + this._next = next; + } + } + return this._current; + }, + + detach: function() { + this._current = this._next = this.root = null; + } + }; + + function createIterator(root) { + return new NodeIterator(root); + } + + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } + + DomPosition.prototype = { + equals: function(pos) { + return !!pos && this.node === pos.node && this.offset == pos.offset; + }, + + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; + }, + + toString: function() { + return this.inspect(); + } + }; + + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } + + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11, + INVALID_NODE_TYPE_ERR: 24 + }; + + DOMException.prototype.toString = function() { + return this.message; + }; + + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + isOrIsAncestorOf: isOrIsAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + isTextOrCommentNode: isTextOrCommentNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: getBody, + isWindow: isWindow, + getContentDocument: getContentDocument, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + isBrokenNode: isBrokenNode, + inspectNode: inspectNode, + getComputedStyleProperty: getComputedStyleProperty, + createTestElement: createTestElement, + removeNode: removeNode, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; + + api.DOMException = DOMException; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Pure JavaScript implementation of DOM Range + api.createCoreModule("DomRange", ["DomUtil"], function(api, module) { + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + var isCharacterDataNode = dom.isCharacterDataNode; + var getNodeIndex = dom.getNodeIndex; + var isOrIsAncestorOf = dom.isOrIsAncestorOf; + var getDocument = dom.getDocument; + var comparePoints = dom.comparePoints; + var splitDataNode = dom.splitDataNode; + var getClosestAncestorIn = dom.getClosestAncestorIn; + var getNodeLength = dom.getNodeLength; + var arrayContains = dom.arrayContains; + var getRootContainer = dom.getRootContainer; + var crashyTextNodes = api.features.crashyTextNodes; + + var removeNode = dom.removeNode; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); + } + + function getRangeDocument(range) { + return range.document || getDocument(range.startContainer); + } + + function getRangeRoot(range) { + return getRootContainer(range.startContainer); + } + + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node)); + } + + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node) + 1); + } + + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); + } + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); + } + return firstNodeInserted; + } + + function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { + assertRangeValid(rangeA); + assertRangeValid(rangeB); + + if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + + var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), + endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + } + + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(); + } + + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + if (rangeIterator.isPartiallySelectedSubtree()) { + if (func(node) === false) { + iteratorState.stop = true; + return; + } else { + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of + // the node selected by the Range. + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(); + if (iteratorState.stop) { + return; + } + } + } else { + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendants + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } + } + } + } + + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(); + } else { + iterator.remove(); + } + } + } + + function extractSubtree(iterator) { + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(); + } else { + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function getNodesInRange(range, nodeTypes, filter) { + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } + + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if (filterNodeTypes && !regex.test(node.nodeType)) { + return; + } + if (filterExists && !filter(node)) { + return; + } + // Don't include a boundary container if it is a character data node and the range does not contain any + // of its character data. See issue 190. + var sc = range.startContainer; + if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { + return; + } + + var ec = range.endContainer; + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { + return; + } + + nodes.push(node); + }); + return nodes; + } + + function inspect(range) { + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); + } + } + } + + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, + + reset: function() { + this._current = null; + this._next = this._first; + }, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; + + // Check for partially selected text nodes + if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + removeNode(current); + } else { + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(false); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); + + if (isOrIsAncestorOf(current, this.sc)) { + startContainer = this.sc; + startOffset = this.so; + } + if (isOrIsAncestorOf(current, this.ec)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function() { + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; + + function createAncestorFinder(nodeTypes) { + return function(node, selfIsAncestor) { + var t, n = selfIsAncestor ? node : node.parentNode; + while (n) { + t = n.nodeType; + if (arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; + } + return null; + }; + } + + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!arrayContains(invalidTypes, node.nodeType)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } + + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } + + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } + + function isValidOffset(node, offset) { + return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + } + + function isRangeValid(range) { + return (!!range.startContainer && !!range.endContainer && + !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) && + getRootContainer(range.startContainer) == getRootContainer(range.endContainer) && + isValidOffset(range.startContainer, range.startOffset) && + isValidOffset(range.endContainer, range.endOffset)); + } + + function assertRangeValid(range) { + if (!isRangeValid(range)) { + throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "x"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" && + dom.isHtmlNamespace(getDocument(el).documentElement) && + dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + function splitRangeBoundaries(range, positionsToPreserve) { + assertRangeValid(range); + + var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; + var startEndSame = (sc === ec); + + if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + splitDataNode(ec, eo, positionsToPreserve); + } + + if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { + sc = splitDataNode(sc, so, positionsToPreserve); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { + eo++; + } + so = 0; + } + range.setStartAndEnd(sc, so, ec, eo); + } + + function rangeToHtml(range) { + assertRangeValid(range); + var container = range.commonAncestorContainer.parentNode.cloneNode(false); + container.appendChild( range.cloneContents() ); + return container.innerHTML; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; + + util.extend(api.rangePrototype, { + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return comparePoints(nodeA, offsetA, nodeB, offsetB); + }, + + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (isOrIsAncestorOf(node, this.startContainer)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); + } + return clone; + } + }, + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); + }, + + cloneRange: function() { + assertRangeValid(this); + var range = new Range(getRangeDocument(this)); + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = this[prop]; + } + return range; + }, + + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textParts = [], iterator = new RangeIterator(this, true); + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments + if (node.nodeType == 3 || node.nodeType == 4) { + textParts.push(node.data); + } + }); + iterator.detach(); + return textParts.join(""); + } + }, + + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. + + compareNode: function(node) { + assertRangeValid(this); + + var parent = node.parentNode; + var nodeIndex = getNodeIndex(node); + + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } + + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); + + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, + + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, + + createContextualFragment: createContextualFragment, + + toHtml: function() { + return rangeToHtml(this); + }, + + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + if (getRootContainer(node) != getRangeRoot(this)) { + return false; + } + + var parent = node.parentNode, offset = getNodeIndex(node); + if (!parent) { + return true; + } + + var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, + + // The methods below are non-standard and invented by me. + + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range) { + return rangesIntersect(this, range, false); + }, + + // Sharing a boundary start-to-end or end-to-start does count as intersection. + intersectsOrTouchesRange: function(range) { + return rangesIntersect(this, range, true); + }, + + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); + + var intersectionRange = this.cloneRange(); + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); + } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, + + union: function(range) { + if (this.intersectsOrTouchesRange(range)) { + var unionRange = this.cloneRange(); + if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new DOMException("Ranges do not intersect"); + } + }, + + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, + + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; + }, + + containsRange: function(range) { + var intersection = this.intersection(range); + return intersection !== null && range.equals(intersection); + }, + + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + return this.containsRange(nodeRange); + } else { + return this.containsNodeContents(node); + } + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + this.setStartAfter(node); + this.collapse(true); + }, + + getBookmark: function(containerNode) { + var doc = getRangeDocument(this); + var preSelectionRange = api.createRange(doc); + containerNode = containerNode || dom.getBody(doc); + preSelectionRange.selectNodeContents(containerNode); + var range = this.intersection(preSelectionRange); + var start = 0, end = 0; + if (range) { + preSelectionRange.setEnd(range.startContainer, range.startOffset); + start = preSelectionRange.toString().length; + end = start + range.toString().length; + } + + return { + start: start, + end: end, + containerNode: containerNode + }; + }, + + moveToBookmark: function(bookmark) { + var containerNode = bookmark.containerNode; + var charIndex = 0; + this.setStart(containerNode, 0); + this.collapse(true); + var nodeStack = [containerNode], node, foundStart = false, stop = false; + var nextCharIndex, i, childNodes; + + while (!stop && (node = nodeStack.pop())) { + if (node.nodeType == 3) { + nextCharIndex = charIndex + node.length; + if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { + this.setStart(node, bookmark.start - charIndex); + foundStart = true; + } + if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { + this.setEnd(node, bookmark.end - charIndex); + stop = true; + } + charIndex = nextCharIndex; + } else { + childNodes = node.childNodes; + i = childNodes.length; + while (i--) { + nodeStack.push(childNodes[i]); + } + } + } + }, + + getName: function() { + return "DomRange"; + }, + + equals: function(range) { + return Range.rangesEqual(this, range); + }, + + isValid: function() { + return isRangeValid(this); + }, + + inspect: function() { + return inspect(this); + }, + + detach: function() { + // In DOM4, detach() is now a no-op. + } + }); + + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; + + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } + + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } + + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + + var iterator = new RangeIterator(this, true); + + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } + + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); + + iterator.reset(); + + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); + + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); + + return returnValue; + }; + } + + function createPrototypeRange(constructor, boundaryUpdater) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } + + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } + } + + // Set up inheritance + var F = function() {}; + F.prototype = api.rangePrototype; + constructor.prototype = new F(); + + util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeEnd(this, node, offset); + }, + + /** + * Convenience method to set a range's start and end boundaries. Overloaded as follows: + * - Two parameters (node, offset) creates a collapsed range at that position + * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at + * startOffset and ending at endOffset + * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in + * startNode and ending at endOffset in endNode + */ + setStartAndEnd: function() { + var args = arguments; + var sc = args[0], so = args[1], ec = sc, eo = so; + + switch (args.length) { + case 3: + eo = args[2]; + break; + case 4: + ec = args[2]; + eo = args[3]; + break; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + setBoundary: function(node, offset, isStart) { + this["set" + (isStart ? "Start" : "End")](node, offset); + }, + + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { + assertRangeValid(this); + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + } + }, + + selectNodeContents: function(node) { + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, getNodeLength(node)); + }, + + selectNode: function(node) { + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); + + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, + + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + splitBoundaries: function() { + splitRangeBoundaries(this); + }, + + splitBoundariesPreservingPositions: function(positionsToPreserve) { + splitRangeBoundaries(this, positionsToPreserve); + }, + + normalizeBoundaries: function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + + var mergeForward = function(node) { + var sibling = node.nextSibling; + if (sibling && sibling.nodeType == node.nodeType) { + ec = node; + eo = node.length; + node.appendData(sibling.data); + removeNode(sibling); + } + }; + + var mergeBackward = function(node) { + var sibling = node.previousSibling; + if (sibling && sibling.nodeType == node.nodeType) { + sc = node; + var nodeLength = node.length; + so = sibling.length; + node.insertData(0, sibling.data); + removeNode(sibling); + if (sc == ec) { + eo += so; + ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; + + var normalizeStart = true; + var sibling; + + if (isCharacterDataNode(ec)) { + if (eo == ec.length) { + mergeForward(ec); + } else if (eo == 0) { + sibling = ec.previousSibling; + if (sibling && sibling.nodeType == ec.nodeType) { + eo = sibling.length; + if (sc == ec) { + normalizeStart = false; + } + sibling.appendData(ec.data); + removeNode(ec); + ec = sibling; + } + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && isCharacterDataNode(endNode)) { + mergeForward(endNode); + } + } + normalizeStart = !this.collapsed; + } + + if (normalizeStart) { + if (isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); + } else if (so == sc.length) { + sibling = sc.nextSibling; + if (sibling && sibling.nodeType == sc.nodeType) { + if (ec == sibling) { + ec = sc; + eo += sc.length; + } + sc.appendData(sibling.data); + removeNode(sibling); + } + } + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && isCharacterDataNode(startNode)) { + mergeBackward(startNode); + } + } + } + } else { + sc = ec; + so = eo; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + collapseToPoint: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + this.setStartAndEnd(node, offset); + } + }); + + copyComparisonConstants(constructor); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Updates commonAncestorContainer and collapsed after boundary change + function updateCollapsedAndCommonAncestor(range) { + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + range.commonAncestorContainer = range.collapsed ? + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); + } + + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; + range.document = dom.getDocument(startContainer); + + updateCollapsedAndCommonAncestor(range); + } + + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this.document = doc; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries); + + util.extend(Range, { + rangeProperties: rangeProperties, + RangeIterator: RangeIterator, + copyComparisonConstants: copyComparisonConstants, + createPrototypeRange: createPrototypeRange, + inspect: inspect, + toHtml: rangeToHtml, + getRangeDocument: getRangeDocument, + rangesEqual: function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + } + }); + + api.DomRange = Range; + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wrappers for the browser's native DOM Range and/or TextRange implementation + api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { + var WrappedRange, WrappedTextRange; + var dom = api.dom; + var util = api.util; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; + var getBody = dom.getBody; + var getContentDocument = dom.getContentDocument; + var isCharacterDataNode = dom.isCharacterDataNode; + + + /*----------------------------------------------------------------------------------------------------------------*/ + + if (api.features.implementsDomRange) { + // This is a wrapper around the browser's native DOM Range. It has two aims: + // - Provide workarounds for specific browser bugs + // - provide convenient extensions, which are inherited from Rangy's DomRange + + (function() { + var rangeProto; + var rangeProperties = DomRange.rangeProperties; + + function updateRangeProperties(range) { + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = range.nativeRange[prop]; + } + // Fix for broken collapsed property in IE 9. + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + } + + function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); + var nativeRangeDifferent = !range.equals(range.nativeRange); + + // Always set both boundaries for the benefit of IE9 (see issue 35) + if (startMoved || endMoved || nativeRangeDifferent) { + range.setEnd(endContainer, endOffset); + range.setStart(startContainer, startOffset); + } + } + + var createBeforeAfterNodeSetter; + + WrappedRange = function(range) { + if (!range) { + throw module.createError("WrappedRange: Range must be specified"); + } + this.nativeRange = range; + updateRangeProperties(this); + }; + + DomRange.createPrototypeRange(WrappedRange, updateNativeRange); + + rangeProto = WrappedRange.prototype; + + rangeProto.selectNode = function(node) { + this.nativeRange.selectNode(node); + updateRangeProperties(this); + }; + + rangeProto.cloneContents = function() { + return this.nativeRange.cloneContents(); + }; + + // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, + // insertNode() is never delegated to the native range. + + rangeProto.surroundContents = function(node) { + this.nativeRange.surroundContents(node); + updateRangeProperties(this); + }; + + rangeProto.collapse = function(isStart) { + this.nativeRange.collapse(isStart); + updateRangeProperties(this); + }; + + rangeProto.cloneRange = function() { + return new WrappedRange(this.nativeRange.cloneRange()); + }; + + rangeProto.refresh = function() { + updateRangeProperties(this); + }; + + rangeProto.toString = function() { + return this.nativeRange.toString(); + }; + + // Create test range and node for feature detection + + var testTextNode = document.createTextNode("test"); + getBody(document).appendChild(testTextNode); + var range = document.createRange(); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and + // correct for it + + range.setStart(testTextNode, 0); + range.setEnd(testTextNode, 0); + + try { + range.setStart(testTextNode, 1); + + rangeProto.setStart = function(node, offset) { + this.nativeRange.setStart(node, offset); + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + this.nativeRange.setEnd(node, offset); + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name) { + return function(node) { + this.nativeRange[name](node); + updateRangeProperties(this); + }; + }; + + } catch(ex) { + + rangeProto.setStart = function(node, offset) { + try { + this.nativeRange.setStart(node, offset); + } catch (ex) { + this.nativeRange.setEnd(node, offset); + this.nativeRange.setStart(node, offset); + } + updateRangeProperties(this); + }; + + rangeProto.setEnd = function(node, offset) { + try { + this.nativeRange.setEnd(node, offset); + } catch (ex) { + this.nativeRange.setStart(node, offset); + this.nativeRange.setEnd(node, offset); + } + updateRangeProperties(this); + }; + + createBeforeAfterNodeSetter = function(name, oppositeName) { + return function(node) { + try { + this.nativeRange[name](node); + } catch (ex) { + this.nativeRange[oppositeName](node); + this.nativeRange[name](node); + } + updateRangeProperties(this); + }; + }; + } + + rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); + rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); + rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); + rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); + + /*--------------------------------------------------------------------------------------------------------*/ + + // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing + // whether the native implementation can be trusted + rangeProto.selectNodeContents = function(node) { + this.setStartAndEnd(node, 0, dom.getNodeLength(node)); + }; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for + // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 + + range.selectNodeContents(testTextNode); + range.setEnd(testTextNode, 3); + + var range2 = document.createRange(); + range2.selectNodeContents(testTextNode); + range2.setEnd(testTextNode, 4); + range2.setStart(testTextNode, 2); + + if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && + range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { + // This is the wrong way round, so correct for it + + rangeProto.compareBoundaryPoints = function(type, range) { + range = range.nativeRange || range; + if (type == range.START_TO_END) { + type = range.END_TO_START; + } else if (type == range.END_TO_START) { + type = range.START_TO_END; + } + return this.nativeRange.compareBoundaryPoints(type, range); + }; + } else { + rangeProto.compareBoundaryPoints = function(type, range) { + return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. + + var el = document.createElement("div"); + el.innerHTML = "123"; + var textNode = el.firstChild; + var body = getBody(document); + body.appendChild(el); + + range.setStart(textNode, 1); + range.setEnd(textNode, 2); + range.deleteContents(); + + if (textNode.data == "13") { + // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and + // extractContents() + rangeProto.deleteContents = function() { + this.nativeRange.deleteContents(); + updateRangeProperties(this); + }; + + rangeProto.extractContents = function() { + var frag = this.nativeRange.extractContents(); + updateRangeProperties(this); + return frag; + }; + } else { + } + + body.removeChild(el); + body = null; + + /*--------------------------------------------------------------------------------------------------------*/ + + // Test for existence of createContextualFragment and delegate to it if it exists + if (util.isHostMethod(range, "createContextualFragment")) { + rangeProto.createContextualFragment = function(fragmentStr) { + return this.nativeRange.createContextualFragment(fragmentStr); + }; + } + + /*--------------------------------------------------------------------------------------------------------*/ + + // Clean up + getBody(document).removeChild(testTextNode); + + rangeProto.getName = function() { + return "WrappedRange"; + }; + + api.WrappedRange = WrappedRange; + + api.createNativeRange = function(doc) { + doc = getContentDocument(doc, module, "createNativeRange"); + return doc.createRange(); + }; + })(); + } + + if (api.features.implementsTextRange) { + /* + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() + method. For example, in the following (where pipes denote the selection boundaries): + + + + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" + + This method returns the common ancestor node of the following: + - the parentElement() of the textRange + - the parentElement() of the textRange after calling collapse(true) + - the parentElement() of the textRange after calling collapse(false) + */ + var getTextRangeContainerElement = function(textRange) { + var parentEl = textRange.parentElement(); + var range = textRange.duplicate(); + range.collapse(true); + var startEl = range.parentElement(); + range = textRange.duplicate(); + range.collapse(false); + var endEl = range.parentElement(); + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); + + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + }; + + var textRangeIsCollapsed = function(textRange) { + return textRange.compareEndPoints("StartToEnd", textRange) == 0; + }; + + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started + // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) + // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange + // bugs, handling for inputs and images, plus optimizations. + var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { + var workingRange = textRange.duplicate(); + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); + + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { + containerElement = wholeRangeContainerElement; + } + + + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx + if (!containerElement.canHaveHTML) { + var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + return { + boundaryPosition: pos, + nodeInfo: { + nodeIndex: pos.offset, + containerElement: pos.node + } + }; + } + + var workingNode = dom.getDocument(containerElement).createElement("span"); + + // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 + // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 + if (workingNode.parentNode) { + dom.removeNode(workingNode); + } + + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; + var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; + var childNodeCount = containerElement.childNodes.length; + var end = childNodeCount; + + // Check end first. Code within the loop assumes that the endth child node of the container is definitely + // after the range boundary. + var nodeIndex = end; + + while (true) { + if (nodeIndex == childNodeCount) { + containerElement.appendChild(workingNode); + } else { + containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); + } + workingRange.moveToElementText(workingNode); + comparison = workingRange.compareEndPoints(workingComparisonType, textRange); + if (comparison == 0 || start == end) { + break; + } else if (comparison == -1) { + if (end == start + 1) { + // We know the endth child node is after the range boundary, so we must be done. + break; + } else { + start = nodeIndex; + } + } else { + end = (end == start + 1) ? start : nodeIndex; + } + nodeIndex = Math.floor((start + end) / 2); + containerElement.removeChild(workingNode); + } + + + // We've now reached or gone past the boundary of the text range we're interested in + // so have identified the node we want + boundaryNode = workingNode.nextSibling; + + if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of + // the node containing the text range's boundary, so we move the end of the working range to the + // boundary point and measure the length of its text to get the boundary's offset within the node. + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); + + var offset; + + if (/[\r\n]/.test(boundaryNode.data)) { + /* + For the particular case of a boundary within a text node containing rendered line breaks (within a +
 element, for example), we need a slightly complicated approach to get the boundary's offset in
+                        IE. The facts:
+
+                        - Each line break is represented as \r in the text node's data/nodeValue properties
+                        - Each line break is represented as \r\n in the TextRange's 'text' property
+                        - The 'text' property of the TextRange does not contain trailing line breaks
+
+                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
+                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
+                        to use this to store the characters moved when moving both the start and end of the range to the
+                        start of the document body and subtracting the start offset from the end offset (the
+                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
+                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
+                        the end of the document) has the same problem.
+
+                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
+                        end boundary one character at a time and incrementing a counter with the value returned by the
+                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
+                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
+                        by the location of the range within the document).
+
+                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
+                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
+                        be longer than the text of the TextRange, so the start of the range is moved that length initially
+                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
+                        property. This has good performance in most situations compared to the previous two methods.
+                        */
+                        var tempRange = workingRange.duplicate();
+                        var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                        offset = tempRange.moveStart("character", rangeLength);
+                        while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                            offset++;
+                            tempRange.moveStart("character", 1);
+                        }
+                    } else {
+                        offset = workingRange.text.length;
+                    }
+                    boundaryPosition = new DomPosition(boundaryNode, offset);
+                } else {
+
+                    // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+                    // a position within that, and likewise for a start boundary preceding a character data node
+                    previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+                    nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+                    if (nextNode && isCharacterDataNode(nextNode)) {
+                        boundaryPosition = new DomPosition(nextNode, 0);
+                    } else if (previousNode && isCharacterDataNode(previousNode)) {
+                        boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
+                    } else {
+                        boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+                    }
+                }
+
+                // Clean up
+                dom.removeNode(workingNode);
+
+                return {
+                    boundaryPosition: boundaryPosition,
+                    nodeInfo: {
+                        nodeIndex: nodeIndex,
+                        containerElement: containerElement
+                    }
+                };
+            };
+
+            // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
+            // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+            // (http://code.google.com/p/ierange/)
+            var createBoundaryTextRange = function(boundaryPosition, isStart) {
+                var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+                var doc = dom.getDocument(boundaryPosition.node);
+                var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
+                var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
+
+                if (nodeIsDataNode) {
+                    boundaryNode = boundaryPosition.node;
+                    boundaryParent = boundaryNode.parentNode;
+                } else {
+                    childNodes = boundaryPosition.node.childNodes;
+                    boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+                    boundaryParent = boundaryPosition.node;
+                }
+
+                // Position the range immediately before the node containing the boundary
+                workingNode = doc.createElement("span");
+
+                // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
+                // the element rather than immediately before or after it
+                workingNode.innerHTML = "&#feff;";
+
+                // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+                // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+                if (boundaryNode) {
+                    boundaryParent.insertBefore(workingNode, boundaryNode);
+                } else {
+                    boundaryParent.appendChild(workingNode);
+                }
+
+                workingRange.moveToElementText(workingNode);
+                workingRange.collapse(!isStart);
+
+                // Clean up
+                boundaryParent.removeChild(workingNode);
+
+                // Move the working range to the text offset, if required
+                if (nodeIsDataNode) {
+                    workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+                }
+
+                return workingRange;
+            };
+
+            /*------------------------------------------------------------------------------------------------------------*/
+
+            // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+            // prototype
+
+            WrappedTextRange = function(textRange) {
+                this.textRange = textRange;
+                this.refresh();
+            };
+
+            WrappedTextRange.prototype = new DomRange(document);
+
+            WrappedTextRange.prototype.refresh = function() {
+                var start, end, startBoundary;
+
+                // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+                var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+                if (textRangeIsCollapsed(this.textRange)) {
+                    end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
+                        true).boundaryPosition;
+                } else {
+                    startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                    start = startBoundary.boundaryPosition;
+
+                    // An optimization used here is that if the start and end boundaries have the same parent element, the
+                    // search scope for the end boundary can be limited to exclude the portion of the element that precedes
+                    // the start boundary
+                    end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
+                        startBoundary.nodeInfo).boundaryPosition;
+                }
+
+                this.setStart(start.node, start.offset);
+                this.setEnd(end.node, end.offset);
+            };
+
+            WrappedTextRange.prototype.getName = function() {
+                return "WrappedTextRange";
+            };
+
+            DomRange.copyComparisonConstants(WrappedTextRange);
+
+            var rangeToTextRange = function(range) {
+                if (range.collapsed) {
+                    return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                } else {
+                    var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                    var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                    var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
+                    textRange.setEndPoint("StartToStart", startRange);
+                    textRange.setEndPoint("EndToEnd", endRange);
+                    return textRange;
+                }
+            };
+
+            WrappedTextRange.rangeToTextRange = rangeToTextRange;
+
+            WrappedTextRange.prototype.toTextRange = function() {
+                return rangeToTextRange(this);
+            };
+
+            api.WrappedTextRange = WrappedTextRange;
+
+            // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
+            // implementation to use by default.
+            if (!api.features.implementsDomRange || api.config.preferTextRange) {
+                // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+                var globalObj = (function(f) { return f("return this;")(); })(Function);
+                if (typeof globalObj.Range == "undefined") {
+                    globalObj.Range = WrappedTextRange;
+                }
+
+                api.createNativeRange = function(doc) {
+                    doc = getContentDocument(doc, module, "createNativeRange");
+                    return getBody(doc).createTextRange();
+                };
+
+                api.WrappedRange = WrappedTextRange;
+            }
+        }
+
+        api.createRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRange");
+            return new api.WrappedRange(api.createNativeRange(doc));
+        };
+
+        api.createRangyRange = function(doc) {
+            doc = getContentDocument(doc, module, "createRangyRange");
+            return new DomRange(doc);
+        };
+
+        util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
+        util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
+
+        api.addShimListener(function(win) {
+            var doc = win.document;
+            if (typeof doc.createRange == "undefined") {
+                doc.createRange = function() {
+                    return api.createRange(doc);
+                };
+            }
+            doc = win = null;
+        });
+    });
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
+    // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
+    api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
+        api.config.checkSelectionRanges = true;
+
+        var BOOLEAN = "boolean";
+        var NUMBER = "number";
+        var dom = api.dom;
+        var util = api.util;
+        var isHostMethod = util.isHostMethod;
+        var DomRange = api.DomRange;
+        var WrappedRange = api.WrappedRange;
+        var DOMException = api.DOMException;
+        var DomPosition = dom.DomPosition;
+        var getNativeSelection;
+        var selectionIsCollapsed;
+        var features = api.features;
+        var CONTROL = "Control";
+        var getDocument = dom.getDocument;
+        var getBody = dom.getBody;
+        var rangesEqual = DomRange.rangesEqual;
+
+
+        // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
+        // "forward" or "forwards") or a Boolean (true for backwards).
+        function isDirectionBackward(dir) {
+            return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
+        }
+
+        function getWindow(win, methodName) {
+            if (!win) {
+                return window;
+            } else if (dom.isWindow(win)) {
+                return win;
+            } else if (win instanceof WrappedSelection) {
+                return win.win;
+            } else {
+                var doc = dom.getContentDocument(win, module, methodName);
+                return dom.getWindow(doc);
+            }
+        }
+
+        function getWinSelection(winParam) {
+            return getWindow(winParam, "getWinSelection").getSelection();
+        }
+
+        function getDocSelection(winParam) {
+            return getWindow(winParam, "getDocSelection").document.selection;
+        }
+
+        function winSelectionIsBackward(sel) {
+            var backward = false;
+            if (sel.anchorNode) {
+                backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+            }
+            return backward;
+        }
+
+        // Test for the Range/TextRange and Selection features required
+        // Test for ability to retrieve selection
+        var implementsWinGetSelection = isHostMethod(window, "getSelection"),
+            implementsDocSelection = util.isHostObject(document, "selection");
+
+        features.implementsWinGetSelection = implementsWinGetSelection;
+        features.implementsDocSelection = implementsDocSelection;
+
+        var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+        if (useDocumentSelection) {
+            getNativeSelection = getDocSelection;
+            api.isSelectionValid = function(winParam) {
+                var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
+
+                // Check whether the selection TextRange is actually contained within the correct document
+                return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
+            };
+        } else if (implementsWinGetSelection) {
+            getNativeSelection = getWinSelection;
+            api.isSelectionValid = function() {
+                return true;
+            };
+        } else {
+            module.fail("Neither document.selection or window.getSelection() detected.");
+            return false;
+        }
+
+        api.getNativeSelection = getNativeSelection;
+
+        var testSelection = getNativeSelection();
+
+        // In Firefox, the selection is null in an iframe with display: none. See issue #138.
+        if (!testSelection) {
+            module.fail("Native selection was null (possibly issue 138?)");
+            return false;
+        }
+
+        var testRange = api.createNativeRange(document);
+        var body = getBody(document);
+
+        // Obtaining a range from a selection
+        var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
+            ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
+
+        features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+        // Test for existence of native selection extend() method
+        var selectionHasExtend = isHostMethod(testSelection, "extend");
+        features.selectionHasExtend = selectionHasExtend;
+
+        // Test if rangeCount exists
+        var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
+        features.selectionHasRangeCount = selectionHasRangeCount;
+
+        var selectionSupportsMultipleRanges = false;
+        var collapsedNonEditableSelectionsSupported = true;
+
+        var addRangeBackwardToNative = selectionHasExtend ?
+            function(nativeSelection, range) {
+                var doc = DomRange.getRangeDocument(range);
+                var endRange = api.createRange(doc);
+                endRange.collapseToPoint(range.endContainer, range.endOffset);
+                nativeSelection.addRange(getNativeRange(endRange));
+                nativeSelection.extend(range.startContainer, range.startOffset);
+            } : null;
+
+        if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+                typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
+
+            (function() {
+                // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
+                // performed on the current document's selection. See issue 109.
+
+                // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
+                // will result in the selection direction begin reversed if the original selection was backwards and the
+                // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
+                var sel = window.getSelection();
+                if (sel) {
+                    // Store the current selection
+                    var originalSelectionRangeCount = sel.rangeCount;
+                    var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
+                    var originalSelectionRanges = [];
+                    var originalSelectionBackward = winSelectionIsBackward(sel);
+                    for (var i = 0; i < originalSelectionRangeCount; ++i) {
+                        originalSelectionRanges[i] = sel.getRangeAt(i);
+                    }
+
+                    // Create some test elements
+                    var testEl = dom.createTestElement(document, "", false);
+                    var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
+
+                    // Test whether the native selection will allow a collapsed selection within a non-editable element
+                    var r1 = document.createRange();
+
+                    r1.setStart(textNode, 1);
+                    r1.collapse(true);
+                    sel.removeAllRanges();
+                    sel.addRange(r1);
+                    collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+                    sel.removeAllRanges();
+
+                    // Test whether the native selection is capable of supporting multiple ranges.
+                    if (!selectionHasMultipleRanges) {
+                        // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
+                        // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
+                        // nothing we can do about this while retaining the feature test so we have to resort to a browser
+                        // sniff. I'm not happy about it. See
+                        // https://code.google.com/p/chromium/issues/detail?id=399791
+                        var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
+                        if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
+                            selectionSupportsMultipleRanges = false;
+                        } else {
+                            var r2 = r1.cloneRange();
+                            r1.setStart(textNode, 0);
+                            r2.setEnd(textNode, 3);
+                            r2.setStart(textNode, 2);
+                            sel.addRange(r1);
+                            sel.addRange(r2);
+                            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+                        }
+                    }
+
+                    // Clean up
+                    dom.removeNode(testEl);
+                    sel.removeAllRanges();
+
+                    for (i = 0; i < originalSelectionRangeCount; ++i) {
+                        if (i == 0 && originalSelectionBackward) {
+                            if (addRangeBackwardToNative) {
+                                addRangeBackwardToNative(sel, originalSelectionRanges[i]);
+                            } else {
+                                api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
+                                sel.addRange(originalSelectionRanges[i]);
+                            }
+                        } else {
+                            sel.addRange(originalSelectionRanges[i]);
+                        }
+                    }
+                }
+            })();
+        }
+
+        features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+        features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+        // ControlRanges
+        var implementsControlRange = false, testControlRange;
+
+        if (body && isHostMethod(body, "createControlRange")) {
+            testControlRange = body.createControlRange();
+            if (util.areHostProperties(testControlRange, ["item", "add"])) {
+                implementsControlRange = true;
+            }
+        }
+        features.implementsControlRange = implementsControlRange;
+
+        // Selection collapsedness
+        if (selectionHasAnchorAndFocus) {
+            selectionIsCollapsed = function(sel) {
+                return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+            };
+        } else {
+            selectionIsCollapsed = function(sel) {
+                return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+            };
+        }
+
+        function updateAnchorAndFocusFromRange(sel, range, backward) {
+            var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
+            sel.anchorNode = range[anchorPrefix + "Container"];
+            sel.anchorOffset = range[anchorPrefix + "Offset"];
+            sel.focusNode = range[focusPrefix + "Container"];
+            sel.focusOffset = range[focusPrefix + "Offset"];
+        }
+
+        function updateAnchorAndFocusFromNativeSelection(sel) {
+            var nativeSel = sel.nativeSelection;
+            sel.anchorNode = nativeSel.anchorNode;
+            sel.anchorOffset = nativeSel.anchorOffset;
+            sel.focusNode = nativeSel.focusNode;
+            sel.focusOffset = nativeSel.focusOffset;
+        }
+
+        function updateEmptySelection(sel) {
+            sel.anchorNode = sel.focusNode = null;
+            sel.anchorOffset = sel.focusOffset = 0;
+            sel.rangeCount = 0;
+            sel.isCollapsed = true;
+            sel._ranges.length = 0;
+        }
+
+        function getNativeRange(range) {
+            var nativeRange;
+            if (range instanceof DomRange) {
+                nativeRange = api.createNativeRange(range.getDocument());
+                nativeRange.setEnd(range.endContainer, range.endOffset);
+                nativeRange.setStart(range.startContainer, range.startOffset);
+            } else if (range instanceof WrappedRange) {
+                nativeRange = range.nativeRange;
+            } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
+                nativeRange = range;
+            }
+            return nativeRange;
+        }
+
+        function rangeContainsSingleElement(rangeNodes) {
+            if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+                return false;
+            }
+            for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+                if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        function getSingleElementFromRange(range) {
+            var nodes = range.getNodes();
+            if (!rangeContainsSingleElement(nodes)) {
+                throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+            }
+            return nodes[0];
+        }
+
+        // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
+        function isTextRange(range) {
+            return !!range && typeof range.text != "undefined";
+        }
+
+        function updateFromTextRange(sel, range) {
+            // Create a Range from the selected TextRange
+            var wrappedRange = new WrappedRange(range);
+            sel._ranges = [wrappedRange];
+
+            updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+            sel.rangeCount = 1;
+            sel.isCollapsed = wrappedRange.collapsed;
+        }
+
+        function updateControlSelection(sel) {
+            // Update the wrapped selection based on what's now in the native selection
+            sel._ranges.length = 0;
+            if (sel.docSelection.type == "None") {
+                updateEmptySelection(sel);
+            } else {
+                var controlRange = sel.docSelection.createRange();
+                if (isTextRange(controlRange)) {
+                    // This case (where the selection type is "Control" and calling createRange() on the selection returns
+                    // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+                    // ControlRange have been removed from the ControlRange and removed from the document.
+                    updateFromTextRange(sel, controlRange);
+                } else {
+                    sel.rangeCount = controlRange.length;
+                    var range, doc = getDocument(controlRange.item(0));
+                    for (var i = 0; i < sel.rangeCount; ++i) {
+                        range = api.createRange(doc);
+                        range.selectNode(controlRange.item(i));
+                        sel._ranges.push(range);
+                    }
+                    sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+                }
+            }
+        }
+
+        function addRangeToControlSelection(sel, range) {
+            var controlRange = sel.docSelection.createRange();
+            var rangeElement = getSingleElementFromRange(range);
+
+            // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+            // contained by the supplied range
+            var doc = getDocument(controlRange.item(0));
+            var newControlRange = getBody(doc).createControlRange();
+            for (var i = 0, len = controlRange.length; i < len; ++i) {
+                newControlRange.add(controlRange.item(i));
+            }
+            try {
+                newControlRange.add(rangeElement);
+            } catch (ex) {
+                throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+            }
+            newControlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        var getSelectionRangeAt;
+
+        if (isHostMethod(testSelection, "getRangeAt")) {
+            // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
+            // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
+            // lesson to us all, especially me.
+            getSelectionRangeAt = function(sel, index) {
+                try {
+                    return sel.getRangeAt(index);
+                } catch (ex) {
+                    return null;
+                }
+            };
+        } else if (selectionHasAnchorAndFocus) {
+            getSelectionRangeAt = function(sel) {
+                var doc = getDocument(sel.anchorNode);
+                var range = api.createRange(doc);
+                range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
+
+                // Handle the case when the selection was selected backwards (from the end to the start in the
+                // document)
+                if (range.collapsed !== this.isCollapsed) {
+                    range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
+                }
+
+                return range;
+            };
+        }
+
+        function WrappedSelection(selection, docSelection, win) {
+            this.nativeSelection = selection;
+            this.docSelection = docSelection;
+            this._ranges = [];
+            this.win = win;
+            this.refresh();
+        }
+
+        WrappedSelection.prototype = api.selectionPrototype;
+
+        function deleteProperties(sel) {
+            sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
+            sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
+            sel.detached = true;
+        }
+
+        var cachedRangySelections = [];
+
+        function actOnCachedSelection(win, action) {
+            var i = cachedRangySelections.length, cached, sel;
+            while (i--) {
+                cached = cachedRangySelections[i];
+                sel = cached.selection;
+                if (action == "deleteAll") {
+                    deleteProperties(sel);
+                } else if (cached.win == win) {
+                    if (action == "delete") {
+                        cachedRangySelections.splice(i, 1);
+                        return true;
+                    } else {
+                        return sel;
+                    }
+                }
+            }
+            if (action == "deleteAll") {
+                cachedRangySelections.length = 0;
+            }
+            return null;
+        }
+
+        var getSelection = function(win) {
+            // Check if the parameter is a Rangy Selection object
+            if (win && win instanceof WrappedSelection) {
+                win.refresh();
+                return win;
+            }
+
+            win = getWindow(win, "getNativeSelection");
+
+            var sel = actOnCachedSelection(win);
+            var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+            if (sel) {
+                sel.nativeSelection = nativeSel;
+                sel.docSelection = docSel;
+                sel.refresh();
+            } else {
+                sel = new WrappedSelection(nativeSel, docSel, win);
+                cachedRangySelections.push( { win: win, selection: sel } );
+            }
+            return sel;
+        };
+
+        api.getSelection = getSelection;
+
+        util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
+
+        var selProto = WrappedSelection.prototype;
+
+        function createControlSelection(sel, ranges) {
+            // Ensure that the selection becomes of type "Control"
+            var doc = getDocument(ranges[0].startContainer);
+            var controlRange = getBody(doc).createControlRange();
+            for (var i = 0, el, len = ranges.length; i < len; ++i) {
+                el = getSingleElementFromRange(ranges[i]);
+                try {
+                    controlRange.add(el);
+                } catch (ex) {
+                    throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
+                }
+            }
+            controlRange.select();
+
+            // Update the wrapped selection based on what's now in the native selection
+            updateControlSelection(sel);
+        }
+
+        // Selecting a range
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+            selProto.removeAllRanges = function() {
+                this.nativeSelection.removeAllRanges();
+                updateEmptySelection(this);
+            };
+
+            var addRangeBackward = function(sel, range) {
+                addRangeBackwardToNative(sel.nativeSelection, range);
+                sel.refresh();
+            };
+
+            if (selectionHasRangeCount) {
+                selProto.addRange = function(range, direction) {
+                    if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                        addRangeToControlSelection(this, range);
+                    } else {
+                        if (isDirectionBackward(direction) && selectionHasExtend) {
+                            addRangeBackward(this, range);
+                        } else {
+                            var previousRangeCount;
+                            if (selectionSupportsMultipleRanges) {
+                                previousRangeCount = this.rangeCount;
+                            } else {
+                                this.removeAllRanges();
+                                previousRangeCount = 0;
+                            }
+                            // Clone the native range so that changing the selected range does not affect the selection.
+                            // This is contrary to the spec but is the only way to achieve consistency between browsers. See
+                            // issue 80.
+                            var clonedNativeRange = getNativeRange(range).cloneRange();
+                            try {
+                                this.nativeSelection.addRange(clonedNativeRange);
+                            } catch (ex) {
+                            }
+
+                            // Check whether adding the range was successful
+                            this.rangeCount = this.nativeSelection.rangeCount;
+
+                            if (this.rangeCount == previousRangeCount + 1) {
+                                // The range was added successfully
+
+                                // Check whether the range that we added to the selection is reflected in the last range extracted from
+                                // the selection
+                                if (api.config.checkSelectionRanges) {
+                                    var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+                                    if (nativeRange && !rangesEqual(nativeRange, range)) {
+                                        // Happens in WebKit with, for example, a selection placed at the start of a text node
+                                        range = new WrappedRange(nativeRange);
+                                    }
+                                }
+                                this._ranges[this.rangeCount - 1] = range;
+                                updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
+                                this.isCollapsed = selectionIsCollapsed(this);
+                            } else {
+                                // The range was not added successfully. The simplest thing is to refresh
+                                this.refresh();
+                            }
+                        }
+                    }
+                };
+            } else {
+                selProto.addRange = function(range, direction) {
+                    if (isDirectionBackward(direction) && selectionHasExtend) {
+                        addRangeBackward(this, range);
+                    } else {
+                        this.nativeSelection.addRange(getNativeRange(range));
+                        this.refresh();
+                    }
+                };
+            }
+
+            selProto.setRanges = function(ranges) {
+                if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
+                    createControlSelection(this, ranges);
+                } else {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        this.addRange(ranges[i]);
+                    }
+                }
+            };
+        } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
+                   implementsControlRange && useDocumentSelection) {
+
+            selProto.removeAllRanges = function() {
+                // Added try/catch as fix for issue #21
+                try {
+                    this.docSelection.empty();
+
+                    // Check for empty() not working (issue #24)
+                    if (this.docSelection.type != "None") {
+                        // Work around failure to empty a control selection by instead selecting a TextRange and then
+                        // calling empty()
+                        var doc;
+                        if (this.anchorNode) {
+                            doc = getDocument(this.anchorNode);
+                        } else if (this.docSelection.type == CONTROL) {
+                            var controlRange = this.docSelection.createRange();
+                            if (controlRange.length) {
+                                doc = getDocument( controlRange.item(0) );
+                            }
+                        }
+                        if (doc) {
+                            var textRange = getBody(doc).createTextRange();
+                            textRange.select();
+                            this.docSelection.empty();
+                        }
+                    }
+                } catch(ex) {}
+                updateEmptySelection(this);
+            };
+
+            selProto.addRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    api.WrappedTextRange.rangeToTextRange(range).select();
+                    this._ranges[0] = range;
+                    this.rangeCount = 1;
+                    this.isCollapsed = this._ranges[0].collapsed;
+                    updateAnchorAndFocusFromRange(this, range, false);
+                }
+            };
+
+            selProto.setRanges = function(ranges) {
+                this.removeAllRanges();
+                var rangeCount = ranges.length;
+                if (rangeCount > 1) {
+                    createControlSelection(this, ranges);
+                } else if (rangeCount) {
+                    this.addRange(ranges[0]);
+                }
+            };
+        } else {
+            module.fail("No means of selecting a Range or TextRange was found");
+            return false;
+        }
+
+        selProto.getRangeAt = function(index) {
+            if (index < 0 || index >= this.rangeCount) {
+                throw new DOMException("INDEX_SIZE_ERR");
+            } else {
+                // Clone the range to preserve selection-range independence. See issue 80.
+                return this._ranges[index].cloneRange();
+            }
+        };
+
+        var refreshSelection;
+
+        if (useDocumentSelection) {
+            refreshSelection = function(sel) {
+                var range;
+                if (api.isSelectionValid(sel.win)) {
+                    range = sel.docSelection.createRange();
+                } else {
+                    range = getBody(sel.win.document).createTextRange();
+                    range.collapse(true);
+                }
+
+                if (sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else if (isTextRange(range)) {
+                    updateFromTextRange(sel, range);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
+            refreshSelection = function(sel) {
+                if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+                    updateControlSelection(sel);
+                } else {
+                    sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+                    if (sel.rangeCount) {
+                        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                            sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+                        }
+                        updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
+                        sel.isCollapsed = selectionIsCollapsed(sel);
+                    } else {
+                        updateEmptySelection(sel);
+                    }
+                }
+            };
+        } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
+            refreshSelection = function(sel) {
+                var range, nativeSel = sel.nativeSelection;
+                if (nativeSel.anchorNode) {
+                    range = getSelectionRangeAt(nativeSel, 0);
+                    sel._ranges = [range];
+                    sel.rangeCount = 1;
+                    updateAnchorAndFocusFromNativeSelection(sel);
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            };
+        } else {
+            module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+            return false;
+        }
+
+        selProto.refresh = function(checkForChanges) {
+            var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+            var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
+
+            refreshSelection(this);
+            if (checkForChanges) {
+                // Check the range count first
+                var i = oldRanges.length;
+                if (i != this._ranges.length) {
+                    return true;
+                }
+
+                // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
+                // ranges after this
+                if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
+                    return true;
+                }
+
+                // Finally, compare each range in turn
+                while (i--) {
+                    if (!rangesEqual(oldRanges[i], this._ranges[i])) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        };
+
+        // Removal of a single range
+        var removeRangeManually = function(sel, range) {
+            var ranges = sel.getAllRanges();
+            sel.removeAllRanges();
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                if (!rangesEqual(range, ranges[i])) {
+                    sel.addRange(ranges[i]);
+                }
+            }
+            if (!sel.rangeCount) {
+                updateEmptySelection(sel);
+            }
+        };
+
+        if (implementsControlRange && implementsDocSelection) {
+            selProto.removeRange = function(range) {
+                if (this.docSelection.type == CONTROL) {
+                    var controlRange = this.docSelection.createRange();
+                    var rangeElement = getSingleElementFromRange(range);
+
+                    // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                    // element contained by the supplied range
+                    var doc = getDocument(controlRange.item(0));
+                    var newControlRange = getBody(doc).createControlRange();
+                    var el, removed = false;
+                    for (var i = 0, len = controlRange.length; i < len; ++i) {
+                        el = controlRange.item(i);
+                        if (el !== rangeElement || removed) {
+                            newControlRange.add(controlRange.item(i));
+                        } else {
+                            removed = true;
+                        }
+                    }
+                    newControlRange.select();
+
+                    // Update the wrapped selection based on what's now in the native selection
+                    updateControlSelection(this);
+                } else {
+                    removeRangeManually(this, range);
+                }
+            };
+        } else {
+            selProto.removeRange = function(range) {
+                removeRangeManually(this, range);
+            };
+        }
+
+        // Detecting if a selection is backward
+        var selectionIsBackward;
+        if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
+            selectionIsBackward = winSelectionIsBackward;
+
+            selProto.isBackward = function() {
+                return selectionIsBackward(this);
+            };
+        } else {
+            selectionIsBackward = selProto.isBackward = function() {
+                return false;
+            };
+        }
+
+        // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
+        selProto.isBackwards = selProto.isBackward;
+
+        // Selection stringifier
+        // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
+        // The current spec does not yet define this method.
+        selProto.toString = function() {
+            var rangeTexts = [];
+            for (var i = 0, len = this.rangeCount; i < len; ++i) {
+                rangeTexts[i] = "" + this._ranges[i];
+            }
+            return rangeTexts.join("");
+        };
+
+        function assertNodeInSameDocument(sel, node) {
+            if (sel.win.document != getDocument(node)) {
+                throw new DOMException("WRONG_DOCUMENT_ERR");
+            }
+        }
+
+        // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
+        selProto.collapse = function(node, offset) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.collapseToPoint(node, offset);
+            this.setSingleRange(range);
+            this.isCollapsed = true;
+        };
+
+        selProto.collapseToStart = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[0];
+                this.collapse(range.startContainer, range.startOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        selProto.collapseToEnd = function() {
+            if (this.rangeCount) {
+                var range = this._ranges[this.rangeCount - 1];
+                this.collapse(range.endContainer, range.endOffset);
+            } else {
+                throw new DOMException("INVALID_STATE_ERR");
+            }
+        };
+
+        // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
+        // specified so the native implementation is never used by Rangy.
+        selProto.selectAllChildren = function(node) {
+            assertNodeInSameDocument(this, node);
+            var range = api.createRange(node);
+            range.selectNodeContents(node);
+            this.setSingleRange(range);
+        };
+
+        selProto.deleteFromDocument = function() {
+            // Sepcial behaviour required for IE's control selections
+            if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var element;
+                while (controlRange.length) {
+                    element = controlRange.item(0);
+                    controlRange.remove(element);
+                    dom.removeNode(element);
+                }
+                this.refresh();
+            } else if (this.rangeCount) {
+                var ranges = this.getAllRanges();
+                if (ranges.length) {
+                    this.removeAllRanges();
+                    for (var i = 0, len = ranges.length; i < len; ++i) {
+                        ranges[i].deleteContents();
+                    }
+                    // The spec says nothing about what the selection should contain after calling deleteContents on each
+                    // range. Firefox moves the selection to where the final selected range was, so we emulate that
+                    this.addRange(ranges[len - 1]);
+                }
+            }
+        };
+
+        // The following are non-standard extensions
+        selProto.eachRange = function(func, returnValue) {
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {
+                if ( func( this.getRangeAt(i) ) ) {
+                    return returnValue;
+                }
+            }
+        };
+
+        selProto.getAllRanges = function() {
+            var ranges = [];
+            this.eachRange(function(range) {
+                ranges.push(range);
+            });
+            return ranges;
+        };
+
+        selProto.setSingleRange = function(range, direction) {
+            this.removeAllRanges();
+            this.addRange(range, direction);
+        };
+
+        selProto.callMethodOnEachRange = function(methodName, params) {
+            var results = [];
+            this.eachRange( function(range) {
+                results.push( range[methodName].apply(range, params || []) );
+            } );
+            return results;
+        };
+
+        function createStartOrEndSetter(isStart) {
+            return function(node, offset) {
+                var range;
+                if (this.rangeCount) {
+                    range = this.getRangeAt(0);
+                    range["set" + (isStart ? "Start" : "End")](node, offset);
+                } else {
+                    range = api.createRange(this.win.document);
+                    range.setStartAndEnd(node, offset);
+                }
+                this.setSingleRange(range, this.isBackward());
+            };
+        }
+
+        selProto.setStart = createStartOrEndSetter(true);
+        selProto.setEnd = createStartOrEndSetter(false);
+
+        // Add select() method to Range prototype. Any existing selection will be removed.
+        api.rangePrototype.select = function(direction) {
+            getSelection( this.getDocument() ).setSingleRange(this, direction);
+        };
+
+        selProto.changeEachRange = function(func) {
+            var ranges = [];
+            var backward = this.isBackward();
+
+            this.eachRange(function(range) {
+                func(range);
+                ranges.push(range);
+            });
+
+            this.removeAllRanges();
+            if (backward && ranges.length == 1) {
+                this.addRange(ranges[0], "backward");
+            } else {
+                this.setRanges(ranges);
+            }
+        };
+
+        selProto.containsNode = function(node, allowPartial) {
+            return this.eachRange( function(range) {
+                return range.containsNode(node, allowPartial);
+            }, true ) || false;
+        };
+
+        selProto.getBookmark = function(containerNode) {
+            return {
+                backward: this.isBackward(),
+                rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
+            };
+        };
+
+        selProto.moveToBookmark = function(bookmark) {
+            var selRanges = [];
+            for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
+                range = api.createRange(this.win);
+                range.moveToBookmark(rangeBookmark);
+                selRanges.push(range);
+            }
+            if (bookmark.backward) {
+                this.setSingleRange(selRanges[0], "backward");
+            } else {
+                this.setRanges(selRanges);
+            }
+        };
+
+        selProto.saveRanges = function() {
+            return {
+                backward: this.isBackward(),
+                ranges: this.callMethodOnEachRange("cloneRange")
+            };
+        };
+
+        selProto.restoreRanges = function(selRanges) {
+            this.removeAllRanges();
+            for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
+                this.addRange(range, (selRanges.backward && i == 0));
+            }
+        };
+
+        selProto.toHtml = function() {
+            var rangeHtmls = [];
+            this.eachRange(function(range) {
+                rangeHtmls.push( DomRange.toHtml(range) );
+            });
+            return rangeHtmls.join("");
+        };
+
+        if (features.implementsTextRange) {
+            selProto.getNativeTextRange = function() {
+                var sel, textRange;
+                if ( (sel = this.docSelection) ) {
+                    var range = sel.createRange();
+                    if (isTextRange(range)) {
+                        return range;
+                    } else {
+                        throw module.createError("getNativeTextRange: selection is a control selection");
+                    }
+                } else if (this.rangeCount > 0) {
+                    return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
+                } else {
+                    throw module.createError("getNativeTextRange: selection contains no range");
+                }
+            };
+        }
+
+        function inspect(sel) {
+            var rangeInspects = [];
+            var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+            var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+            var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+            if (typeof sel.rangeCount != "undefined") {
+                for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                    rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+                }
+            }
+            return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+                    ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+        }
+
+        selProto.getName = function() {
+            return "WrappedSelection";
+        };
+
+        selProto.inspect = function() {
+            return inspect(this);
+        };
+
+        selProto.detach = function() {
+            actOnCachedSelection(this.win, "delete");
+            deleteProperties(this);
+        };
+
+        WrappedSelection.detachAll = function() {
+            actOnCachedSelection(null, "deleteAll");
+        };
+
+        WrappedSelection.inspect = inspect;
+        WrappedSelection.isDirectionBackward = isDirectionBackward;
+
+        api.Selection = WrappedSelection;
+
+        api.selectionPrototype = selProto;
+
+        api.addShimListener(function(win) {
+            if (typeof win.getSelection == "undefined") {
+                win.getSelection = function() {
+                    return getSelection(win);
+                };
+            }
+            win = null;
+        });
+    });
+    
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    // Wait for document to load before initializing
+    var docReady = false;
+
+    var loadHandler = function(e) {
+        if (!docReady) {
+            docReady = true;
+            if (!api.initialized && api.config.autoInitialize) {
+                init();
+            }
+        }
+    };
+
+    if (isBrowser) {
+        // Test whether the document has already been loaded and initialize immediately if so
+        if (document.readyState == "complete") {
+            loadHandler();
+        } else {
+            if (isHostMethod(document, "addEventListener")) {
+                document.addEventListener("DOMContentLoaded", loadHandler, false);
+            }
+
+            // Add a fallback in case the DOMContentLoaded event isn't supported
+            addListener(window, "load", loadHandler);
+        }
+    }
+
+    return api;
+}, this);
+;/**
+ * Text range module for Rangy.
+ * Text-based manipulation and searching of ranges and selections.
+ *
+ * Features
+ *
+ * - Ability to move range boundaries by character or word offsets
+ * - Customizable word tokenizer
+ * - Ignores text nodes inside 
+ */
+(function(wysihtml5) {
+  var /**
+       * Don't auto-link urls that are contained in the following elements:
+       */
+      IGNORE_URLS_IN        = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
+      /**
+       * revision 1:
+       *    /(\S+\.{1}[^\s\,\.\!]+)/g
+       *
+       * revision 2:
+       *    /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
+       *
+       * put this in the beginning if you don't wan't to match within a word
+       *    (^|[\>\(\{\[\s\>])
+       */
+      URL_REG_EXP           = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
+      TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
+      MAX_DISPLAY_LENGTH    = 100,
+      BRACKETS              = { ")": "(", "]": "[", "}": "{" };
+
+  function autoLink(element, ignoreInClasses) {
+    if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
+      return element;
+    }
+
+    if (element === element.ownerDocument.documentElement) {
+      element = element.ownerDocument.body;
+    }
+
+    return _parseNode(element, ignoreInClasses);
+  }
+
+  /**
+   * This is basically a rebuild of
+   * the rails auto_link_urls text helper
+   */
+  function _convertUrlsToLinks(str) {
+    return str.replace(URL_REG_EXP, function(match, url) {
+      var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
+          opening     = BRACKETS[punctuation];
+      url = url.replace(TRAILING_CHAR_REG_EXP, "");
+
+      if (url.split(opening).length > url.split(punctuation).length) {
+        url = url + punctuation;
+        punctuation = "";
+      }
+      var realUrl    = url,
+          displayUrl = url;
+      if (url.length > MAX_DISPLAY_LENGTH) {
+        displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
+      }
+      // Add http prefix if necessary
+      if (realUrl.substr(0, 4) === "www.") {
+        realUrl = "http://" + realUrl;
+      }
+
+      return '' + displayUrl + '' + punctuation;
+    });
+  }
+
+  /**
+   * Creates or (if already cached) returns a temp element
+   * for the given document object
+   */
+  function _getTempElement(context) {
+    var tempElement = context._wysihtml5_tempElement;
+    if (!tempElement) {
+      tempElement = context._wysihtml5_tempElement = context.createElement("div");
+    }
+    return tempElement;
+  }
+
+  /**
+   * Replaces the original text nodes with the newly auto-linked dom tree
+   */
+  function _wrapMatchesInNode(textNode) {
+    var parentNode  = textNode.parentNode,
+        nodeValue   = wysihtml5.lang.string(textNode.data).escapeHTML(),
+        tempElement = _getTempElement(parentNode.ownerDocument);
+
+    // We need to insert an empty/temporary  to fix IE quirks
+    // Elsewise IE would strip white space in the beginning
+    tempElement.innerHTML = "" + _convertUrlsToLinks(nodeValue);
+    tempElement.removeChild(tempElement.firstChild);
+
+    while (tempElement.firstChild) {
+      // inserts tempElement.firstChild before textNode
+      parentNode.insertBefore(tempElement.firstChild, textNode);
+    }
+    parentNode.removeChild(textNode);
+  }
+
+  function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
+    var nodeName;
+    while (node.parentNode) {
+      node = node.parentNode;
+      nodeName = node.nodeName;
+      if (node.className && wysihtml5.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
+        return true;
+      }
+      if (IGNORE_URLS_IN.contains(nodeName)) {
+        return true;
+      } else if (nodeName === "body") {
+        return false;
+      }
+    }
+    return false;
+  }
+
+  function _parseNode(element, ignoreInClasses) {
+    if (IGNORE_URLS_IN.contains(element.nodeName)) {
+      return;
+    }
+
+    if (element.className && wysihtml5.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
+      return;
+    }
+
+    if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
+      _wrapMatchesInNode(element);
+      return;
+    }
+
+    var childNodes        = wysihtml5.lang.array(element.childNodes).get(),
+        childNodesLength  = childNodes.length,
+        i                 = 0;
+
+    for (; i 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+  };
+})(wysihtml5);
+;wysihtml5.dom.contains = (function() {
+  var documentElement = document.documentElement;
+  if (documentElement.contains) {
+    return function(container, element) {
+      if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
+        if (element.parentNode === container) {
+          return true;
+        }
+        element = element.parentNode;
+      }
+      return container !== element && container.contains(element);
+    };
+  } else if (documentElement.compareDocumentPosition) {
+    return function(container, element) {
+      // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
+      return !!(container.compareDocumentPosition(element) & 16);
+    };
+  }
+})();
+;/**
+ * Converts an HTML fragment/element into a unordered/ordered list
+ *
+ * @param {Element} element The element which should be turned into a list
+ * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
+ * @return {Element} The created list
+ *
+ * @example
+ *    
+ *    
+ *      eminem
+ * dr. dre + *
50 Cent
+ *
+ * + * + * + * + *
    + *
  • eminem
  • + *
  • dr. dre
  • + *
  • 50 Cent
  • + *
+ */ +wysihtml5.dom.convertToList = (function() { + function _createListItem(doc, list) { + var listItem = doc.createElement("li"); + list.appendChild(listItem); + return listItem; + } + + function _createList(doc, type) { + return doc.createElement(type); + } + + function convertToList(element, listType, uneditableClass) { + if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { + // Already a list + return element; + } + + var doc = element.ownerDocument, + list = _createList(doc, listType), + lineBreaks = element.querySelectorAll("br"), + lineBreaksLength = lineBreaks.length, + childNodes, + childNodesLength, + childNode, + lineBreak, + parentNode, + isBlockElement, + isLineBreak, + currentListItem, + i; + + // First find
at the end of inline elements and move them behind them + for (i=0; i if empty, otherwise create a new one + currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; + currentListItem.appendChild(childNode); + currentListItem = null; + continue; + } + + if (isLineBreak) { + // Only create a new list item in the next iteration when the current one has already content + currentListItem = currentListItem.firstChild ? null : currentListItem; + continue; + } + + currentListItem.appendChild(childNode); + } + + if (childNodes.length === 0) { + _createListItem(doc, list); + } + + element.parentNode.replaceChild(list, element); + return list; + } + + return convertToList; +})(); +;/** + * Copy a set of attributes from one element to another + * + * @param {Array} attributesToCopy List of attributes which should be copied + * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to + * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked + * with the element where to copy the attributes to (see example) + * + * @example + * var textarea = document.querySelector("textarea"), + * div = document.querySelector("div[contenteditable=true]"), + * anotherDiv = document.querySelector("div.preview"); + * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); + * + */ +wysihtml5.dom.copyAttributes = function(attributesToCopy) { + return { + from: function(elementToCopyFrom) { + return { + to: function(elementToCopyTo) { + var attribute, + i = 0, + length = attributesToCopy.length; + for (; i 0) { + var hasOneStyle = false, + styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty]; + for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) { + // Some old IE-s have different property name for cssFloat + prop = wysihtml5.browser.fixStyleKey(styles[j]); + if (node.style[prop]) { + if (properties.styleValue) { + // Style value as additional parameter + if (properties.styleValue instanceof RegExp) { + // style value as Regexp + if (node.style[prop].trim().match(properties.styleValue).length > 0) { + hasOneStyle = true; + break; + } + } else if (Array.isArray(properties.styleValue)) { + // style value as array + if (properties.styleValue.indexOf(node.style[prop].trim())) { + hasOneStyle = true; + break; + } + } else { + // style value as string + if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) { + hasOneStyle = true; + break; + } + } + } else { + hasOneStyle = true; + break; + } + } + if (!hasOneStyle) { + return false; + } + } + } + + if (properties.attribute) { + var attr = wysihtml5.dom.getAttributes(node), + attrList = [], + hasOneAttribute = false; + + if (Array.isArray(properties.attribute)) { + attrList = properties.attribute; + } else { + attrList[properties.attribute] = properties.attributeValue; + } + + for (var a in attrList) { + if (attrList.hasOwnProperty(a)) { + if (typeof attrList[a] === "undefined") { + if (typeof attr[a] !== "undefined") { + hasOneAttribute = true; + break; + } + } else if (attr[a] === attrList[a]) { + hasOneAttribute = true; + break; + } + } + } + + if (!hasOneAttribute) { + return false; + } + + } + + return true; + } + + }; + }; +})(wysihtml5); +;/** + * Returns the given html wrapped in a div element + * + * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly + * when inserted via innerHTML + * + * @param {String} html The html which should be wrapped in a dom element + * @param {Obejct} [context] Document object of the context the html belongs to + * + * @example + * wysihtml5.dom.getAsDom("
foo
"); + */ +wysihtml5.dom.getAsDom = (function() { + + var _innerHTMLShiv = function(html, context) { + var tempElement = context.createElement("div"); + tempElement.style.display = "none"; + context.body.appendChild(tempElement); + // IE throws an exception when trying to insert via innerHTML + try { tempElement.innerHTML = html; } catch(e) {} + context.body.removeChild(tempElement); + return tempElement; + }; + + /** + * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element + */ + var _ensureHTML5Compatibility = function(context) { + if (context._wysihtml5_supportsHTML5Tags) { + return; + } + for (var i=0, length=HTML5_ELEMENTS.length; i "block" + */ +wysihtml5.dom.getStyle = (function() { + var stylePropertyMapping = { + "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" + }, + REG_EXP_CAMELIZE = /\-[a-z]/g; + + function camelize(str) { + return str.replace(REG_EXP_CAMELIZE, function(match) { + return match.charAt(1).toUpperCase(); + }); + } + + return function(property) { + return { + from: function(element) { + if (element.nodeType !== wysihtml5.ELEMENT_NODE) { + return; + } + + var doc = element.ownerDocument, + camelizedProperty = stylePropertyMapping[property] || camelize(property), + style = element.style, + currentStyle = element.currentStyle, + styleValue = style[camelizedProperty]; + if (styleValue) { + return styleValue; + } + + // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant + // window.getComputedStyle, since it returns css property values in their original unit: + // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle + // gives you the original "50%". + // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio + if (currentStyle) { + try { + return currentStyle[camelizedProperty]; + } catch(e) { + //ie will occasionally fail for unknown reasons. swallowing exception + } + } + + var win = doc.defaultView || doc.parentWindow, + needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA", + originalOverflow, + returnValue; + + if (win.getComputedStyle) { + // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars + // therfore we remove and restore the scrollbar and calculate the value in between + if (needsOverflowReset) { + originalOverflow = style.overflow; + style.overflow = "hidden"; + } + returnValue = win.getComputedStyle(element, null).getPropertyValue(property); + if (needsOverflowReset) { + style.overflow = originalOverflow || ""; + } + return returnValue; + } + } + }; + }; +})(); +;wysihtml5.dom.getTextNodes = function(node, ingoreEmpty){ + var all = []; + for (node=node.firstChild;node;node=node.nextSibling){ + if (node.nodeType == 3) { + if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) { + all.push(node); + } + } else { + all = all.concat(wysihtml5.dom.getTextNodes(node, ingoreEmpty)); + } + } + return all; +}; +;/** + * High performant way to check whether an element with a specific tag name is in the given document + * Optimized for being heavily executed + * Unleashes the power of live node lists + * + * @param {Object} doc The document object of the context where to check + * @param {String} tagName Upper cased tag name + * @example + * wysihtml5.dom.hasElementWithTagName(document, "IMG"); + */ +wysihtml5.dom.hasElementWithTagName = (function() { + var LIVE_CACHE = {}, + DOCUMENT_IDENTIFIER = 1; + + function _getDocumentIdentifier(doc) { + return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); + } + + return function(doc, tagName) { + var key = _getDocumentIdentifier(doc) + ":" + tagName, + cacheEntry = LIVE_CACHE[key]; + if (!cacheEntry) { + cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); + } + + return cacheEntry.length > 0; + }; +})(); +;/** + * High performant way to check whether an element with a specific class name is in the given document + * Optimized for being heavily executed + * Unleashes the power of live node lists + * + * @param {Object} doc The document object of the context where to check + * @param {String} tagName Upper cased tag name + * @example + * wysihtml5.dom.hasElementWithClassName(document, "foobar"); + */ +(function(wysihtml5) { + var LIVE_CACHE = {}, + DOCUMENT_IDENTIFIER = 1; + + function _getDocumentIdentifier(doc) { + return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); + } + + wysihtml5.dom.hasElementWithClassName = function(doc, className) { + // getElementsByClassName is not supported by IE<9 + // but is sometimes mocked via library code (which then doesn't return live node lists) + if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) { + return !!doc.querySelector("." + className); + } + + var key = _getDocumentIdentifier(doc) + ":" + className, + cacheEntry = LIVE_CACHE[key]; + if (!cacheEntry) { + cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); + } + + return cacheEntry.length > 0; + }; +})(wysihtml5); +;wysihtml5.dom.insert = function(elementToInsert) { + return { + after: function(element) { + element.parentNode.insertBefore(elementToInsert, element.nextSibling); + }, + + before: function(element) { + element.parentNode.insertBefore(elementToInsert, element); + }, + + into: function(element) { + element.appendChild(elementToInsert); + } + }; +}; +;wysihtml5.dom.insertCSS = function(rules) { + rules = rules.join("\n"); + + return { + into: function(doc) { + var styleElement = doc.createElement("style"); + styleElement.type = "text/css"; + + if (styleElement.styleSheet) { + styleElement.styleSheet.cssText = rules; + } else { + styleElement.appendChild(doc.createTextNode(rules)); + } + + var link = doc.querySelector("head link"); + if (link) { + link.parentNode.insertBefore(styleElement, link); + return; + } else { + var head = doc.querySelector("head"); + if (head) { + head.appendChild(styleElement); + } + } + } + }; +}; +;// TODO: Refactor dom tree traversing here +(function(wysihtml5) { + wysihtml5.dom.lineBreaks = function(node) { + + function _isLineBreak(n) { + return n.nodeName === "BR"; + } + + /** + * Checks whether the elment causes a visual line break + * (
or block elements) + */ + function _isLineBreakOrBlockElement(element) { + if (_isLineBreak(element)) { + return true; + } + + if (wysihtml5.dom.getStyle("display").from(element) === "block") { + return true; + } + + return false; + } + + return { + + /* wysihtml5.dom.lineBreaks(element).add(); + * + * Adds line breaks before and after the given node if the previous and next siblings + * aren't already causing a visual line break (block element or
) + */ + add: function(options) { + var doc = node.ownerDocument, + nextSibling = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}), + previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true}); + + if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { + wysihtml5.dom.insert(doc.createElement("br")).after(node); + } + if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { + wysihtml5.dom.insert(doc.createElement("br")).before(node); + } + }, + + /* wysihtml5.dom.lineBreaks(element).remove(); + * + * Removes line breaks before and after the given node + */ + remove: function(options) { + var nextSibling = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}), + previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true}); + + if (nextSibling && _isLineBreak(nextSibling)) { + nextSibling.parentNode.removeChild(nextSibling); + } + if (previousSibling && _isLineBreak(previousSibling)) { + previousSibling.parentNode.removeChild(previousSibling); + } + } + }; + }; +})(wysihtml5);;/** + * Method to set dom events + * + * @example + * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); + */ +wysihtml5.dom.observe = function(element, eventNames, handler) { + eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; + + var handlerWrapper, + eventName, + i = 0, + length = eventNames.length; + + for (; i
foo bar
+ * + * var userHTML = '
I'm a table!
'; + * wysihtml5.dom.parse(userHTML); + * // => 'I'm a table!' + * + * var userHTML = '
foobar
foobar
'; + * wysihtml5.dom.parse(userHTML, { + * tags: { + * div: undefined, + * br: true + * } + * }); + * // => '' + * + * var userHTML = '
foo
bar
'; + * wysihtml5.dom.parse(userHTML, { + * classes: { + * red: 1, + * green: 1 + * }, + * tags: { + * div: { + * rename_tag: "p" + * } + * } + * }); + * // => '

foo

bar

' + */ + +wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { + /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors. + * Refactor whole code as this method while workind is kind of awkward too */ + + /** + * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML + * new DOMParser().parseFromString('') will cause a parseError since the + * node isn't closed + * + * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML. + */ + var NODE_TYPE_MAPPING = { + "1": _handleElement, + "3": _handleText, + "8": _handleComment + }, + // Rename unknown tags to this + DEFAULT_NODE_NAME = "span", + WHITE_SPACE_REG_EXP = /\s+/, + defaultRules = { tags: {}, classes: {} }, + currentRules = {}, + blockElements = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" , + "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU", + "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"]; + + /** + * Iterates over all childs of the element, recreates them, appends them into a document fragment + * which later replaces the entire body content + */ + function parse(elementOrHtml, config) { + wysihtml5.lang.object(currentRules).merge(defaultRules).merge(config.rules).get(); + + var context = config.context || elementOrHtml.ownerDocument || document, + fragment = context.createDocumentFragment(), + isString = typeof(elementOrHtml) === "string", + clearInternals = false, + element, + newNode, + firstChild; + + if (config.clearInternals === true) { + clearInternals = true; + } + + if (isString) { + element = wysihtml5.dom.getAsDom(elementOrHtml, context); + } else { + element = elementOrHtml; + } + + if (currentRules.selectors) { + _applySelectorRules(element, currentRules.selectors); + } + + while (element.firstChild) { + firstChild = element.firstChild; + newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass); + if (newNode) { + fragment.appendChild(newNode); + } + if (firstChild !== newNode) { + element.removeChild(firstChild); + } + } + + if (config.unjoinNbsps) { + // replace joined non-breakable spaces with unjoined + var txtnodes = wysihtml5.dom.getTextNodes(fragment); + for (var n = txtnodes.length; n--;) { + txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 "); + } + } + + // Clear element contents + element.innerHTML = ""; + + // Insert new DOM tree + element.appendChild(fragment); + + return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; + } + + function _convert(oldNode, cleanUp, clearInternals, uneditableClass) { + var oldNodeType = oldNode.nodeType, + oldChilds = oldNode.childNodes, + oldChildsLength = oldChilds.length, + method = NODE_TYPE_MAPPING[oldNodeType], + i = 0, + fragment, + newNode, + newChild, + nodeDisplay; + + // Passes directly elemets with uneditable class + if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) { + return oldNode; + } + + newNode = method && method(oldNode, clearInternals); + + // Remove or unwrap node in case of return value null or false + if (!newNode) { + if (newNode === false) { + // false defines that tag should be removed but contents should remain (unwrap) + fragment = oldNode.ownerDocument.createDocumentFragment(); + + for (i = oldChildsLength; i--;) { + if (oldChilds[i]) { + newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass); + if (newChild) { + if (oldChilds[i] === newChild) { + i--; + } + fragment.insertBefore(newChild, fragment.firstChild); + } + } + } + + nodeDisplay = wysihtml5.dom.getStyle("display").from(oldNode); + + if (nodeDisplay === '') { + // Handle display style when element not in dom + nodeDisplay = wysihtml5.lang.array(blockElements).contains(oldNode.tagName) ? "block" : ""; + } + if (wysihtml5.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) { + fragment.appendChild(oldNode.ownerDocument.createElement("br")); + } + + // TODO: try to minimize surplus spaces + if (wysihtml5.lang.array([ + "div", "pre", "p", + "table", "td", "th", + "ul", "ol", "li", + "dd", "dl", + "footer", "header", "section", + "h1", "h2", "h3", "h4", "h5", "h6" + ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) { + // add space at first when unwraping non-textflow elements + if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) { + fragment.appendChild(oldNode.ownerDocument.createTextNode(" ")); + } + } + + if (fragment.normalize) { + fragment.normalize(); + } + return fragment; + } else { + // Remove + return null; + } + } + + // Converts all childnodes + for (i=0; i elements + if (cleanUp && + newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && + (!newNode.childNodes.length || + ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml5-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) || + !newNode.attributes.length) + ) { + fragment = newNode.ownerDocument.createDocumentFragment(); + while (newNode.firstChild) { + fragment.appendChild(newNode.firstChild); + } + if (fragment.normalize) { + fragment.normalize(); + } + return fragment; + } + + if (newNode.normalize) { + newNode.normalize(); + } + return newNode; + } + + function _applySelectorRules (element, selectorRules) { + var sel, method, els; + + for (sel in selectorRules) { + if (selectorRules.hasOwnProperty(sel)) { + if (wysihtml5.lang.object(selectorRules[sel]).isFunction()) { + method = selectorRules[sel]; + } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) { + method = elementHandlingMethods[selectorRules[sel]]; + } + els = element.querySelectorAll(sel); + for (var i = els.length; i--;) { + method(els[i]); + } + } + } + } + + function _handleElement(oldNode, clearInternals) { + var rule, + newNode, + tagRules = currentRules.tags, + nodeName = oldNode.nodeName.toLowerCase(), + scopeName = oldNode.scopeName, + renameTag; + + /** + * We already parsed that element + * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) + */ + if (oldNode._wysihtml5) { + return null; + } + oldNode._wysihtml5 = 1; + + if (oldNode.className === "wysihtml5-temp") { + return null; + } + + /** + * IE is the only browser who doesn't include the namespace in the + * nodeName, that's why we have to prepend it by ourselves + * scopeName is a proprietary IE feature + * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx + */ + if (scopeName && scopeName != "HTML") { + nodeName = scopeName + ":" + nodeName; + } + /** + * Repair node + * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags + * A

doesn't need to be closed according HTML4-5 spec, we simply replace it with a

to preserve its content and layout + */ + if ("outerHTML" in oldNode) { + if (!wysihtml5.browser.autoClosesUnclosedTags() && + oldNode.nodeName === "P" && + oldNode.outerHTML.slice(-4).toLowerCase() !== "

") { + nodeName = "div"; + } + } + + if (nodeName in tagRules) { + rule = tagRules[nodeName]; + if (!rule || rule.remove) { + return null; + } else if (rule.unwrap) { + return false; + } + rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; + } else if (oldNode.firstChild) { + rule = { rename_tag: DEFAULT_NODE_NAME }; + } else { + // Remove empty unknown elements + return null; + } + + // tests if type condition is met or node should be removed/unwrapped/renamed + if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) { + if (rule.remove_action) { + if (rule.remove_action === "unwrap") { + return false; + } else if (rule.remove_action === "rename") { + renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME; + } else { + return null; + } + } else { + return null; + } + } + + newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName); + _handleAttributes(oldNode, newNode, rule, clearInternals); + _handleStyles(oldNode, newNode, rule); + + oldNode = null; + + if (newNode.normalize) { newNode.normalize(); } + return newNode; + } + + function _testTypes(oldNode, rules, types, clearInternals) { + var definition, type; + + // do not interfere with placeholder span or pasting caret position is not maintained + if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml5-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) { + return true; + } + + for (type in types) { + if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) { + definition = rules.type_definitions[type]; + if (_testType(oldNode, definition)) { + return true; + } + } + } + return false; + } + + function array_contains(a, obj) { + var i = a.length; + while (i--) { + if (a[i] === obj) { + return true; + } + } + return false; + } + + function _testType(oldNode, definition) { + + var nodeClasses = oldNode.getAttribute("class"), + nodeStyles = oldNode.getAttribute("style"), + classesLength, s, s_corrected, a, attr, currentClass, styleProp; + + // test for methods + if (definition.methods) { + for (var m in definition.methods) { + if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) { + + if (typeCeckMethods[m](oldNode)) { + return true; + } + } + } + } + + // test for classes, if one found return true + if (nodeClasses && definition.classes) { + nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP); + classesLength = nodeClasses.length; + for (var i = 0; i < classesLength; i++) { + if (definition.classes[nodeClasses[i]]) { + return true; + } + } + } + + // test for styles, if one found return true + if (nodeStyles && definition.styles) { + + nodeStyles = nodeStyles.split(';'); + for (s in definition.styles) { + if (definition.styles.hasOwnProperty(s)) { + for (var sp = nodeStyles.length; sp--;) { + styleProp = nodeStyles[sp].split(':'); + + if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) { + if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml5.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) { + return true; + } + } + } + } + } + } + + // test for attributes in general against regex match + if (definition.attrs) { + for (a in definition.attrs) { + if (definition.attrs.hasOwnProperty(a)) { + attr = wysihtml5.dom.getAttribute(oldNode, a); + if (typeof(attr) === "string") { + if (attr.search(definition.attrs[a]) > -1) { + return true; + } + } + } + } + } + return false; + } + + function _handleStyles(oldNode, newNode, rule) { + var s, v; + if(rule && rule.keep_styles) { + for (s in rule.keep_styles) { + if (rule.keep_styles.hasOwnProperty(s)) { + v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s]; + // value can be regex and if so should match or style skipped + if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) { + continue; + } + if (s === "float") { + // IE compability + newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v; + } else if (oldNode.style[s]) { + newNode.style[s] = v; + } + } + } + } + }; + + function _getAttributesBeginningWith(beginning, attributes) { + var returnAttributes = []; + for (var attr in attributes) { + if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) { + returnAttributes.push(attr); + } + } + return returnAttributes; + } + + function _checkAttribute(attributeName, attributeValue, methodName, nodeName) { + var method = wysihtml5.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName], + newAttributeValue; + + if (method) { + newAttributeValue = method(attributeValue, nodeName); + if (typeof(newAttributeValue) === "string") { + return newAttributeValue; + } + } + + return false; + } + + function _checkAttributes(oldNode, local_attributes) { + var globalAttributes = wysihtml5.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes + checkAttributes = wysihtml5.lang.object(globalAttributes).merge( wysihtml5.lang.object(local_attributes || {}).clone()).get(), + attributes = {}, + oldAttributes = wysihtml5.dom.getAttributes(oldNode), + attributeName, newValue, matchingAttributes; + + for (attributeName in checkAttributes) { + if ((/\*$/).test(attributeName)) { + + matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes); + for (var i = 0, imax = matchingAttributes.length; i < imax; i++) { + + newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName); + if (newValue !== false) { + attributes[matchingAttributes[i]] = newValue; + } + } + } else { + newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName); + if (newValue !== false) { + attributes[attributeName] = newValue; + } + } + } + + return attributes; + } + + // TODO: refactor. Too long to read + function _handleAttributes(oldNode, newNode, rule, clearInternals) { + var attributes = {}, // fresh new set of attributes to set on newNode + setClass = rule.set_class, // classes to set + addClass = rule.add_class, // add classes based on existing attributes + addStyle = rule.add_style, // add styles based on existing attributes + setAttributes = rule.set_attributes, // attributes to set on the current node + allowedClasses = currentRules.classes, + i = 0, + classes = [], + styles = [], + newClasses = [], + oldClasses = [], + classesLength, + newClassesLength, + currentClass, + newClass, + attributeName, + method; + + if (setAttributes) { + attributes = wysihtml5.lang.object(setAttributes).clone(); + } + + // check/convert values of attributes + attributes = wysihtml5.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get(); + + if (setClass) { + classes.push(setClass); + } + + if (addClass) { + for (attributeName in addClass) { + method = addClassMethods[addClass[attributeName]]; + if (!method) { + continue; + } + newClass = method(wysihtml5.dom.getAttribute(oldNode, attributeName)); + if (typeof(newClass) === "string") { + classes.push(newClass); + } + } + } + + if (addStyle) { + for (attributeName in addStyle) { + method = addStyleMethods[addStyle[attributeName]]; + if (!method) { + continue; + } + + newStyle = method(wysihtml5.dom.getAttribute(oldNode, attributeName)); + if (typeof(newStyle) === "string") { + styles.push(newStyle); + } + } + } + + + if (typeof(allowedClasses) === "string" && allowedClasses === "any") { + if (oldNode.getAttribute("class")) { + if (currentRules.classes_blacklist) { + oldClasses = oldNode.getAttribute("class"); + if (oldClasses) { + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); + } + + classesLength = classes.length; + for (; i 0) { + attributes["class"] = wysihtml5.lang.array(classes).unique().join(" "); + } + } + } else { + // make sure that wysihtml5 temp class doesn't get stripped out + if (!clearInternals) { + allowedClasses["_wysihtml5-temp-placeholder"] = 1; + allowedClasses["_rangySelectionBoundary"] = 1; + allowedClasses["wysiwyg-tmp-selected-cell"] = 1; + } + + // add old classes last + oldClasses = oldNode.getAttribute("class"); + if (oldClasses) { + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); + } + classesLength = classes.length; + for (; i under https when it's new attribute value is non-https + // TODO: Investigate this further and check for smarter handling + try { + newNode.setAttribute(attributeName, attributes[attributeName]); + } catch(e) {} + } + + // IE8 sometimes loses the width/height attributes when those are set before the "src" + // so we make sure to set them again + if (attributes.src) { + if (typeof(attributes.width) !== "undefined") { + newNode.setAttribute("width", attributes.width); + } + if (typeof(attributes.height) !== "undefined") { + newNode.setAttribute("height", attributes.height); + } + } + } + + function _handleText(oldNode) { + var nextSibling = oldNode.nextSibling; + if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) { + // Concatenate text nodes + nextSibling.data = oldNode.data.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + } else { + // \uFEFF = wysihtml5.INVISIBLE_SPACE (used as a hack in certain rich text editing situations) + var data = oldNode.data.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + return oldNode.ownerDocument.createTextNode(data); + } + } + + function _handleComment(oldNode) { + if (currentRules.comments) { + return oldNode.ownerDocument.createComment(oldNode.nodeValue); + } + } + + // ------------ attribute checks ------------ \\ + var attributeCheckMethods = { + url: (function() { + var REG_EXP = /^https?:\/\//i; + return function(attributeValue) { + if (!attributeValue || !attributeValue.match(REG_EXP)) { + return null; + } + return attributeValue.replace(REG_EXP, function(match) { + return match.toLowerCase(); + }); + }; + })(), + + src: (function() { + var REG_EXP = /^(\/|https?:\/\/)/i; + return function(attributeValue) { + if (!attributeValue || !attributeValue.match(REG_EXP)) { + return null; + } + return attributeValue.replace(REG_EXP, function(match) { + return match.toLowerCase(); + }); + }; + })(), + + href: (function() { + var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i; + return function(attributeValue) { + if (!attributeValue || !attributeValue.match(REG_EXP)) { + return null; + } + return attributeValue.replace(REG_EXP, function(match) { + return match.toLowerCase(); + }); + }; + })(), + + alt: (function() { + var REG_EXP = /[^ a-z0-9_\-]/gi; + return function(attributeValue, nodeName) { + if (!attributeValue) { + if (nodeName === "IMG") { + return ""; + } else { + return null; + } + } + return attributeValue.replace(REG_EXP, ""); + }; + })(), + + // Integers. Does not work with floating point numbers and units + numbers: (function() { + var REG_EXP = /\D/g; + return function(attributeValue) { + attributeValue = (attributeValue || "").replace(REG_EXP, ""); + return attributeValue || null; + }; + })(), + + // Useful for with/height attributes where floating points and percentages are allowed + dimension: (function() { + var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/; + return function(attributeValue) { + attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3"); + return attributeValue || null; + }; + })(), + + any: (function() { + return function(attributeValue) { + if (!attributeValue) { + return null; + } + return attributeValue; + }; + })() + }; + + // ------------ style converter (converts an html attribute to a style) ------------ \\ + var addStyleMethods = { + align_text: (function() { + var mapping = { + left: "text-align: left;", + right: "text-align: right;", + center: "text-align: center;" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + }; + + // ------------ class converter (converts an html attribute to a class name) ------------ \\ + var addClassMethods = { + align_img: (function() { + var mapping = { + left: "wysiwyg-float-left", + right: "wysiwyg-float-right" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + + align_text: (function() { + var mapping = { + left: "wysiwyg-text-align-left", + right: "wysiwyg-text-align-right", + center: "wysiwyg-text-align-center", + justify: "wysiwyg-text-align-justify" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + + clear_br: (function() { + var mapping = { + left: "wysiwyg-clear-left", + right: "wysiwyg-clear-right", + both: "wysiwyg-clear-both", + all: "wysiwyg-clear-both" + }; + return function(attributeValue) { + return mapping[String(attributeValue).toLowerCase()]; + }; + })(), + + size_font: (function() { + var mapping = { + "1": "wysiwyg-font-size-xx-small", + "2": "wysiwyg-font-size-small", + "3": "wysiwyg-font-size-medium", + "4": "wysiwyg-font-size-large", + "5": "wysiwyg-font-size-x-large", + "6": "wysiwyg-font-size-xx-large", + "7": "wysiwyg-font-size-xx-large", + "-": "wysiwyg-font-size-smaller", + "+": "wysiwyg-font-size-larger" + }; + return function(attributeValue) { + return mapping[String(attributeValue).charAt(0)]; + }; + })() + }; + + // checks if element is possibly visible + var typeCeckMethods = { + has_visible_contet: (function() { + var txt, + isVisible = false, + visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript', + 'style', 'table', 'iframe', 'object', 'embed', 'audio', + 'svg', 'input', 'button', 'select','textarea', 'canvas']; + + return function(el) { + + // has visible innertext. so is visible + txt = (el.innerText || el.textContent).replace(/\s/g, ''); + if (txt && txt.length > 0) { + return true; + } + + // matches list of visible dimensioned elements + for (var i = visibleElements.length; i--;) { + if (el.querySelector(visibleElements[i])) { + return true; + } + } + + // try to measure dimesions in last resort. (can find only of elements in dom) + if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) { + return true; + } + + return false; + }; + })() + }; + + var elementHandlingMethods = { + unwrap: function (element) { + wysihtml5.dom.unwrap(element); + }, + + remove: function (element) { + element.parentNode.removeChild(element); + } + }; + + return parse(elementOrHtml_current, config_current); +}; +;/** + * Checks for empty text node childs and removes them + * + * @param {Element} node The element in which to cleanup + * @example + * wysihtml5.dom.removeEmptyTextNodes(element); + */ +wysihtml5.dom.removeEmptyTextNodes = function(node) { + var childNode, + childNodes = wysihtml5.lang.array(node.childNodes).get(), + childNodesLength = childNodes.length, + i = 0; + + for (; i to a

) and keeps its childs + * + * @param {Element} element The list element which should be renamed + * @param {Element} newNodeName The desired tag name + * + * @example + * + *

    + *
  • eminem
  • + *
  • dr. dre
  • + *
  • 50 Cent
  • + *
+ * + * + * + * + *
    + *
  1. eminem
  2. + *
  3. dr. dre
  4. + *
  5. 50 Cent
  6. + *
+ */ +wysihtml5.dom.renameElement = function(element, newNodeName) { + var newElement = element.ownerDocument.createElement(newNodeName), + firstChild; + while (firstChild = element.firstChild) { + newElement.appendChild(firstChild); + } + wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement); + + if (element.parentNode) { + element.parentNode.replaceChild(newElement, element); + } + + return newElement; +}; +;/** + * Takes an element, removes it and replaces it with it's childs + * + * @param {Object} node The node which to replace with it's child nodes + * @example + *
+ * hello + *
+ * + */ +wysihtml5.dom.replaceWithChildNodes = function(node) { + if (!node.parentNode) { + return; + } + + while (node.firstChild) { + node.parentNode.insertBefore(node.firstChild, node); + } + node.parentNode.removeChild(node); +}; +;/** + * Unwraps an unordered/ordered list + * + * @param {Element} element The list element which should be unwrapped + * + * @example + * + *
    + *
  • eminem
  • + *
  • dr. dre
  • + *
  • 50 Cent
  • + *
+ * + * + * + * + * eminem
+ * dr. dre
+ * 50 Cent
+ */ +(function(dom) { + function _isBlockElement(node) { + return dom.getStyle("display").from(node) === "block"; + } + + function _isLineBreak(node) { + return node.nodeName === "BR"; + } + + function _appendLineBreak(element) { + var lineBreak = element.ownerDocument.createElement("br"); + element.appendChild(lineBreak); + } + + function resolveList(list, useLineBreaks) { + if (!list.nodeName.match(/^(MENU|UL|OL)$/)) { + return; + } + + var doc = list.ownerDocument, + fragment = doc.createDocumentFragment(), + previousSibling = wysihtml5.dom.domNode(list).prev({ignoreBlankTexts: true}), + nextSibling = wysihtml5.dom.domNode(list).next({ignoreBlankTexts: true}), + firstChild, + lastChild, + isLastChild, + shouldAppendLineBreak, + paragraph, + listItem, + lastListItem = list.lastElementChild || list.lastChild, + isLastItem; + + if (useLineBreaks) { + // Insert line break if list is after a non-block element + if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) { + _appendLineBreak(fragment); + } + + while (listItem = (list.firstElementChild || list.firstChild)) { + lastChild = listItem.lastChild; + isLastItem = listItem === lastListItem; + while (firstChild = listItem.firstChild) { + isLastChild = firstChild === lastChild; + // This needs to be done before appending it to the fragment, as it otherwise will lose style information + shouldAppendLineBreak = (!isLastItem || (nextSibling && !_isBlockElement(nextSibling))) && isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); + fragment.appendChild(firstChild); + if (shouldAppendLineBreak) { + _appendLineBreak(fragment); + } + } + + listItem.parentNode.removeChild(listItem); + } + } else { + while (listItem = (list.firstElementChild || list.firstChild)) { + if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) { + while (firstChild = listItem.firstChild) { + fragment.appendChild(firstChild); + } + } else { + paragraph = doc.createElement("p"); + while (firstChild = listItem.firstChild) { + paragraph.appendChild(firstChild); + } + fragment.appendChild(paragraph); + } + listItem.parentNode.removeChild(listItem); + } + } + + list.parentNode.replaceChild(fragment, list); + } + + dom.resolveList = resolveList; +})(wysihtml5.dom); +;/** + * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way + * + * Browser Compatibility: + * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" + * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) + * + * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons: + * - sandboxing doesn't work correctly with inlined content (src="javascript:'...'") + * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...) + * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire + * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe + * can do anything as if the sandbox attribute wasn't set + * + * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready + * @param {Object} [config] Optional parameters + * + * @example + * new wysihtml5.dom.Sandbox(function(sandbox) { + * sandbox.getWindow().document.body.innerHTML = ''; + * }); + */ +(function(wysihtml5) { + var /** + * Default configuration + */ + doc = document, + /** + * Properties to unset/protect on the window object + */ + windowProperties = [ + "parent", "top", "opener", "frameElement", "frames", + "localStorage", "globalStorage", "sessionStorage", "indexedDB" + ], + /** + * Properties on the window object which are set to an empty function + */ + windowProperties2 = [ + "open", "close", "openDialog", "showModalDialog", + "alert", "confirm", "prompt", + "openDatabase", "postMessage", + "XMLHttpRequest", "XDomainRequest" + ], + /** + * Properties to unset/protect on the document object + */ + documentProperties = [ + "referrer", + "write", "open", "close" + ]; + + wysihtml5.dom.Sandbox = Base.extend( + /** @scope wysihtml5.dom.Sandbox.prototype */ { + + constructor: function(readyCallback, config) { + this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; + this.config = wysihtml5.lang.object({}).merge(config).get(); + if (!this.config.className) { + this.config.className = "wysihtml5-sandbox"; + } + this.editableArea = this._createIframe(); + }, + + insertInto: function(element) { + if (typeof(element) === "string") { + element = doc.getElementById(element); + } + + element.appendChild(this.editableArea); + }, + + getIframe: function() { + return this.editableArea; + }, + + getWindow: function() { + this._readyError(); + }, + + getDocument: function() { + this._readyError(); + }, + + destroy: function() { + var iframe = this.getIframe(); + iframe.parentNode.removeChild(iframe); + }, + + _readyError: function() { + throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet"); + }, + + /** + * Creates the sandbox iframe + * + * Some important notes: + * - We can't use HTML5 sandbox for now: + * setting it causes that the iframe's dom can't be accessed from the outside + * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom + * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. + * In order to make this happen we need to set the "allow-scripts" flag. + * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. + * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) + * - IE needs to have the security="restricted" attribute set before the iframe is + * inserted into the dom tree + * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even + * though it supports it + * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore + * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely + * on the onreadystatechange event + */ + _createIframe: function() { + var that = this, + iframe = doc.createElement("iframe"); + iframe.className = this.config.className; + wysihtml5.dom.setAttributes({ + "security": "restricted", + "allowtransparency": "true", + "frameborder": 0, + "width": 0, + "height": 0, + "marginwidth": 0, + "marginheight": 0 + }).on(iframe); + + // Setting the src like this prevents ssl warnings in IE6 + if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { + iframe.src = "javascript:''"; + } + + iframe.onload = function() { + iframe.onreadystatechange = iframe.onload = null; + that._onLoadIframe(iframe); + }; + + iframe.onreadystatechange = function() { + if (/loaded|complete/.test(iframe.readyState)) { + iframe.onreadystatechange = iframe.onload = null; + that._onLoadIframe(iframe); + } + }; + + return iframe; + }, + + /** + * Callback for when the iframe has finished loading + */ + _onLoadIframe: function(iframe) { + // don't resume when the iframe got unloaded (eg. by removing it from the dom) + if (!wysihtml5.dom.contains(doc.documentElement, iframe)) { + return; + } + + var that = this, + iframeWindow = iframe.contentWindow, + iframeDocument = iframe.contentWindow.document, + charset = doc.characterSet || doc.charset || "utf-8", + sandboxHtml = this._getHtml({ + charset: charset, + stylesheets: this.config.stylesheets + }); + + // Create the basic dom tree including proper DOCTYPE and charset + iframeDocument.open("text/html", "replace"); + iframeDocument.write(sandboxHtml); + iframeDocument.close(); + + this.getWindow = function() { return iframe.contentWindow; }; + this.getDocument = function() { return iframe.contentWindow.document; }; + + // Catch js errors and pass them to the parent's onerror event + // addEventListener("error") doesn't work properly in some browsers + // TODO: apparently this doesn't work in IE9! + iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { + throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber); + }; + + if (!wysihtml5.browser.supportsSandboxedIframes()) { + // Unset a bunch of sensitive variables + // Please note: This isn't hack safe! + // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information + // IE is secure though, which is the most important thing, since IE is the only browser, who + // takes over scripts & styles into contentEditable elements when copied from external websites + // or applications (Microsoft Word, ...) + var i, length; + for (i=0, length=windowProperties.length; i'; + } + } + templateVars.stylesheets = html; + + return wysihtml5.lang.string( + '' + + '#{stylesheets}' + + '' + ).interpolate(templateVars); + }, + + /** + * Method to unset/override existing variables + * @example + * // Make cookie unreadable and unwritable + * this._unset(document, "cookie", "", true); + */ + _unset: function(object, property, value, setter) { + try { object[property] = value; } catch(e) {} + + try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} + if (setter) { + try { object.__defineSetter__(property, function() {}); } catch(e) {} + } + + if (!wysihtml5.browser.crashesWhenDefineProperty(property)) { + try { + var config = { + get: function() { return value; } + }; + if (setter) { + config.set = function() {}; + } + Object.defineProperty(object, property, config); + } catch(e) {} + } + } + }); +})(wysihtml5); +;(function(wysihtml5) { + var doc = document; + wysihtml5.dom.ContentEditableArea = Base.extend({ + getContentEditable: function() { + return this.element; + }, + + getWindow: function() { + return this.element.ownerDocument.defaultView || this.element.ownerDocument.parentWindow; + }, + + getDocument: function() { + return this.element.ownerDocument; + }, + + constructor: function(readyCallback, config, contentEditable) { + this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; + this.config = wysihtml5.lang.object({}).merge(config).get(); + if (!this.config.className) { + this.config.className = "wysihtml5-sandbox"; + } + if (contentEditable) { + this.element = this._bindElement(contentEditable); + } else { + this.element = this._createElement(); + } + }, + + destroy: function() { + + }, + + // creates a new contenteditable and initiates it + _createElement: function() { + var element = doc.createElement("div"); + element.className = this.config.className; + this._loadElement(element); + return element; + }, + + // initiates an allready existent contenteditable + _bindElement: function(contentEditable) { + contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml5-sandbox" : "wysihtml5-sandbox"; + this._loadElement(contentEditable, true); + return contentEditable; + }, + + _loadElement: function(element, contentExists) { + var that = this; + + if (!contentExists) { + var innerHtml = this._getHtml(); + element.innerHTML = innerHtml; + } + + this.loaded = true; + // Trigger the callback + setTimeout(function() { that.callback(that); }, 0); + }, + + _getHtml: function(templateVars) { + return ''; + } + + }); +})(wysihtml5); +;(function() { + var mapping = { + "className": "class" + }; + wysihtml5.dom.setAttributes = function(attributes) { + return { + on: function(element) { + for (var i in attributes) { + element.setAttribute(mapping[i] || i, attributes[i]); + } + } + }; + }; +})(); +;wysihtml5.dom.setStyles = function(styles) { + return { + on: function(element) { + var style = element.style; + if (typeof(styles) === "string") { + style.cssText += ";" + styles; + return; + } + for (var i in styles) { + if (i === "float") { + style.cssFloat = styles[i]; + style.styleFloat = styles[i]; + } else { + style[i] = styles[i]; + } + } + } + }; +}; +;/** + * Simulate HTML5 placeholder attribute + * + * Needed since + * - div[contentEditable] elements don't support it + * - older browsers (such as IE8 and Firefox 3.6) don't support it at all + * + * @param {Object} parent Instance of main wysihtml5.Editor class + * @param {Element} view Instance of wysihtml5.views.* class + * @param {String} placeholderText + * + * @example + * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); + */ +(function(dom) { + dom.simulatePlaceholder = function(editor, view, placeholderText, placeholderClassName) { + var CLASS_NAME = placeholderClassName || "wysihtml5-placeholder", + unset = function() { + var composerIsVisible = view.element.offsetWidth > 0 && view.element.offsetHeight > 0; + if (view.hasPlaceholderSet()) { + view.clear(); + view.element.focus(); + if (composerIsVisible ) { + setTimeout(function() { + var sel = view.selection.getSelection(); + if (!sel.focusNode || !sel.anchorNode) { + view.selection.selectNode(view.element.firstChild || view.element); + } + }, 0); + } + } + view.placeholderSet = false; + dom.removeClass(view.element, CLASS_NAME); + }, + set = function() { + if (view.isEmpty() && !view.placeholderSet) { + view.placeholderSet = true; + view.setValue(placeholderText, false); + dom.addClass(view.element, CLASS_NAME); + } + }; + + editor + .on("set_placeholder", set) + .on("unset_placeholder", unset) + .on("focus:composer", unset) + .on("paste:composer", unset) + .on("blur:composer", set); + + set(); + }; +})(wysihtml5.dom); +;(function(dom) { + var documentElement = document.documentElement; + if ("textContent" in documentElement) { + dom.setTextContent = function(element, text) { + element.textContent = text; + }; + + dom.getTextContent = function(element) { + return element.textContent; + }; + } else if ("innerText" in documentElement) { + dom.setTextContent = function(element, text) { + element.innerText = text; + }; + + dom.getTextContent = function(element) { + return element.innerText; + }; + } else { + dom.setTextContent = function(element, text) { + element.nodeValue = text; + }; + + dom.getTextContent = function(element) { + return element.nodeValue; + }; + } +})(wysihtml5.dom); +;/** + * Get a set of attribute from one element + * + * IE gives wrong results for hasAttribute/getAttribute, for example: + * var td = document.createElement("td"); + * td.getAttribute("rowspan"); // => "1" in IE + * + * Therefore we have to check the element's outerHTML for the attribute +*/ + +wysihtml5.dom.getAttribute = function(node, attributeName) { + var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); + attributeName = attributeName.toLowerCase(); + var nodeName = node.nodeName; + if (nodeName == "IMG" && attributeName == "src" && wysihtml5.dom.isLoadedImage(node) === true) { + // Get 'src' attribute value via object property since this will always contain the + // full absolute url (http://...) + // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host + // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) + return node.src; + } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { + // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML + var outerHTML = node.outerHTML.toLowerCase(), + // TODO: This might not work for attributes without value: + hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; + + return hasAttribute ? node.getAttribute(attributeName) : null; + } else{ + return node.getAttribute(attributeName); + } +}; +;/** + * Get all attributes of an element + * + * IE gives wrong results for hasAttribute/getAttribute, for example: + * var td = document.createElement("td"); + * td.getAttribute("rowspan"); // => "1" in IE + * + * Therefore we have to check the element's outerHTML for the attribute +*/ + +wysihtml5.dom.getAttributes = function(node) { + var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(), + nodeName = node.nodeName, + attributes = [], + attr; + + for (attr in node.attributes) { + if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr))) { + if (node.attributes[attr].specified) { + if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml5.dom.isLoadedImage(node) === true) { + attributes['src'] = node.src; + } else if (wysihtml5.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) { + if (node.attributes[attr].value !== 1) { + attributes[node.attributes[attr].name] = node.attributes[attr].value; + } + } else { + attributes[node.attributes[attr].name] = node.attributes[attr].value; + } + } + } + } + return attributes; +}; +;/** + * Check whether the given node is a proper loaded image + * FIXME: Returns undefined when unknown (Chrome, Safari) +*/ + +wysihtml5.dom.isLoadedImage = function (node) { + try { + return node.complete && !node.mozMatchesSelector(":-moz-broken"); + } catch(e) { + if (node.complete && node.readyState === "complete") { + return true; + } + } +}; +;(function(wysihtml5) { + + var api = wysihtml5.dom; + + var MapCell = function(cell) { + this.el = cell; + this.isColspan= false; + this.isRowspan= false; + this.firstCol= true; + this.lastCol= true; + this.firstRow= true; + this.lastRow= true; + this.isReal= true; + this.spanCollection= []; + this.modified = false; + }; + + var TableModifyerByCell = function (cell, table) { + if (cell) { + this.cell = cell; + this.table = api.getParentElement(cell, { query: "table" }); + } else if (table) { + this.table = table; + this.cell = this.table.querySelectorAll('th, td')[0]; + } + }; + + function queryInList(list, query) { + var ret = [], + q; + for (var e = 0, len = list.length; e < len; e++) { + q = list[e].querySelectorAll(query); + if (q) { + for(var i = q.length; i--; ret.unshift(q[i])); + } + } + return ret; + } + + function removeElement(el) { + el.parentNode.removeChild(el); + } + + function insertAfter(referenceNode, newNode) { + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); + } + + function nextNode(node, tag) { + var element = node.nextSibling; + while (element.nodeType !=1) { + element = element.nextSibling; + if (!tag || tag == element.tagName.toLowerCase()) { + return element; + } + } + return null; + } + + TableModifyerByCell.prototype = { + + addSpannedCellToMap: function(cell, map, r, c, cspan, rspan) { + var spanCollect = [], + rmax = r + ((rspan) ? parseInt(rspan, 10) - 1 : 0), + cmax = c + ((cspan) ? parseInt(cspan, 10) - 1 : 0); + + for (var rr = r; rr <= rmax; rr++) { + if (typeof map[rr] == "undefined") { map[rr] = []; } + for (var cc = c; cc <= cmax; cc++) { + map[rr][cc] = new MapCell(cell); + map[rr][cc].isColspan = (cspan && parseInt(cspan, 10) > 1); + map[rr][cc].isRowspan = (rspan && parseInt(rspan, 10) > 1); + map[rr][cc].firstCol = cc == c; + map[rr][cc].lastCol = cc == cmax; + map[rr][cc].firstRow = rr == r; + map[rr][cc].lastRow = rr == rmax; + map[rr][cc].isReal = cc == c && rr == r; + map[rr][cc].spanCollection = spanCollect; + + spanCollect.push(map[rr][cc]); + } + } + }, + + setCellAsModified: function(cell) { + cell.modified = true; + if (cell.spanCollection.length > 0) { + for (var s = 0, smax = cell.spanCollection.length; s < smax; s++) { + cell.spanCollection[s].modified = true; + } + } + }, + + setTableMap: function() { + var map = []; + var tableRows = this.getTableRows(), + ridx, row, cells, cidx, cell, + c, + cspan, rspan; + + for (ridx = 0; ridx < tableRows.length; ridx++) { + row = tableRows[ridx]; + cells = this.getRowCells(row); + c = 0; + if (typeof map[ridx] == "undefined") { map[ridx] = []; } + for (cidx = 0; cidx < cells.length; cidx++) { + cell = cells[cidx]; + + // If cell allready set means it is set by col or rowspan, + // so increase cols index until free col is found + while (typeof map[ridx][c] != "undefined") { c++; } + + cspan = api.getAttribute(cell, 'colspan'); + rspan = api.getAttribute(cell, 'rowspan'); + + if (cspan || rspan) { + this.addSpannedCellToMap(cell, map, ridx, c, cspan, rspan); + c = c + ((cspan) ? parseInt(cspan, 10) : 1); + } else { + map[ridx][c] = new MapCell(cell); + c++; + } + } + } + this.map = map; + return map; + }, + + getRowCells: function(row) { + var inlineTables = this.table.querySelectorAll('table'), + inlineCells = (inlineTables) ? queryInList(inlineTables, 'th, td') : [], + allCells = row.querySelectorAll('th, td'), + tableCells = (inlineCells.length > 0) ? wysihtml5.lang.array(allCells).without(inlineCells) : allCells; + + return tableCells; + }, + + getTableRows: function() { + var inlineTables = this.table.querySelectorAll('table'), + inlineRows = (inlineTables) ? queryInList(inlineTables, 'tr') : [], + allRows = this.table.querySelectorAll('tr'), + tableRows = (inlineRows.length > 0) ? wysihtml5.lang.array(allRows).without(inlineRows) : allRows; + + return tableRows; + }, + + getMapIndex: function(cell) { + var r_length = this.map.length, + c_length = (this.map && this.map[0]) ? this.map[0].length : 0; + + for (var r_idx = 0;r_idx < r_length; r_idx++) { + for (var c_idx = 0;c_idx < c_length; c_idx++) { + if (this.map[r_idx][c_idx].el === cell) { + return {'row': r_idx, 'col': c_idx}; + } + } + } + return false; + }, + + getElementAtIndex: function(idx) { + this.setTableMap(); + if (this.map[idx.row] && this.map[idx.row][idx.col] && this.map[idx.row][idx.col].el) { + return this.map[idx.row][idx.col].el; + } + return null; + }, + + getMapElsTo: function(to_cell) { + var els = []; + this.setTableMap(); + this.idx_start = this.getMapIndex(this.cell); + this.idx_end = this.getMapIndex(to_cell); + + // switch indexes if start is bigger than end + if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { + var temp_idx = this.idx_start; + this.idx_start = this.idx_end; + this.idx_end = temp_idx; + } + if (this.idx_start.col > this.idx_end.col) { + var temp_cidx = this.idx_start.col; + this.idx_start.col = this.idx_end.col; + this.idx_end.col = temp_cidx; + } + + if (this.idx_start != null && this.idx_end != null) { + for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) { + for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) { + els.push(this.map[row][col].el); + } + } + } + return els; + }, + + orderSelectionEnds: function(secondcell) { + this.setTableMap(); + this.idx_start = this.getMapIndex(this.cell); + this.idx_end = this.getMapIndex(secondcell); + + // switch indexes if start is bigger than end + if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { + var temp_idx = this.idx_start; + this.idx_start = this.idx_end; + this.idx_end = temp_idx; + } + if (this.idx_start.col > this.idx_end.col) { + var temp_cidx = this.idx_start.col; + this.idx_start.col = this.idx_end.col; + this.idx_end.col = temp_cidx; + } + + return { + "start": this.map[this.idx_start.row][this.idx_start.col].el, + "end": this.map[this.idx_end.row][this.idx_end.col].el + }; + }, + + createCells: function(tag, nr, attrs) { + var doc = this.table.ownerDocument, + frag = doc.createDocumentFragment(), + cell; + for (var i = 0; i < nr; i++) { + cell = doc.createElement(tag); + + if (attrs) { + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + cell.setAttribute(attr, attrs[attr]); + } + } + } + + // add non breaking space + cell.appendChild(document.createTextNode("\u00a0")); + frag.appendChild(cell); + } + return frag; + }, + + // Returns next real cell (not part of spanned cell unless first) on row if selected index is not real. I no real cells -1 will be returned + correctColIndexForUnreals: function(col, row) { + var r = this.map[row], + corrIdx = -1; + for (var i = 0, max = col; i < col; i++) { + if (r[i].isReal){ + corrIdx++; + } + } + return corrIdx; + }, + + getLastNewCellOnRow: function(row, rowLimit) { + var cells = this.getRowCells(row), + cell, idx; + + for (var cidx = 0, cmax = cells.length; cidx < cmax; cidx++) { + cell = cells[cidx]; + idx = this.getMapIndex(cell); + if (idx === false || (typeof rowLimit != "undefined" && idx.row != rowLimit)) { + return cell; + } + } + return null; + }, + + removeEmptyTable: function() { + var cells = this.table.querySelectorAll('td, th'); + if (!cells || cells.length == 0) { + removeElement(this.table); + return true; + } else { + return false; + } + }, + + // Splits merged cell on row to unique cells + splitRowToCells: function(cell) { + if (cell.isColspan) { + var colspan = parseInt(api.getAttribute(cell.el, 'colspan') || 1, 10), + cType = cell.el.tagName.toLowerCase(); + if (colspan > 1) { + var newCells = this.createCells(cType, colspan -1); + insertAfter(cell.el, newCells); + } + cell.el.removeAttribute('colspan'); + } + }, + + getRealRowEl: function(force, idx) { + var r = null, + c = null; + + idx = idx || this.idx; + + for (var cidx = 0, cmax = this.map[idx.row].length; cidx < cmax; cidx++) { + c = this.map[idx.row][cidx]; + if (c.isReal) { + r = api.getParentElement(c.el, { query: "tr" }); + if (r) { + return r; + } + } + } + + if (r === null && force) { + r = api.getParentElement(this.map[idx.row][idx.col].el, { query: "tr" }) || null; + } + + return r; + }, + + injectRowAt: function(row, col, colspan, cType, c) { + var r = this.getRealRowEl(false, {'row': row, 'col': col}), + new_cells = this.createCells(cType, colspan); + + if (r) { + var n_cidx = this.correctColIndexForUnreals(col, row); + if (n_cidx >= 0) { + insertAfter(this.getRowCells(r)[n_cidx], new_cells); + } else { + r.insertBefore(new_cells, r.firstChild); + } + } else { + var rr = this.table.ownerDocument.createElement('tr'); + rr.appendChild(new_cells); + insertAfter(api.getParentElement(c.el, { query: "tr" }), rr); + } + }, + + canMerge: function(to) { + this.to = to; + this.setTableMap(); + this.idx_start = this.getMapIndex(this.cell); + this.idx_end = this.getMapIndex(this.to); + + // switch indexes if start is bigger than end + if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { + var temp_idx = this.idx_start; + this.idx_start = this.idx_end; + this.idx_end = temp_idx; + } + if (this.idx_start.col > this.idx_end.col) { + var temp_cidx = this.idx_start.col; + this.idx_start.col = this.idx_end.col; + this.idx_end.col = temp_cidx; + } + + for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) { + for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) { + if (this.map[row][col].isColspan || this.map[row][col].isRowspan) { + return false; + } + } + } + return true; + }, + + decreaseCellSpan: function(cell, span) { + var nr = parseInt(api.getAttribute(cell.el, span), 10) - 1; + if (nr >= 1) { + cell.el.setAttribute(span, nr); + } else { + cell.el.removeAttribute(span); + if (span == 'colspan') { + cell.isColspan = false; + } + if (span == 'rowspan') { + cell.isRowspan = false; + } + cell.firstCol = true; + cell.lastCol = true; + cell.firstRow = true; + cell.lastRow = true; + cell.isReal = true; + } + }, + + removeSurplusLines: function() { + var row, cell, ridx, rmax, cidx, cmax, allRowspan; + + this.setTableMap(); + if (this.map) { + ridx = 0; + rmax = this.map.length; + for (;ridx < rmax; ridx++) { + row = this.map[ridx]; + allRowspan = true; + cidx = 0; + cmax = row.length; + for (; cidx < cmax; cidx++) { + cell = row[cidx]; + if (!(api.getAttribute(cell.el, "rowspan") && parseInt(api.getAttribute(cell.el, "rowspan"), 10) > 1 && cell.firstRow !== true)) { + allRowspan = false; + break; + } + } + if (allRowspan) { + cidx = 0; + for (; cidx < cmax; cidx++) { + this.decreaseCellSpan(row[cidx], 'rowspan'); + } + } + } + + // remove rows without cells + var tableRows = this.getTableRows(); + ridx = 0; + rmax = tableRows.length; + for (;ridx < rmax; ridx++) { + row = tableRows[ridx]; + if (row.childNodes.length == 0 && (/^\s*$/.test(row.textContent || row.innerText))) { + removeElement(row); + } + } + } + }, + + fillMissingCells: function() { + var r_max = 0, + c_max = 0, + prevcell = null; + + this.setTableMap(); + if (this.map) { + + // find maximal dimensions of broken table + r_max = this.map.length; + for (var ridx = 0; ridx < r_max; ridx++) { + if (this.map[ridx].length > c_max) { c_max = this.map[ridx].length; } + } + + for (var row = 0; row < r_max; row++) { + for (var col = 0; col < c_max; col++) { + if (this.map[row] && !this.map[row][col]) { + if (col > 0) { + this.map[row][col] = new MapCell(this.createCells('td', 1)); + prevcell = this.map[row][col-1]; + if (prevcell && prevcell.el && prevcell.el.parent) { // if parent does not exist element is removed from dom + insertAfter(this.map[row][col-1].el, this.map[row][col].el); + } + } + } + } + } + } + }, + + rectify: function() { + if (!this.removeEmptyTable()) { + this.removeSurplusLines(); + this.fillMissingCells(); + return true; + } else { + return false; + } + }, + + unmerge: function() { + if (this.rectify()) { + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + + if (this.idx) { + var thisCell = this.map[this.idx.row][this.idx.col], + colspan = (api.getAttribute(thisCell.el, "colspan")) ? parseInt(api.getAttribute(thisCell.el, "colspan"), 10) : 1, + cType = thisCell.el.tagName.toLowerCase(); + + if (thisCell.isRowspan) { + var rowspan = parseInt(api.getAttribute(thisCell.el, "rowspan"), 10); + if (rowspan > 1) { + for (var nr = 1, maxr = rowspan - 1; nr <= maxr; nr++){ + this.injectRowAt(this.idx.row + nr, this.idx.col, colspan, cType, thisCell); + } + } + thisCell.el.removeAttribute('rowspan'); + } + this.splitRowToCells(thisCell); + } + } + }, + + // merges cells from start cell (defined in creating obj) to "to" cell + merge: function(to) { + if (this.rectify()) { + if (this.canMerge(to)) { + var rowspan = this.idx_end.row - this.idx_start.row + 1, + colspan = this.idx_end.col - this.idx_start.col + 1; + + for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) { + for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) { + + if (row == this.idx_start.row && col == this.idx_start.col) { + if (rowspan > 1) { + this.map[row][col].el.setAttribute('rowspan', rowspan); + } + if (colspan > 1) { + this.map[row][col].el.setAttribute('colspan', colspan); + } + } else { + // transfer content + if (!(/^\s*\s*$/.test(this.map[row][col].el.innerHTML.toLowerCase()))) { + this.map[this.idx_start.row][this.idx_start.col].el.innerHTML += ' ' + this.map[row][col].el.innerHTML; + } + removeElement(this.map[row][col].el); + } + + } + } + this.rectify(); + } else { + if (window.console) { + console.log('Do not know how to merge allready merged cells.'); + } + } + } + }, + + // Decreases rowspan of a cell if it is done on first cell of rowspan row (real cell) + // Cell is moved to next row (if it is real) + collapseCellToNextRow: function(cell) { + var cellIdx = this.getMapIndex(cell.el), + newRowIdx = cellIdx.row + 1, + newIdx = {'row': newRowIdx, 'col': cellIdx.col}; + + if (newRowIdx < this.map.length) { + + var row = this.getRealRowEl(false, newIdx); + if (row !== null) { + var n_cidx = this.correctColIndexForUnreals(newIdx.col, newIdx.row); + if (n_cidx >= 0) { + insertAfter(this.getRowCells(row)[n_cidx], cell.el); + } else { + var lastCell = this.getLastNewCellOnRow(row, newRowIdx); + if (lastCell !== null) { + insertAfter(lastCell, cell.el); + } else { + row.insertBefore(cell.el, row.firstChild); + } + } + if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) { + cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1); + } else { + cell.el.removeAttribute('rowspan'); + } + } + } + }, + + // Removes a cell when removing a row + // If is rowspan cell then decreases the rowspan + // and moves cell to next row if needed (is first cell of rowspan) + removeRowCell: function(cell) { + if (cell.isReal) { + if (cell.isRowspan) { + this.collapseCellToNextRow(cell); + } else { + removeElement(cell.el); + } + } else { + if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) { + cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1); + } else { + cell.el.removeAttribute('rowspan'); + } + } + }, + + getRowElementsByCell: function() { + var cells = []; + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (this.idx !== false) { + var modRow = this.map[this.idx.row]; + for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) { + if (modRow[cidx].isReal) { + cells.push(modRow[cidx].el); + } + } + } + return cells; + }, + + getColumnElementsByCell: function() { + var cells = []; + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (this.idx !== false) { + for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) { + if (this.map[ridx][this.idx.col] && this.map[ridx][this.idx.col].isReal) { + cells.push(this.map[ridx][this.idx.col].el); + } + } + } + return cells; + }, + + // Removes the row of selected cell + removeRow: function() { + var oldRow = api.getParentElement(this.cell, { query: "tr" }); + if (oldRow) { + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (this.idx !== false) { + var modRow = this.map[this.idx.row]; + for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) { + if (!modRow[cidx].modified) { + this.setCellAsModified(modRow[cidx]); + this.removeRowCell(modRow[cidx]); + } + } + } + removeElement(oldRow); + } + }, + + removeColCell: function(cell) { + if (cell.isColspan) { + if (parseInt(api.getAttribute(cell.el, 'colspan'), 10) > 2) { + cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) - 1); + } else { + cell.el.removeAttribute('colspan'); + } + } else if (cell.isReal) { + removeElement(cell.el); + } + }, + + removeColumn: function() { + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (this.idx !== false) { + for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) { + if (!this.map[ridx][this.idx.col].modified) { + this.setCellAsModified(this.map[ridx][this.idx.col]); + this.removeColCell(this.map[ridx][this.idx.col]); + } + } + } + }, + + // removes row or column by selected cell element + remove: function(what) { + if (this.rectify()) { + switch (what) { + case 'row': + this.removeRow(); + break; + case 'column': + this.removeColumn(); + break; + } + this.rectify(); + } + }, + + addRow: function(where) { + var doc = this.table.ownerDocument; + + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (where == "below" && api.getAttribute(this.cell, 'rowspan')) { + this.idx.row = this.idx.row + parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1; + } + + if (this.idx !== false) { + var modRow = this.map[this.idx.row], + newRow = doc.createElement('tr'); + + for (var ridx = 0, rmax = modRow.length; ridx < rmax; ridx++) { + if (!modRow[ridx].modified) { + this.setCellAsModified(modRow[ridx]); + this.addRowCell(modRow[ridx], newRow, where); + } + } + + switch (where) { + case 'below': + insertAfter(this.getRealRowEl(true), newRow); + break; + case 'above': + var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { query: "tr" }); + if (cr) { + cr.parentNode.insertBefore(newRow, cr); + } + break; + } + } + }, + + addRowCell: function(cell, row, where) { + var colSpanAttr = (cell.isColspan) ? {"colspan" : api.getAttribute(cell.el, 'colspan')} : null; + if (cell.isReal) { + if (where != 'above' && cell.isRowspan) { + cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el,'rowspan'), 10) + 1); + } else { + row.appendChild(this.createCells('td', 1, colSpanAttr)); + } + } else { + if (where != 'above' && cell.isRowspan && cell.lastRow) { + row.appendChild(this.createCells('td', 1, colSpanAttr)); + } else if (c.isRowspan) { + cell.el.attr('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) + 1); + } + } + }, + + add: function(where) { + if (this.rectify()) { + if (where == 'below' || where == 'above') { + this.addRow(where); + } + if (where == 'before' || where == 'after') { + this.addColumn(where); + } + } + }, + + addColCell: function (cell, ridx, where) { + var doAdd, + cType = cell.el.tagName.toLowerCase(); + + // defines add cell vs expand cell conditions + // true means add + switch (where) { + case "before": + doAdd = (!cell.isColspan || cell.firstCol); + break; + case "after": + doAdd = (!cell.isColspan || cell.lastCol || (cell.isColspan && c.el == this.cell)); + break; + } + + if (doAdd){ + // adds a cell before or after current cell element + switch (where) { + case "before": + cell.el.parentNode.insertBefore(this.createCells(cType, 1), cell.el); + break; + case "after": + insertAfter(cell.el, this.createCells(cType, 1)); + break; + } + + // handles if cell has rowspan + if (cell.isRowspan) { + this.handleCellAddWithRowspan(cell, ridx+1, where); + } + + } else { + // expands cell + cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) + 1); + } + }, + + addColumn: function(where) { + var row, modCell; + + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (where == "after" && api.getAttribute(this.cell, 'colspan')) { + this.idx.col = this.idx.col + parseInt(api.getAttribute(this.cell, 'colspan'), 10) - 1; + } + + if (this.idx !== false) { + for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++ ) { + row = this.map[ridx]; + if (row[this.idx.col]) { + modCell = row[this.idx.col]; + if (!modCell.modified) { + this.setCellAsModified(modCell); + this.addColCell(modCell, ridx , where); + } + } + } + } + }, + + handleCellAddWithRowspan: function (cell, ridx, where) { + var addRowsNr = parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1, + crow = api.getParentElement(cell.el, { query: "tr" }), + cType = cell.el.tagName.toLowerCase(), + cidx, temp_r_cells, + doc = this.table.ownerDocument, + nrow; + + for (var i = 0; i < addRowsNr; i++) { + cidx = this.correctColIndexForUnreals(this.idx.col, (ridx + i)); + crow = nextNode(crow, 'tr'); + if (crow) { + if (cidx > 0) { + switch (where) { + case "before": + temp_r_cells = this.getRowCells(crow); + if (cidx > 0 && this.map[ridx + i][this.idx.col].el != temp_r_cells[cidx] && cidx == temp_r_cells.length - 1) { + insertAfter(temp_r_cells[cidx], this.createCells(cType, 1)); + } else { + temp_r_cells[cidx].parentNode.insertBefore(this.createCells(cType, 1), temp_r_cells[cidx]); + } + + break; + case "after": + insertAfter(this.getRowCells(crow)[cidx], this.createCells(cType, 1)); + break; + } + } else { + crow.insertBefore(this.createCells(cType, 1), crow.firstChild); + } + } else { + nrow = doc.createElement('tr'); + nrow.appendChild(this.createCells(cType, 1)); + this.table.appendChild(nrow); + } + } + } + }; + + api.table = { + getCellsBetween: function(cell1, cell2) { + var c1 = new TableModifyerByCell(cell1); + return c1.getMapElsTo(cell2); + }, + + addCells: function(cell, where) { + var c = new TableModifyerByCell(cell); + c.add(where); + }, + + removeCells: function(cell, what) { + var c = new TableModifyerByCell(cell); + c.remove(what); + }, + + mergeCellsBetween: function(cell1, cell2) { + var c1 = new TableModifyerByCell(cell1); + c1.merge(cell2); + }, + + unmergeCell: function(cell) { + var c = new TableModifyerByCell(cell); + c.unmerge(); + }, + + orderSelectionEnds: function(cell, cell2) { + var c = new TableModifyerByCell(cell); + return c.orderSelectionEnds(cell2); + }, + + indexOf: function(cell) { + var c = new TableModifyerByCell(cell); + c.setTableMap(); + return c.getMapIndex(cell); + }, + + findCell: function(table, idx) { + var c = new TableModifyerByCell(null, table); + return c.getElementAtIndex(idx); + }, + + findRowByCell: function(cell) { + var c = new TableModifyerByCell(cell); + return c.getRowElementsByCell(); + }, + + findColumnByCell: function(cell) { + var c = new TableModifyerByCell(cell); + return c.getColumnElementsByCell(); + }, + + canMerge: function(cell1, cell2) { + var c = new TableModifyerByCell(cell1); + return c.canMerge(cell2); + } + }; + +})(wysihtml5); +;// does a selector query on element or array of elements +wysihtml5.dom.query = function(elements, query) { + var ret = [], + q; + + if (elements.nodeType) { + elements = [elements]; + } + + for (var e = 0, len = elements.length; e < len; e++) { + q = elements[e].querySelectorAll(query); + if (q) { + for(var i = q.length; i--; ret.unshift(q[i])); + } + } + return ret; +}; +;wysihtml5.dom.compareDocumentPosition = (function() { + var documentElement = document.documentElement; + if (documentElement.compareDocumentPosition) { + return function(container, element) { + return container.compareDocumentPosition(element); + }; + } else { + return function( container, element ) { + // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license + var thisOwner, otherOwner; + + if( container.nodeType === 9) // Node.DOCUMENT_NODE + thisOwner = container; + else + thisOwner = container.ownerDocument; + + if( element.nodeType === 9) // Node.DOCUMENT_NODE + otherOwner = element; + else + otherOwner = element.ownerDocument; + + if( container === element ) return 0; + if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY; + if( container.ownerDocument === element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS; + if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED; + + // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child. + if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml5.lang.array(container.childNodes).indexOf( element ) !== -1) + return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY; + + if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml5.lang.array(element.childNodes).indexOf( container ) !== -1) + return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS; + + var point = container; + var parents = [ ]; + var previous = null; + while( point ) { + if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS; + parents.push( point ); + point = point.parentNode; + } + point = element; + previous = null; + while( point ) { + if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY; + var location_index = wysihtml5.lang.array(parents).indexOf( point ); + if( location_index !== -1) { + var smallest_common_ancestor = parents[ location_index ]; + var this_index = wysihtml5.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] ); + var other_index = wysihtml5.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous ); + if( this_index > other_index ) { + return 2; //Node.DOCUMENT_POSITION_PRECEDING; + } + else { + return 4; //Node.DOCUMENT_POSITION_FOLLOWING; + } + } + previous = point; + point = point.parentNode; + } + return 1; //Node.DOCUMENT_POSITION_DISCONNECTED; + }; + } +})(); +;/* Unwraps element and returns list of childNodes that the node contained. + * + * Example: + * var childnodes = wysihtml5.dom.unwrap(document.querySelector('.unwrap-me')); +*/ + +wysihtml5.dom.unwrap = function(node) { + var children = []; + if (node.parentNode) { + while (node.lastChild) { + children.unshift(node.lastChild); + wysihtml5.dom.insert(node.lastChild).after(node); + } + node.parentNode.removeChild(node); + } + return children; +}; +;/* + * Methods for fetching pasted html before it gets inserted into content +**/ + +/* Modern event.clipboardData driven approach. + * Advantage is that it does not have to loose selection or modify dom to catch the data. + * IE does not support though. +**/ +wysihtml5.dom.getPastedHtml = function(event) { + var html; + if (wysihtml5.browser.supportsModernPaste() && event.clipboardData) { + if (wysihtml5.lang.array(event.clipboardData.types).contains('text/html')) { + html = event.clipboardData.getData('text/html'); + } else if (wysihtml5.lang.array(event.clipboardData.types).contains('text/plain')) { + html = wysihtml5.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true); + } + } + return html; +}; + +/* Older temprorary contenteditable as paste source catcher method for fallbacks */ +wysihtml5.dom.getPastedHtmlWithDiv = function (composer, f) { + var selBookmark = composer.selection.getBookmark(), + doc = composer.element.ownerDocument, + cleanerDiv = doc.createElement('DIV'), + scrollPos = composer.getScrollPos(); + + doc.body.appendChild(cleanerDiv); + + cleanerDiv.style.width = "1px"; + cleanerDiv.style.height = "1px"; + cleanerDiv.style.overflow = "hidden"; + cleanerDiv.style.position = "absolute"; + cleanerDiv.style.top = scrollPos.y + "px"; + cleanerDiv.style.left = scrollPos.x + "px"; + + cleanerDiv.setAttribute('contenteditable', 'true'); + cleanerDiv.focus(); + + setTimeout(function () { + var html; + + composer.selection.setBookmark(selBookmark); + html = cleanerDiv.innerHTML; + if (html && (/^$/i).test(html.trim())) { + html = false; + } + f(html); + cleanerDiv.parentNode.removeChild(cleanerDiv); + }, 0); +}; +;wysihtml5.dom.removeInvisibleSpaces = function(node) { + var textNodes = wysihtml5.dom.getTextNodes(node); + for (var n = textNodes.length; n--;) { + textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + } +}; +;/** + * Fix most common html formatting misbehaviors of browsers implementation when inserting + * content via copy & paste contentEditable + * + * @author Christopher Blum + */ +wysihtml5.quirks.cleanPastedHTML = (function() { + + var styleToRegex = function (styleStr) { + var trimmedStr = wysihtml5.lang.string(styleStr).trim(), + escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + + return new RegExp("^((?!^" + escapedStr + "$).)*$", "i"); + }; + + var extendRulesWithStyleExceptions = function (rules, exceptStyles) { + var newRules = wysihtml5.lang.object(rules).clone(true), + tag, style; + + for (tag in newRules.tags) { + + if (newRules.tags.hasOwnProperty(tag)) { + if (newRules.tags[tag].keep_styles) { + for (style in newRules.tags[tag].keep_styles) { + if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) { + if (exceptStyles[style]) { + newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]); + } + } + } + } + } + } + + return newRules; + }; + + var pickRuleset = function(ruleset, html) { + var pickedSet, defaultSet; + + if (!ruleset) { + return null; + } + + for (var i = 0, max = ruleset.length; i < max; i++) { + if (!ruleset[i].condition) { + defaultSet = ruleset[i].set; + } + if (ruleset[i].condition && ruleset[i].condition.test(html)) { + return ruleset[i].set; + } + } + + return defaultSet; + }; + + return function(html, options) { + var exceptStyles = { + 'color': wysihtml5.dom.getStyle("color").from(options.referenceNode), + 'fontSize': wysihtml5.dom.getStyle("font-size").from(options.referenceNode) + }, + rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles), + newHtml; + + newHtml = wysihtml5.dom.parse(html, { + "rules": rules, + "cleanUp": true, // elements, empty or without attributes, should be removed/replaced with their content + "context": options.referenceNode.ownerDocument, + "uneditableClass": options.uneditableClass, + "clearInternals" : true, // don't paste temprorary selection and other markings + "unjoinNbsps" : true + }); + + return newHtml; + }; + +})(); +;/** + * IE and Opera leave an empty paragraph in the contentEditable element after clearing it + * + * @param {Object} contentEditableElement The contentEditable element to observe for clearing events + * @exaple + * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); + */ +wysihtml5.quirks.ensureProperClearing = (function() { + var clearIfNecessary = function() { + var element = this; + setTimeout(function() { + var innerHTML = element.innerHTML.toLowerCase(); + if (innerHTML == "

 

" || + innerHTML == "

 

 

") { + element.innerHTML = ""; + } + }, 0); + }; + + return function(composer) { + wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); + }; +})(); +;// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 +// +// In Firefox this: +// var d = document.createElement("div"); +// d.innerHTML =''; +// d.innerHTML; +// will result in: +// +// which is wrong +(function(wysihtml5) { + var TILDE_ESCAPED = "%7E"; + wysihtml5.quirks.getCorrectInnerHTML = function(element) { + var innerHTML = element.innerHTML; + if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { + return innerHTML; + } + + var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), + url, + urlToSearch, + length, + i; + for (i=0, length=elementsWithTilde.length; i 0) { + for (var i = 0; i < selectedCells.length; i++) { + dom.removeClass(selectedCells[i], selection_class); + } + } + } + } + + function addSelections (cells) { + for (var i = 0; i < cells.length; i++) { + dom.addClass(cells[i], selection_class); + } + } + + function handleMouseMove (event) { + var curTable = null, + cell = dom.getParentElement(event.target, { query: "td, th" }, false, editable), + oldEnd; + + if (cell && select.table && select.start) { + curTable = dom.getParentElement(cell, { query: "table" }, false, editable); + if (curTable && curTable === select.table) { + removeCellSelections(); + oldEnd = select.end; + select.end = cell; + select.cells = dom.table.getCellsBetween(select.start, cell); + if (select.cells.length > 1) { + editor.composer.selection.deselect(); + } + addSelections(select.cells); + if (select.end !== oldEnd) { + editor.fire("tableselectchange").fire("tableselectchange:composer"); + } + } + } + } + + function handleMouseUp (event) { + editable.removeEventListener("mousemove", handleMouseMove); + editable.removeEventListener("mouseup", handleMouseUp); + editor.fire("tableselect").fire("tableselect:composer"); + setTimeout(function() { + bindSideclick(); + },0); + } + + var sideClickHandler = function(event) { + editable.ownerDocument.removeEventListener("click", sideClickHandler); + if (dom.getParentElement(event.target, { query: "table" }, false, editable) != select.table) { + removeCellSelections(); + select.table = null; + select.start = null; + select.end = null; + editor.fire("tableunselect").fire("tableunselect:composer"); + } + }; + + function bindSideclick () { + editable.ownerDocument.addEventListener("click", sideClickHandler); + } + + function selectCells (start, end) { + select.start = start; + select.end = end; + select.table = dom.getParentElement(select.start, { query: "table" }, false, editable); + selectedCells = dom.table.getCellsBetween(select.start, select.end); + addSelections(selectedCells); + bindSideclick(); + editor.fire("tableselect").fire("tableselect:composer"); + } + + return init(); + +}; +;(function(wysihtml5) { + + // List of supported color format parsing methods + // If radix is not defined 10 is expected as default + var colorParseMethods = { + rgba : { + regex: /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i, + name: "rgba" + }, + rgb : { + regex: /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i, + name: "rgb" + }, + hex6 : { + regex: /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i, + name: "hex", + radix: 16 + }, + hex3 : { + regex: /^#([0-9a-f])([0-9a-f])([0-9a-f])/i, + name: "hex", + radix: 16 + } + }, + // Takes a style key name as an argument and makes a regex that can be used to the match key:value pair from style string + makeParamRegExp = function (p) { + return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+", "gi"); + }; + + // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns suitable parsing method for it + function getColorParseMethod (colorStr) { + var prop, colorTypeConf; + + for (prop in colorParseMethods) { + if (!colorParseMethods.hasOwnProperty(prop)) { continue; } + + colorTypeConf = colorParseMethods[prop]; + + if (colorTypeConf.regex.test(colorStr)) { + return colorTypeConf; + } + } + } + + // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns the type of that color format "hex", "rgb", "rgba". + function getColorFormat (colorStr) { + var type = getColorParseMethod(colorStr); + + return type ? type.name : undefined; + } + + // Public API functions for styleParser + wysihtml5.quirks.styleParser = { + + // Takes color string value as an argument and returns suitable parsing method for it + getColorParseMethod : getColorParseMethod, + + // Takes color string value as an argument and returns the type of that color format "hex", "rgb", "rgba". + getColorFormat : getColorFormat, + + /* Parses a color string to and array of [red, green, blue, alpha]. + * paramName: optional argument to parse color value directly from style string parameter + * + * Examples: + * var colorArray = wysihtml5.quirks.styleParser.parseColor("#ABC"); // [170, 187, 204, 1] + * var colorArray = wysihtml5.quirks.styleParser.parseColor("#AABBCC"); // [170, 187, 204, 1] + * var colorArray = wysihtml5.quirks.styleParser.parseColor("rgb(1,2,3)"); // [1, 2, 3, 1] + * var colorArray = wysihtml5.quirks.styleParser.parseColor("rgba(1,2,3,0.5)"); // [1, 2, 3, 0.5] + * + * var colorArray = wysihtml5.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1] + * var colorArray = wysihtml5.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color"); // [0, 0, 0, 1] + */ + parseColor : function (stylesStr, paramName) { + var paramsRegex, params, colorType, colorMatch, radix, + colorStr = stylesStr; + + if (paramName) { + paramsRegex = makeParamRegExp(paramName); + + if (!(params = stylesStr.match(paramsRegex))) { return false; } + + params = params.pop().split(":")[1]; + colorStr = wysihtml5.lang.string(params).trim(); + } + + if (!(colorType = getColorParseMethod(colorStr))) { return false; } + if (!(colorMatch = colorStr.match(colorType.regex))) { return false; } + + radix = colorType.radix || 10; + + if (colorType === colorParseMethods.hex3) { + colorMatch.shift(); + colorMatch.push(1); + return wysihtml5.lang.array(colorMatch).map(function(d, idx) { + return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d); + }); + } + + colorMatch.shift(); + + if (!colorMatch[3]) { + colorMatch.push(1); + } + + return wysihtml5.lang.array(colorMatch).map(function(d, idx) { + return (idx < 3) ? parseInt(d, radix): parseFloat(d); + }); + }, + + /* Takes rgba color array [r,g,b,a] as a value and formats it to color string with given format type + * If no format is given, rgba/rgb is returned based on alpha value + * + * Example: + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash"); // "#AABBCC" + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex"); // "AABBCC" + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv"); // "170, 187, 204, 1" + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba"); // "rgba(170,187,204,1)" + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgb"); // "rgb(170,187,204)" + * + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 0.5]); // "rgba(170,187,204,0.5)" + * var colorStr = wysihtml5.quirks.styleParser.unparseColor([170, 187, 204, 1]); // "rgb(170,187,204)" + */ + unparseColor: function(val, colorFormat) { + var hexRadix = 16; + + if (colorFormat === "hex") { + return (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase(); + } else if (colorFormat === "hash") { + return "#" + (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase(); + } else if (colorFormat === "rgb") { + return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")"; + } else if (colorFormat === "rgba") { + return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")"; + } else if (colorFormat === "csv") { + return val[0] + "," + val[1] + "," + val[2] + "," + val[3]; + } + + if (val[3] && val[3] !== 1) { + return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")"; + } else { + return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")"; + } + }, + + // Parses font size value from style string + parseFontSize: function(stylesStr) { + var params = stylesStr.match(makeParamRegExp("font-size")); + if (params) { + return wysihtml5.lang.string(params[params.length - 1].split(":")[1]).trim(); + } + return false; + } + }; + +})(wysihtml5); +;/** + * Selection API + * + * @example + * var selection = new wysihtml5.Selection(editor); + */ +(function(wysihtml5) { + var dom = wysihtml5.dom; + + function _getCumulativeOffsetTop(element) { + var top = 0; + if (element.parentNode) { + do { + top += element.offsetTop || 0; + element = element.offsetParent; + } while (element); + } + return top; + } + + // Provides the depth of ``descendant`` relative to ``ancestor`` + function getDepth(ancestor, descendant) { + var ret = 0; + while (descendant !== ancestor) { + ret++; + descendant = descendant.parentNode; + if (!descendant) + throw new Error("not a descendant of ancestor!"); + } + return ret; + } + + function getRangeNode(node, offset) { + if (node.nodeType === 3) { + return node; + } else { + return node.childNodes[offset] || node; + } + } + + function getWebkitSelectionFixNode(container) { + var blankNode = document.createElement('span'); + + var placeholderRemover = function(event) { + // Self-destructs the caret and keeps the text inserted into it by user + var lastChild; + + container.removeEventListener('mouseup', placeholderRemover); + container.removeEventListener('keydown', placeholderRemover); + container.removeEventListener('touchstart', placeholderRemover); + container.removeEventListener('focus', placeholderRemover); + container.removeEventListener('blur', placeholderRemover); + container.removeEventListener('paste', delayedPlaceholderRemover); + container.removeEventListener('drop', delayedPlaceholderRemover); + container.removeEventListener('beforepaste', delayedPlaceholderRemover); + + if (blankNode && blankNode.parentNode) { + blankNode.parentNode.removeChild(blankNode); + } + }, + delayedPlaceholderRemover = function (event) { + if (blankNode && blankNode.parentNode) { + setTimeout(placeholderRemover, 0); + } + }; + + blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml5.INVISIBLE_SPACE)); + blankNode.className = '_wysihtml5-temp-caret-fix'; + blankNode.style.display = 'block'; + blankNode.style.minWidth = '1px'; + blankNode.style.height = '0px'; + + container.addEventListener('mouseup', placeholderRemover); + container.addEventListener('keydown', placeholderRemover); + container.addEventListener('touchstart', placeholderRemover); + container.addEventListener('focus', placeholderRemover); + container.addEventListener('blur', placeholderRemover); + container.addEventListener('paste', delayedPlaceholderRemover); + container.addEventListener('drop', delayedPlaceholderRemover); + container.addEventListener('beforepaste', delayedPlaceholderRemover); + + return blankNode; + } + + // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon + // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection + function expandRangeToSurround(range) { + if (range.canSurroundContents()) return; + + var common = range.commonAncestorContainer, + start_depth = getDepth(common, range.startContainer), + end_depth = getDepth(common, range.endContainer); + + while(!range.canSurroundContents()) { + // In the following branches, we cannot just decrement the depth variables because the setStartBefore/setEndAfter may move the start or end of the range more than one level relative to ``common``. So we need to recompute the depth. + if (start_depth > end_depth) { + range.setStartBefore(range.startContainer); + start_depth = getDepth(common, range.startContainer); + } + else { + range.setEndAfter(range.endContainer); + end_depth = getDepth(common, range.endContainer); + } + } + } + + wysihtml5.Selection = Base.extend( + /** @scope wysihtml5.Selection.prototype */ { + constructor: function(editor, contain, unselectableClass) { + // Make sure that our external range library is initialized + rangy.init(); + + this.editor = editor; + this.composer = editor.composer; + this.doc = this.composer.doc; + this.win = this.composer.win; + this.contain = contain; + this.unselectableClass = unselectableClass || false; + }, + + /** + * Get the current selection as a bookmark to be able to later restore it + * + * @return {Object} An object that represents the current selection + */ + getBookmark: function() { + var range = this.getRange(); + return range && range.cloneRange(); + }, + + /** + * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark + * + * @param {Object} bookmark An object that represents the current selection + */ + setBookmark: function(bookmark) { + if (!bookmark) { + return; + } + + this.setSelection(bookmark); + }, + + /** + * Set the caret in front of the given node + * + * @param {Object} node The element or text node where to position the caret in front of + * @example + * selection.setBefore(myElement); + */ + setBefore: function(node) { + var range = rangy.createRange(this.doc); + range.setStartBefore(node); + range.setEndBefore(node); + return this.setSelection(range); + }, + + // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail. + // Webkit has an issue with placing caret into places where there are no textnodes near by. + createTemporaryCaretSpaceAfter: function (node) { + var caretPlaceholder = this.doc.createElement('span'), + caretPlaceholderText = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE), + placeholderRemover = (function(event) { + // Self-destructs the caret and keeps the text inserted into it by user + var lastChild; + + this.contain.removeEventListener('mouseup', placeholderRemover); + this.contain.removeEventListener('keydown', keyDownHandler); + this.contain.removeEventListener('touchstart', placeholderRemover); + this.contain.removeEventListener('focus', placeholderRemover); + this.contain.removeEventListener('blur', placeholderRemover); + this.contain.removeEventListener('paste', delayedPlaceholderRemover); + this.contain.removeEventListener('drop', delayedPlaceholderRemover); + this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover); + + // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack + // Otherwise the wrapper can just be removed + if (caretPlaceholder && caretPlaceholder.parentNode) { + caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) { + lastChild = caretPlaceholder.lastChild; + wysihtml5.dom.unwrap(caretPlaceholder); + this.setAfter(lastChild); + } else { + caretPlaceholder.parentNode.removeChild(caretPlaceholder); + } + + } + }).bind(this), + delayedPlaceholderRemover = function (event) { + if (caretPlaceholder && caretPlaceholder.parentNode) { + setTimeout(placeholderRemover, 0); + } + }, + keyDownHandler = function(event) { + if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) { + placeholderRemover(); + } + }; + + caretPlaceholder.className = '_wysihtml5-temp-caret-fix'; + caretPlaceholder.style.position = 'absolute'; + caretPlaceholder.style.display = 'block'; + caretPlaceholder.style.minWidth = '1px'; + caretPlaceholder.style.zIndex = '99999'; + caretPlaceholder.appendChild(caretPlaceholderText); + + node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); + this.setBefore(caretPlaceholderText); + + // Remove the caret fix on any of the following events (some are delayed as content change happens after event) + this.contain.addEventListener('mouseup', placeholderRemover); + this.contain.addEventListener('keydown', keyDownHandler); + this.contain.addEventListener('touchstart', placeholderRemover); + this.contain.addEventListener('focus', placeholderRemover); + this.contain.addEventListener('blur', placeholderRemover); + this.contain.addEventListener('paste', delayedPlaceholderRemover); + this.contain.addEventListener('drop', delayedPlaceholderRemover); + this.contain.addEventListener('beforepaste', delayedPlaceholderRemover); + + return caretPlaceholder; + }, + + /** + * Set the caret after the given node + * + * @param {Object} node The element or text node where to position the caret in front of + * @example + * selection.setBefore(myElement); + * callback is an optional parameter accepting a function to execute when selection ahs been set + */ + setAfter: function(node, notVisual, callback) { + var win = this.win, + range = rangy.createRange(this.doc), + fixWebkitSelection = function() { + // Webkit fails to add selection if there are no textnodes in that region + // (like an uneditable container at the end of content). + var parent = node.parentNode, + lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null; + + if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) { + if (notVisual) { + // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation + // and remove itself in call stack end instead on user interaction + var caretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); + this.selectNode(caretPlaceholder); + setTimeout(function() { + if (caretPlaceholder && caretPlaceholder.parentNode) { + caretPlaceholder.parentNode.removeChild(caretPlaceholder); + } + }, 0); + } else { + this.createTemporaryCaretSpaceAfter(node); + } + } + }.bind(this), + sel; + + range.setStartAfter(node); + range.setEndAfter(node); + + // In IE contenteditable must be focused before we can set selection + // thus setting the focus if activeElement is not this composer + if (!document.activeElement || document.activeElement !== this.composer.element) { + var scrollPos = this.composer.getScrollPos(); + this.composer.element.focus(); + this.composer.setScrollPos(scrollPos); + setTimeout(function() { + sel = this.setSelection(range); + fixWebkitSelection(); + if (callback) { + callback(sel); + } + }.bind(this), 0); + } else { + sel = this.setSelection(range); + fixWebkitSelection(); + if (callback) { + callback(sel); + } + } + }, + + /** + * Ability to select/mark nodes + * + * @param {Element} node The node/element to select + * @example + * selection.selectNode(document.getElementById("my-image")); + */ + selectNode: function(node, avoidInvisibleSpace) { + var range = rangy.createRange(this.doc), + isElement = node.nodeType === wysihtml5.ELEMENT_NODE, + canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), + content = isElement ? node.innerHTML : node.data, + isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), + displayStyle = dom.getStyle("display").from(node), + isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); + + if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) { + // Make sure that caret is visible in node by inserting a zero width no breaking space + try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} + } + if (canHaveHTML) { + range.selectNodeContents(node); + } else { + range.selectNode(node); + } + + if (canHaveHTML && isEmpty && isElement) { + range.collapse(isBlockElement); + } else if (canHaveHTML && isEmpty) { + range.setStartAfter(node); + range.setEndAfter(node); + } + + this.setSelection(range); + }, + + /** + * Get the node which contains the selection + * + * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" + * @return {Object} The node that contains the caret + * @example + * var nodeThatContainsCaret = selection.getSelectedNode(); + */ + getSelectedNode: function(controlRange) { + var selection, + range; + + if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { + range = this.doc.selection.createRange(); + if (range && range.length) { + return range.item(0); + } + } + + selection = this.getSelection(this.doc); + if (selection.focusNode === selection.anchorNode) { + return selection.focusNode; + } else { + range = this.getRange(this.doc); + return range ? range.commonAncestorContainer : this.doc.body; + } + }, + + fixSelBorders: function() { + var range = this.getRange(); + expandRangeToSurround(range); + this.setSelection(range); + }, + + getSelectedOwnNodes: function(controlRange) { + var selection, + ranges = this.getOwnRanges(), + ownNodes = []; + + for (var i = 0, maxi = ranges.length; i < maxi; i++) { + ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body); + } + return ownNodes; + }, + + findNodesInSelection: function(nodeTypes) { + var ranges = this.getOwnRanges(), + nodes = [], curNodes; + for (var i = 0, maxi = ranges.length; i < maxi; i++) { + curNodes = ranges[i].getNodes([1], function(node) { + return wysihtml5.lang.array(nodeTypes).contains(node.nodeName); + }); + nodes = nodes.concat(curNodes); + } + return nodes; + }, + + filterElements: function(filter) { + var ranges = this.getOwnRanges(), + nodes = [], curNodes; + + for (var i = 0, maxi = ranges.length; i < maxi; i++) { + curNodes = ranges[i].getNodes([1], function(element){ + return filter(element, ranges[i]); + }); + nodes = nodes.concat(curNodes); + } + return nodes; + }, + + containsUneditable: function() { + var uneditables = this.getOwnUneditables(), + selection = this.getSelection(); + + for (var i = 0, maxi = uneditables.length; i < maxi; i++) { + if (selection.containsNode(uneditables[i])) { + return true; + } + } + + return false; + }, + + // Deletes selection contents making sure uneditables/unselectables are not partially deleted + // Triggers wysihtml5:uneditable:delete custom event on all deleted uneditables if customevents suppoorted + deleteContents: function() { + var range = this.getRange(); + this.deleteRangeContents(range); + this.setSelection(range); + }, + + // Makes sure all uneditable sare notified before deleting contents + deleteRangeContents: function (range) { + var startParent, endParent, uneditables, ev; + + if (this.unselectableClass) { + if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) { + range.setStartBefore(startParent); + } + if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) { + range.setEndAfter(endParent); + } + + // If customevents present notify uneditable elements of being deleted + uneditables = range.getNodes([1], (function (node) { + return wysihtml5.dom.hasClass(node, this.unselectableClass); + }).bind(this)); + for (var i = uneditables.length; i--;) { + try { + ev = new CustomEvent("wysihtml5:uneditable:delete"); + uneditables[i].dispatchEvent(ev); + } catch (err) {} + } + } + range.deleteContents(); + }, + + getCaretNode: function () { + var selection = this.getSelection(); + return (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null; + }, + + getPreviousNode: function(node, ignoreEmpty) { + var displayStyle; + if (!node) { + var selection = this.getSelection(); + node = (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null; + } + + if (node === this.contain) { + return false; + } + + var ret = node.previousSibling, + parent; + + if (ret === this.contain) { + return false; + } + + if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) { + // do not count comments and other node types + ret = this.getPreviousNode(ret, ignoreEmpty); + } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) { + // do not count empty textnodes as previous nodes + ret = this.getPreviousNode(ret, ignoreEmpty); + } else if (ignoreEmpty && ret && ret.nodeType === 1) { + // Do not count empty nodes if param set. + // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like + displayStyle = wysihtml5.dom.getStyle("display").from(ret); + if ( + !wysihtml5.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) && + !wysihtml5.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) && + (/^[\s]*$/).test(ret.innerHTML) + ) { + ret = this.getPreviousNode(ret, ignoreEmpty); + } + } else if (!ret && node !== this.contain) { + parent = node.parentNode; + if (parent !== this.contain) { + ret = this.getPreviousNode(parent, ignoreEmpty); + } + } + + return (ret !== this.contain) ? ret : false; + }, + + // Gather info about caret location (caret node, previous and next node) + getNodesNearCaret: function() { + if (!this.isCollapsed()) { + throw "Selection must be caret when using selection.getNodesNearCaret()"; + } + + var r = this.getOwnRanges(), + caretNode, prevNode, nextNode, offset; + + if (r && r.length > 0) { + if (r[0].startContainer.nodeType === 1) { + caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1]; + if (!caretNode && r[0].startOffset === 0) { + // Is first position before all nodes + nextNode = r[0].startContainer.childNodes[0]; + } else if (caretNode) { + prevNode = caretNode.previousSibling; + nextNode = caretNode.nextSibling; + } + } else { + if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) { + caretNode = r[0].startContainer.previousSibling; + if (caretNode.nodeType === 3) { + offset = caretNode.data.length; + } + } else { + caretNode = r[0].startContainer; + offset = r[0].startOffset; + } + prevNode = caretNode.previousSibling; + nextNode = caretNode.nextSibling; + } + + return { + "caretNode": caretNode, + "prevNode": prevNode, + "nextNode": nextNode, + "textOffset": offset + }; + } + + return null; + }, + + getSelectionParentsByTag: function(tagName) { + var nodes = this.getSelectedOwnNodes(), + curEl, parents = []; + + for (var i = 0, maxi = nodes.length; i < maxi; i++) { + curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain); + if (curEl) { + parents.push(curEl); + } + } + return (parents.length) ? parents : null; + }, + + getRangeToNodeEnd: function() { + if (this.isCollapsed()) { + var range = this.getRange(), + sNode = range.startContainer, + pos = range.startOffset, + lastR = rangy.createRange(this.doc); + + lastR.selectNodeContents(sNode); + lastR.setStart(sNode, pos); + return lastR; + } + }, + + caretIsLastInSelection: function() { + var r = rangy.createRange(this.doc), + s = this.getSelection(), + endc = this.getRangeToNodeEnd().cloneContents(), + endtxt = endc.textContent; + + return (/^\s*$/).test(endtxt); + }, + + caretIsFirstInSelection: function(includeLineBreaks) { + var r = rangy.createRange(this.doc), + s = this.getSelection(), + range = this.getRange(), + startNode = getRangeNode(range.startContainer, range.startOffset); + + if (startNode) { + if (startNode.nodeType === wysihtml5.TEXT_NODE) { + if (!startNode.parentNode) { + return false; + } + if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml5.dom.domNode(startNode.previousSibling).is.block())) { + return false; + } + var ws = this.win.getComputedStyle(startNode.parentNode).whiteSpace; + return (ws === "pre" || ws === "pre-wrap") ? range.startOffset === 0 : (/^\s*$/).test(startNode.data.substr(0,range.startOffset)); + } else if (includeLineBreaks && wysihtml5.dom.domNode(startNode).is.lineBreak()) { + return true; + } else { + r.selectNodeContents(this.getRange().commonAncestorContainer); + r.collapse(true); + return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset); + } + } + }, + + caretIsInTheBeginnig: function(ofNode) { + var selection = this.getSelection(), + node = selection.anchorNode, + offset = selection.anchorOffset; + if (ofNode && node) { + return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { query: ofNode }, 1))); + } else if (node) { + return (offset === 0 && !this.getPreviousNode(node, true)); + } + }, + + // Returns object describing node/text before selection + // If includePrevLeaves is true returns also previous last leaf child if selection is in the beginning of current node + getBeforeSelection: function(includePrevLeaves) { + var sel = this.getSelection(), + startNode = (sel.isBackwards()) ? sel.focusNode : sel.anchorNode, + startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset, + rng = this.createRange(), endNode, inTmpCaret; + + // If start is textnode and all is whitespace before caret. Set start offset to 0 + if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) { + startOffset = 0; + } + + // Escape temproray helper nodes if selection in them + inTmpCaret = wysihtml5.dom.getParentElement(startNode, { query: '._wysihtml5-temp-caret-fix' }, 1); + if (inTmpCaret) { + startNode = inTmpCaret.parentNode; + startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret); + } + + if (startNode) { + if (startOffset > 0) { + if (startNode.nodeType === 3) { + rng.setStart(startNode, 0); + rng.setEnd(startNode, startOffset); + return { + type: "text", + range: rng, + offset : startOffset, + node: startNode + }; + } else { + rng.setStartBefore(startNode.childNodes[0]); + endNode = startNode.childNodes[startOffset - 1]; + rng.setEndAfter(endNode); + return { + type: "element", + range: rng, + offset : startOffset, + node: endNode + }; + } + } else { + rng.setStartAndEnd(startNode, 0); + + if (includePrevLeaves) { + var prevNode = this.getPreviousNode(startNode, true), + prevLeaf = null; + + if(prevNode) { + if (prevNode.nodeType === 1 && wysihtml5.dom.hasClass(prevNode, this.unselectableClass)) { + prevLeaf = prevNode; + } else { + prevLeaf = wysihtml5.dom.domNode(prevNode).lastLeafNode(); + } + } + + if (prevLeaf) { + return { + type: "leafnode", + range: rng, + offset : startOffset, + node: prevLeaf + }; + } + } + + return { + type: "none", + range: rng, + offset : startOffset, + node: startNode + }; + } + } + return null; + }, + + // TODO: Figure out a method from following 2 that would work universally + executeAndRestoreRangy: function(method, restoreScrollPosition) { + var sel = rangy.saveSelection(this.win); + if (!sel) { + method(); + } else { + try { + method(); + } catch(e) { + setTimeout(function() { throw e; }, 0); + } + } + rangy.restoreSelection(sel); + }, + + // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween + executeAndRestore: function(method, restoreScrollPosition) { + var body = this.doc.body, + oldScrollTop = restoreScrollPosition && body.scrollTop, + oldScrollLeft = restoreScrollPosition && body.scrollLeft, + className = "_wysihtml5-temp-placeholder", + placeholderHtml = '' + wysihtml5.INVISIBLE_SPACE + '', + range = this.getRange(true), + caretPlaceholder, + newCaretPlaceholder, + nextSibling, prevSibling, + node, node2, range2, + newRange; + + // Nothing selected, execute and say goodbye + if (!range) { + method(body, body); + return; + } + + if (!range.collapsed) { + range2 = range.cloneRange(); + node2 = range2.createContextualFragment(placeholderHtml); + range2.collapse(false); + range2.insertNode(node2); + range2.detach(); + } + + node = range.createContextualFragment(placeholderHtml); + range.insertNode(node); + + if (node2) { + caretPlaceholder = this.contain.querySelectorAll("." + className); + range.setStartBefore(caretPlaceholder[0]); + range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]); + } + this.setSelection(range); + + // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder + try { + method(range.startContainer, range.endContainer); + } catch(e) { + setTimeout(function() { throw e; }, 0); + } + caretPlaceholder = this.contain.querySelectorAll("." + className); + if (caretPlaceholder && caretPlaceholder.length) { + newRange = rangy.createRange(this.doc); + nextSibling = caretPlaceholder[0].nextSibling; + if (caretPlaceholder.length > 1) { + prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling; + } + if (prevSibling && nextSibling) { + newRange.setStartBefore(nextSibling); + newRange.setEndAfter(prevSibling); + } else { + newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]); + newRange.setStartBefore(newCaretPlaceholder); + newRange.setEndAfter(newCaretPlaceholder); + } + this.setSelection(newRange); + for (var i = caretPlaceholder.length; i--;) { + caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]); + } + + } else { + // fallback for when all hell breaks loose + this.contain.focus(); + } + + if (restoreScrollPosition) { + body.scrollTop = oldScrollTop; + body.scrollLeft = oldScrollLeft; + } + + // Remove it again, just to make sure that the placeholder is definitely out of the dom tree + try { + caretPlaceholder.parentNode.removeChild(caretPlaceholder); + } catch(e2) {} + }, + + set: function(node, offset) { + var newRange = rangy.createRange(this.doc); + newRange.setStart(node, offset || 0); + this.setSelection(newRange); + }, + + /** + * Insert html at the caret or selection position and move the cursor after the inserted html + * Replaces selection content if present + * + * @param {String} html HTML string to insert + * @example + * selection.insertHTML("

foobar

"); + */ + insertHTML: function(html) { + var range = this.getRange(), + node = this.doc.createElement('DIV'), + fragment = this.doc.createDocumentFragment(), + lastChild, lastEditorElement; + + if (range) { + range.deleteContents(); + node.innerHTML = html; + lastChild = node.lastChild; + + while (node.firstChild) { + fragment.appendChild(node.firstChild); + } + range.insertNode(fragment); + + lastEditorElement = this.contain.lastChild; + while (lastEditorElement && lastEditorElement.nodeType === 3 && lastEditorElement.previousSibling && (/^\s*$/).test(lastEditorElement.data)) { + lastEditorElement = lastEditorElement.previousSibling; + } + + if (lastChild) { + // fixes some pad cases mostly on webkit where last nr is needed + if (lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === 1) { + this.contain.appendChild(this.doc.createElement('br')); + } + this.setAfter(lastChild); + } + } + }, + + /** + * Insert a node at the caret position and move the cursor behind it + * + * @param {Object} node HTML string to insert + * @example + * selection.insertNode(document.createTextNode("foobar")); + */ + insertNode: function(node) { + var range = this.getRange(); + if (range) { + range.insertNode(node); + } + }, + + canAppendChild: function (node) { + var anchorNode, anchorNodeTagNameLower, + voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"], + range = this.getRange(); + + anchorNode = node || range.startContainer; + + if (anchorNode) { + anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase(); + } + + return voidElements.indexOf(anchorNodeTagNameLower) === -1; + }, + + splitElementAtCaret: function (element, insertNode) { + var sel = this.getSelection(), + range, contentAfterRangeStart, + firstChild, lastChild, childNodes; + + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with + + range.setEndAfter(element); // Place the end of the range after the element + contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment + + childNodes = contentAfterRangeStart.childNodes; + + // Empty elements are cleaned up from extracted content + for (var i = childNodes.length; i --;) { + if (!wysihtml5.dom.domNode(childNodes[i]).is.visible()) { + contentAfterRangeStart.removeChild(childNodes[i]); + } + } + + element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling); + + if (insertNode) { + firstChild = insertNode.firstChild || insertNode; + lastChild = insertNode.lastChild || insertNode; + + element.parentNode.insertBefore(insertNode, element.nextSibling); + + // Select inserted node contents + if (firstChild && lastChild) { + range.setStartBefore(firstChild); + range.setEndAfter(lastChild); + this.setSelection(range); + } + } else { + range.setStartAfter(element); + range.setEndAfter(element); + } + + if (!wysihtml5.dom.domNode(element).is.visible()) { + if (wysihtml5.dom.getTextContent(element) === '') { + element.parentNode.removeChild(element); + } else { + element.parentNode.replaceChild(this.doc.createTextNode(" "), element); + } + } + + + } + }, + + /** + * Wraps current selection with the given node + * + * @param {Object} node The node to surround the selected elements with + */ + surround: function(nodeOptions) { + var ranges = this.getOwnRanges(), + node, nodes = []; + if (ranges.length == 0) { + return nodes; + } + + for (var i = ranges.length; i--;) { + node = this.doc.createElement(nodeOptions.nodeName); + nodes.push(node); + if (nodeOptions.className) { + node.className = nodeOptions.className; + } + if (nodeOptions.cssStyle) { + node.setAttribute('style', nodeOptions.cssStyle); + } + try { + // This only works when the range boundaries are not overlapping other elements + ranges[i].surroundContents(node); + this.selectNode(node); + } catch(e) { + // fallback + node.appendChild(ranges[i].extractContents()); + ranges[i].insertNode(node); + } + } + return nodes; + }, + + /** + * Scroll the current caret position into the view + * FIXME: This is a bit hacky, there might be a smarter way of doing this + * + * @example + * selection.scrollIntoView(); + */ + scrollIntoView: function() { + var doc = this.doc, + tolerance = 5, // px + hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, + tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { + var element = doc.createElement("span"); + // The element needs content in order to be able to calculate it's position properly + element.innerHTML = wysihtml5.INVISIBLE_SPACE; + return element; + })(), + offsetTop; + + if (hasScrollBars) { + this.insertNode(tempElement); + offsetTop = _getCumulativeOffsetTop(tempElement); + tempElement.parentNode.removeChild(tempElement); + if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) { + doc.body.scrollTop = offsetTop; + } + } + }, + + /** + * Select line where the caret is in + */ + selectLine: function() { + var r = rangy.createRange(); + if (wysihtml5.browser.supportsSelectionModify()) { + this._selectLine_W3C(); + } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) { + // For IE Edge as it ditched the old api and did not fully implement the new one (as expected) + this._selectLineUniversal(); + } + }, + + includeRangyRangeHelpers: function() { + var s = this.getSelection(), + r = s.getRangeAt(0), + isHelperNode = function(node) { + return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary')); + }, + getNodeLength = function (node) { + if (node.nodeType === 1) { + return node.childNodes && node.childNodes.length || 0; + } else { + return node.data && node.data.length || 0; + } + }, + anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, + fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode; + + if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) { + r.setEndAfter(fnode.nextSibling); + } + if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) { + r.setStartBefore(anode.previousSibling); + } + r.select(); + }, + + /** + * See https://developer.mozilla.org/en/DOM/Selection/modify + */ + _selectLine_W3C: function() { + var selection = this.win.getSelection(), + initialBoundry = [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset]; + + selection.modify("move", "left", "lineboundary"); + selection.modify("extend", "right", "lineboundary"); + + // IF lineboundary extending did not change selection try universal fallback (FF fails sometimes without a reason) + if (selection.anchorNode === initialBoundry[0] && + selection.anchorOffset === initialBoundry[1] && + selection.focusNode === initialBoundry[2] && + selection.focusOffset === initialBoundry[3] + ) { + this._selectLineUniversal(); + } else { + this.includeRangyRangeHelpers(); + } + }, + + // collapses selection to current line beginning or end + toLineBoundary: function (location, collapse) { + collapse = (typeof collapse === 'undefined') ? false : collapse; + if (wysihtml5.browser.supportsSelectionModify()) { + var selection = this.win.getSelection(); + + selection.modify("extend", location, "lineboundary"); + if (collapse) { + if (location === "left") { + selection.collapseToStart(); + } else if (location === "right") { + selection.collapseToEnd(); + } + } + } + }, + + getRangeRect: function(r) { + var textNode = this.doc.createTextNode("i"), + testNode = this.doc.createTextNode("i"), + rect, cr; + + /*testNode.style.visibility = "hidden"; + testNode.style.width = "0px"; + testNode.style.display = "inline-block"; + testNode.style.overflow = "hidden"; + testNode.appendChild(textNode);*/ + + if (r.collapsed) { + r.insertNode(testNode); + r.selectNode(testNode); + rect = r.nativeRange.getBoundingClientRect(); + r.deleteContents(); + + } else { + rect = r.nativeRange.getBoundingClientRect(); + } + + return rect; + + }, + + _selectLineUniversal: function() { + var s = this.getSelection(), + r = s.getRangeAt(0), + rect, + startRange, endRange, testRange, + count = 0, + amount, testRect, found, + that = this, + isLineBreakingElement = function(el) { + return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml5.lang.array(['BR', 'HR']).contains(el.nodeName)); + }, + prevNode = function(node) { + var pnode = node; + if (pnode) { + while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) { + pnode = pnode.previousSibling; + } + } + return pnode; + }; + + startRange = r.cloneRange(); + endRange = r.cloneRange(); + + if (r.collapsed) { + // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary + // TODO: figure out a shorter and more readable way + if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) { + r.moveEnd('character', 1); + } 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) { + r.moveEnd('character', 1); + } else if ( + r.startOffset > 0 && + ( + r.startContainer.nodeType === 3 || + ( + r.startContainer.nodeType === 1 && + !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1])) + ) + ) + ) { + r.moveStart('character', -1); + } + } + if (!r.collapsed) { + r.insertNode(this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE)); + } + + // Is probably just empty line as can not be expanded + rect = r.nativeRange.getBoundingClientRect(); + // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes; + do { + amount = r.moveStart('character', -1); + testRect = r.nativeRange.getBoundingClientRect(); + + if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) { + r.moveStart('character', 1); + found = true; + } + count++; + } while (amount !== 0 && !found && count < 2000); + count = 0; + found = false; + rect = r.nativeRange.getBoundingClientRect(); + + if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) { + do { + amount = r.moveEnd('character', 1); + testRect = r.nativeRange.getBoundingClientRect(); + if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) { + r.moveEnd('character', -1); + + // Fix a IE line end marked by linebreak element although caret is before it + // If causes problems should be changed to be applied only to IE + 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) { + if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) { + r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length); + } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) { + r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length); + } + } + found = true; + } + count++; + } while (amount !== 0 && !found && count < 2000); + } + r.select(); + this.includeRangyRangeHelpers(); + }, + + getText: function() { + var selection = this.getSelection(); + return selection ? selection.toString() : ""; + }, + + getNodes: function(nodeType, filter) { + var range = this.getRange(); + if (range) { + return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter); + } else { + return []; + } + }, + + // Gets all the elements in selection with nodeType + // Ignores the elements not belonging to current editable area + // If filter is defined nodes must pass the filter function with true to be included in list + getOwnNodes: function(nodeType, filter, splitBounds) { + var ranges = this.getOwnRanges(), + nodes = []; + for (var r = 0, rmax = ranges.length; r < rmax; r++) { + if (ranges[r]) { + if (splitBounds) { + ranges[r].splitBoundaries(); + } + nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter)); + } + } + + return nodes; + }, + + fixRangeOverflow: function(range) { + if (this.contain && this.contain.firstChild && range) { + var containment = range.compareNode(this.contain); + if (containment !== 2) { + if (containment === 1) { + range.setStartBefore(this.contain.firstChild); + } + if (containment === 0) { + range.setEndAfter(this.contain.lastChild); + } + if (containment === 3) { + range.setStartBefore(this.contain.firstChild); + range.setEndAfter(this.contain.lastChild); + } + } else if (this._detectInlineRangeProblems(range)) { + var previousElementSibling = range.endContainer.previousElementSibling; + if (previousElementSibling) { + range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling)); + } + } + } + }, + + _endOffsetForNode: function(node) { + var range = document.createRange(); + range.selectNodeContents(node); + return range.endOffset; + }, + + _detectInlineRangeProblems: function(range) { + var position = dom.compareDocumentPosition(range.startContainer, range.endContainer); + return ( + range.endOffset == 0 && + position & 4 //Node.DOCUMENT_POSITION_FOLLOWING + ); + }, + + getRange: function(dontFix) { + var selection = this.getSelection(), + range = selection && selection.rangeCount && selection.getRangeAt(0); + + if (dontFix !== true) { + this.fixRangeOverflow(range); + } + + return range; + }, + + getOwnUneditables: function() { + var allUneditables = dom.query(this.contain, '.' + this.unselectableClass), + deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass); + + return wysihtml5.lang.array(allUneditables).without(deepUneditables); + }, + + // Returns an array of ranges that belong only to this editable + // Needed as uneditable block in contenteditabel can split range into pieces + // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges + getOwnRanges: function() { + var ranges = [], + r = this.getRange(), + tmpRanges; + + if (r) { ranges.push(r); } + + if (this.unselectableClass && this.contain && r) { + var uneditables = this.getOwnUneditables(), + tmpRange; + if (uneditables.length > 0) { + for (var i = 0, imax = uneditables.length; i < imax; i++) { + tmpRanges = []; + for (var j = 0, jmax = ranges.length; j < jmax; j++) { + if (ranges[j]) { + switch (ranges[j].compareNode(uneditables[i])) { + case 2: + // all selection inside uneditable. remove + break; + case 3: + //section begins before and ends after uneditable. spilt + tmpRange = ranges[j].cloneRange(); + tmpRange.setEndBefore(uneditables[i]); + tmpRanges.push(tmpRange); + + tmpRange = ranges[j].cloneRange(); + tmpRange.setStartAfter(uneditables[i]); + tmpRanges.push(tmpRange); + break; + default: + // in all other cases uneditable does not touch selection. dont modify + tmpRanges.push(ranges[j]); + } + } + ranges = tmpRanges; + } + } + } + } + return ranges; + }, + + getSelection: function() { + return rangy.getSelection(this.win); + }, + + // Sets selection in document to a given range + // Set selection method detects if it fails to set any selection in document and returns null on fail + // (especially needed in webkit where some ranges just can not create selection for no reason) + setSelection: function(range) { + var selection = rangy.getSelection(this.win); + selection.setSingleRange(range); + return (selection && selection.anchorNode && selection.focusNode) ? selection : null; + }, + + + + // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area + selectAll: function() { + var range = this.createRange(), + composer = this.composer, + that = this, + blankEndNode = getWebkitSelectionFixNode(this.composer.element), + blankStartNode = getWebkitSelectionFixNode(this.composer.element), + s; + + var doSelect = function() { + range.setStart(composer.element, 0); + range.setEnd(composer.element, composer.element.childNodes.length); + s = that.setSelection(range); + }; + + var notSelected = function() { + return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None")); + } + + wysihtml5.dom.removeInvisibleSpaces(this.composer.element); + doSelect(); + + if (this.composer.element.firstChild && notSelected()) { + // Try fixing end + this.composer.element.appendChild(blankEndNode); + doSelect(); + + if (notSelected()) { + // Remove end fix + blankEndNode.parentNode.removeChild(blankEndNode); + + // Try fixing beginning + this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild); + doSelect(); + + if (notSelected()) { + // Try fixing both + this.composer.element.appendChild(blankEndNode); + doSelect(); + } + } + } + }, + + createRange: function() { + return rangy.createRange(this.doc); + }, + + isCollapsed: function() { + return this.getSelection().isCollapsed; + }, + + getHtml: function() { + return this.getSelection().toHtml(); + }, + + getPlainText: function () { + return this.getSelection().toString(); + }, + + isEndToEndInNode: function(nodeNames) { + var range = this.getRange(), + parentElement = range.commonAncestorContainer, + startNode = range.startContainer, + endNode = range.endContainer; + + + if (parentElement.nodeType === wysihtml5.TEXT_NODE) { + parentElement = parentElement.parentNode; + } + + if (startNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) { + return false; + } + + if (endNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) { + return false; + } + + while (startNode && startNode !== parentElement) { + if (startNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, startNode)) { + return false; + } + if (wysihtml5.dom.domNode(startNode).prev({ignoreBlankTexts: true})) { + return false; + } + startNode = startNode.parentNode; + } + + while (endNode && endNode !== parentElement) { + if (endNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, endNode)) { + return false; + } + if (wysihtml5.dom.domNode(endNode).next({ignoreBlankTexts: true})) { + return false; + } + endNode = endNode.parentNode; + } + + return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false; + }, + + isInThisEditable: function() { + var sel = this.getSelection(), + fnode = sel.focusNode, + anode = sel.anchorNode; + + // In IE node contains will not work for textnodes, thus taking parentNode + if (fnode && fnode.nodeType !== 1) { + fnode = fnode.parentNode; + } + + if (anode && anode.nodeType !== 1) { + anode = anode.parentNode; + } + + return anode && fnode && + (wysihtml5.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) && + (wysihtml5.dom.contains(this.composer.element, anode) || this.composer.element === anode); + }, + + deselect: function() { + var sel = this.getSelection(); + sel && sel.removeAllRanges(); + } + }); + +})(wysihtml5); +;/** + * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. + * http://code.google.com/p/rangy/ + * + * changed in order to be able ... + * - to use custom tags + * - to detect and replace similar css classes via reg exp + */ +(function(wysihtml5, rangy) { + var defaultTagName = "span"; + + var REG_EXP_WHITE_SPACE = /\s+/g; + + function hasClass(el, cssClass, regExp) { + if (!el.className) { + return false; + } + + var matchingClassNames = el.className.match(regExp) || []; + return matchingClassNames[matchingClassNames.length - 1] === cssClass; + } + + function hasStyleAttr(el, regExp) { + if (!el.getAttribute || !el.getAttribute('style')) { + return false; + } + var matchingStyles = el.getAttribute('style').match(regExp); + return (el.getAttribute('style').match(regExp)) ? true : false; + } + + function addStyle(el, cssStyle, regExp) { + if (el.getAttribute('style')) { + removeStyle(el, regExp); + if (el.getAttribute('style') && !(/^\s*$/).test(el.getAttribute('style'))) { + el.setAttribute('style', cssStyle + ";" + el.getAttribute('style')); + } else { + el.setAttribute('style', cssStyle); + } + } else { + el.setAttribute('style', cssStyle); + } + } + + function addClass(el, cssClass, regExp) { + if (el.className) { + removeClass(el, regExp); + el.className += " " + cssClass; + } else { + el.className = cssClass; + } + } + + function removeClass(el, regExp) { + if (el.className) { + el.className = el.className.replace(regExp, ""); + } + } + + function removeStyle(el, regExp) { + var s, + s2 = []; + if (el.getAttribute('style')) { + s = el.getAttribute('style').split(';'); + for (var i = s.length; i--;) { + if (!s[i].match(regExp) && !(/^\s*$/).test(s[i])) { + s2.push(s[i]); + } + } + if (s2.length) { + el.setAttribute('style', s2.join(';')); + } else { + el.removeAttribute('style'); + } + } + } + + function getMatchingStyleRegexp(el, style) { + var regexes = [], + sSplit = style.split(';'), + elStyle = el.getAttribute('style'); + + if (elStyle) { + elStyle = elStyle.replace(/\s/gi, '').toLowerCase(); + regexes.push(new RegExp("(^|\\s|;)" + style.replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi")); + + for (var i = sSplit.length; i-- > 0;) { + if (!(/^\s*$/).test(sSplit[i])) { + regexes.push(new RegExp("(^|\\s|;)" + sSplit[i].replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi")); + } + } + for (var j = 0, jmax = regexes.length; j < jmax; j++) { + if (elStyle.match(regexes[j])) { + return regexes[j]; + } + } + } + + return false; + } + + function isMatchingAllready(node, tags, style, className) { + if (style) { + return getMatchingStyleRegexp(node, style); + } else if (className) { + return wysihtml5.dom.hasClass(node, className); + } else { + return rangy.dom.arrayContains(tags, node.tagName.toLowerCase()); + } + } + + function areMatchingAllready(nodes, tags, style, className) { + for (var i = nodes.length; i--;) { + if (!isMatchingAllready(nodes[i], tags, style, className)) { + return false; + } + } + return nodes.length ? true : false; + } + + function removeOrChangeStyle(el, style, regExp) { + + var exactRegex = getMatchingStyleRegexp(el, style); + if (exactRegex) { + // adding same style value on property again removes style + removeStyle(el, exactRegex); + return "remove"; + } else { + // adding new style value changes value + addStyle(el, style, regExp); + return "change"; + } + } + + function hasSameClasses(el1, el2) { + return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); + } + + function replaceWithOwnChildren(el) { + var parent = el.parentNode; + while (el.firstChild) { + parent.insertBefore(el.firstChild, el); + } + parent.removeChild(el); + } + + function elementsHaveSameNonClassAttributes(el1, el2) { + if (el1.attributes.length != el2.attributes.length) { + return false; + } + for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { + attr1 = el1.attributes[i]; + name = attr1.name; + if (name != "class") { + attr2 = el2.attributes.getNamedItem(name); + if (attr1.specified != attr2.specified) { + return false; + } + if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) { + return false; + } + } + } + return true; + } + + function isSplitPoint(node, offset) { + if (rangy.dom.isCharacterDataNode(node)) { + if (offset == 0) { + return !!node.previousSibling; + } else if (offset == node.length) { + return !!node.nextSibling; + } else { + return true; + } + } + + return offset > 0 && offset < node.childNodes.length; + } + + function splitNodeAt(node, descendantNode, descendantOffset, container) { + var newNode; + if (rangy.dom.isCharacterDataNode(descendantNode)) { + if (descendantOffset == 0) { + descendantOffset = rangy.dom.getNodeIndex(descendantNode); + descendantNode = descendantNode.parentNode; + } else if (descendantOffset == descendantNode.length) { + descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; + descendantNode = descendantNode.parentNode; + } else { + newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); + } + } + if (!newNode) { + if (!container || descendantNode !== container) { + + newNode = descendantNode.cloneNode(false); + if (newNode.id) { + newNode.removeAttribute("id"); + } + var child; + while ((child = descendantNode.childNodes[descendantOffset])) { + newNode.appendChild(child); + } + rangy.dom.insertAfter(newNode, descendantNode); + + } + } + return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode), container); + } + + function Merge(firstNode) { + this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); + this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; + this.textNodes = [this.firstTextNode]; + } + + Merge.prototype = { + doMerge: function() { + var textBits = [], textNode, parent, text; + for (var i = 0, len = this.textNodes.length; i < len; ++i) { + textNode = this.textNodes[i]; + parent = textNode.parentNode; + textBits[i] = textNode.data; + if (i) { + parent.removeChild(textNode); + if (!parent.hasChildNodes()) { + parent.parentNode.removeChild(parent); + } + } + } + this.firstTextNode.data = text = textBits.join(""); + return text; + }, + + getLength: function() { + var i = this.textNodes.length, len = 0; + while (i--) { + len += this.textNodes[i].length; + } + return len; + }, + + toString: function() { + var textBits = []; + for (var i = 0, len = this.textNodes.length; i < len; ++i) { + textBits[i] = "'" + this.textNodes[i].data + "'"; + } + return "[Merge(" + textBits.join(",") + ")]"; + } + }; + + function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize, cssStyle, similarStyleRegExp, container) { + this.tagNames = tagNames || [defaultTagName]; + this.cssClass = cssClass || ((cssClass === false) ? false : ""); + this.similarClassRegExp = similarClassRegExp; + this.cssStyle = cssStyle || ""; + this.similarStyleRegExp = similarStyleRegExp; + this.normalize = normalize; + this.applyToAnyTagName = false; + this.container = container; + } + + HTMLApplier.prototype = { + getAncestorWithClass: function(node) { + var cssClassMatch; + while (node) { + cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : (this.cssStyle !== "") ? false : true; + if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { + return node; + } + node = node.parentNode; + } + return false; + }, + + // returns parents of node with given style attribute + getAncestorWithStyle: function(node) { + var cssStyleMatch; + while (node) { + cssStyleMatch = this.cssStyle ? hasStyleAttr(node, this.similarStyleRegExp) : false; + + if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) { + return node; + } + node = node.parentNode; + } + return false; + }, + + getMatchingAncestor: function(node) { + var ancestor = this.getAncestorWithClass(node), + matchType = false; + + if (!ancestor) { + ancestor = this.getAncestorWithStyle(node); + if (ancestor) { + matchType = "style"; + } + } else { + if (this.cssStyle) { + matchType = "class"; + } + } + + return { + "element": ancestor, + "type": matchType + }; + }, + + // Normalizes nodes after applying a CSS class to a Range. + postApply: function(textNodes, range) { + var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; + + var merges = [], currentMerge; + + var rangeStartNode = firstNode, rangeEndNode = lastNode; + var rangeStartOffset = 0, rangeEndOffset = lastNode.length; + + var textNode, precedingTextNode; + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + precedingTextNode = null; + if (textNode && textNode.parentNode) { + precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false); + } + if (precedingTextNode) { + if (!currentMerge) { + currentMerge = new Merge(precedingTextNode); + merges.push(currentMerge); + } + currentMerge.textNodes.push(textNode); + if (textNode === firstNode) { + rangeStartNode = currentMerge.firstTextNode; + rangeStartOffset = rangeStartNode.length; + } + if (textNode === lastNode) { + rangeEndNode = currentMerge.firstTextNode; + rangeEndOffset = currentMerge.getLength(); + } + } else { + currentMerge = null; + } + } + // Test whether the first node after the range needs merging + if(lastNode && lastNode.parentNode) { + var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true); + if (nextTextNode) { + if (!currentMerge) { + currentMerge = new Merge(lastNode); + merges.push(currentMerge); + } + currentMerge.textNodes.push(nextTextNode); + } + } + // Do the merges + if (merges.length) { + for (i = 0, len = merges.length; i < len; ++i) { + merges[i].doMerge(); + } + // Set the range boundaries + range.setStart(rangeStartNode, rangeStartOffset); + range.setEnd(rangeEndNode, rangeEndOffset); + } + }, + + getAdjacentMergeableTextNode: function(node, forward) { + var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); + var el = isTextNode ? node.parentNode : node; + var adjacentNode; + var propName = forward ? "nextSibling" : "previousSibling"; + if (isTextNode) { + // Can merge if the node's previous/next sibling is a text node + adjacentNode = node[propName]; + if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) { + return adjacentNode; + } + } else { + // Compare element with its sibling + adjacentNode = el[propName]; + if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) { + return adjacentNode[forward ? "firstChild" : "lastChild"]; + } + } + return null; + }, + + areElementsMergeable: function(el1, el2) { + return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) + && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) + && hasSameClasses(el1, el2) + && elementsHaveSameNonClassAttributes(el1, el2); + }, + + createContainer: function(doc) { + var el = doc.createElement(this.tagNames[0]); + if (this.cssClass) { + el.className = this.cssClass; + } + if (this.cssStyle) { + el.setAttribute('style', this.cssStyle); + } + return el; + }, + + applyToTextNode: function(textNode) { + var parent = textNode.parentNode; + if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { + + if (this.cssClass) { + addClass(parent, this.cssClass, this.similarClassRegExp); + } + if (this.cssStyle) { + addStyle(parent, this.cssStyle, this.similarStyleRegExp); + } + } else { + var el = this.createContainer(rangy.dom.getDocument(textNode)); + textNode.parentNode.insertBefore(el, textNode); + el.appendChild(textNode); + } + }, + + isRemovable: function(el) { + return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && + wysihtml5.lang.string(el.className).trim() === "" && + ( + !el.getAttribute('style') || + wysihtml5.lang.string(el.getAttribute('style')).trim() === "" + ); + }, + + undoToTextNode: function(textNode, range, ancestorWithClass, ancestorWithStyle) { + var styleMode = (ancestorWithClass) ? false : true, + ancestor = ancestorWithClass || ancestorWithStyle, + styleChanged = false; + if (!range.containsNode(ancestor)) { + // Split out the portion of the ancestor from which we can remove the CSS class + var ancestorRange = range.cloneRange(); + ancestorRange.selectNode(ancestor); + + if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { + splitNodeAt(ancestor, range.endContainer, range.endOffset, this.container); + range.setEndAfter(ancestor); + } + if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { + ancestor = splitNodeAt(ancestor, range.startContainer, range.startOffset, this.container); + } + } + + if (!styleMode && this.similarClassRegExp) { + removeClass(ancestor, this.similarClassRegExp); + } + + if (styleMode && this.similarStyleRegExp) { + styleChanged = (removeOrChangeStyle(ancestor, this.cssStyle, this.similarStyleRegExp) === "change"); + } + if (this.isRemovable(ancestor) && !styleChanged) { + replaceWithOwnChildren(ancestor); + } + }, + + applyToRange: function(range) { + var textNodes; + for (var ri = range.length; ri--;) { + textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); + + if (!textNodes.length) { + try { + var node = this.createContainer(range[ri].endContainer.ownerDocument); + range[ri].surroundContents(node); + this.selectNode(range[ri], node); + return; + } catch(e) {} + } + + range[ri].splitBoundaries(); + textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); + if (textNodes.length) { + var textNode; + + for (var i = 0, len = textNodes.length; i < len; ++i) { + textNode = textNodes[i]; + if (!this.getMatchingAncestor(textNode).element) { + this.applyToTextNode(textNode); + } + } + + range[ri].setStart(textNodes[0], 0); + textNode = textNodes[textNodes.length - 1]; + range[ri].setEnd(textNode, textNode.length); + + if (this.normalize) { + this.postApply(textNodes, range[ri]); + } + } + + } + }, + + undoToRange: function(range) { + var textNodes, textNode, ancestorWithClass, ancestorWithStyle, ancestor; + for (var ri = range.length; ri--;) { + + textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); + if (textNodes.length) { + range[ri].splitBoundaries(); + textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); + } else { + var doc = range[ri].endContainer.ownerDocument, + node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + range[ri].insertNode(node); + range[ri].selectNode(node); + textNodes = [node]; + } + + for (var i = 0, len = textNodes.length; i < len; ++i) { + if (range[ri].isValid()) { + textNode = textNodes[i]; + + ancestor = this.getMatchingAncestor(textNode); + if (ancestor.type === "style") { + this.undoToTextNode(textNode, range[ri], false, ancestor.element); + } else if (ancestor.element) { + this.undoToTextNode(textNode, range[ri], ancestor.element); + } + } + } + + if (len == 1) { + this.selectNode(range[ri], textNodes[0]); + } else { + range[ri].setStart(textNodes[0], 0); + textNode = textNodes[textNodes.length - 1]; + range[ri].setEnd(textNode, textNode.length); + + if (this.normalize) { + this.postApply(textNodes, range[ri]); + } + } + + } + }, + + selectNode: function(range, node) { + var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, + canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, + content = isElement ? node.innerHTML : node.data, + isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); + + if (isEmpty && isElement && canHaveHTML) { + // Make sure that caret is visible in node by inserting a zero width no breaking space + try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} + } + range.selectNodeContents(node); + if (isEmpty && isElement) { + range.collapse(false); + } else if (isEmpty) { + range.setStartAfter(node); + range.setEndAfter(node); + } + }, + + getTextSelectedByRange: function(textNode, range) { + var textRange = range.cloneRange(); + textRange.selectNodeContents(textNode); + + var intersectionRange = textRange.intersection(range); + var text = intersectionRange ? intersectionRange.toString() : ""; + textRange.detach(); + + return text; + }, + + isAppliedToRange: function(range) { + var ancestors = [], + appliedType = "full", + ancestor, styleAncestor, textNodes; + + for (var ri = range.length; ri--;) { + + textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); + if (!textNodes.length) { + ancestor = this.getMatchingAncestor(range[ri].startContainer).element; + + return (ancestor) ? { + "elements": [ancestor], + "coverage": appliedType + } : false; + } + + for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { + selectedText = this.getTextSelectedByRange(textNodes[i], range[ri]); + ancestor = this.getMatchingAncestor(textNodes[i]).element; + if (ancestor && selectedText != "") { + ancestors.push(ancestor); + + if (wysihtml5.dom.getTextNodes(ancestor, true).length === 1) { + appliedType = "full"; + } else if (appliedType === "full") { + appliedType = "inline"; + } + } else if (!ancestor) { + appliedType = "partial"; + } + } + + } + + return (ancestors.length) ? { + "elements": ancestors, + "coverage": appliedType + } : false; + }, + + toggleRange: function(range) { + var isApplied = this.isAppliedToRange(range), + parentsExactMatch; + + if (isApplied) { + if (isApplied.coverage === "full") { + this.undoToRange(range); + } else if (isApplied.coverage === "inline") { + parentsExactMatch = areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass); + this.undoToRange(range); + if (!parentsExactMatch) { + this.applyToRange(range); + } + } else { + // partial + if (!areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass)) { + this.undoToRange(range); + } + this.applyToRange(range); + } + } else { + this.applyToRange(range); + } + } + }; + + wysihtml5.selection.HTMLApplier = HTMLApplier; + +})(wysihtml5, rangy); +;/** + * Rich Text Query/Formatting Commands + * + * @example + * var commands = new wysihtml5.Commands(editor); + */ +wysihtml5.Commands = Base.extend( + /** @scope wysihtml5.Commands.prototype */ { + constructor: function(editor) { + this.editor = editor; + this.composer = editor.composer; + this.doc = this.composer.doc; + }, + + /** + * Check whether the browser supports the given command + * + * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") + * @example + * commands.supports("createLink"); + */ + support: function(command) { + return wysihtml5.browser.supportsCommand(this.doc, command); + }, + + /** + * Check whether the browser supports the given command + * + * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") + * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) + * @example + * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg"); + */ + exec: function(command, value) { + var obj = wysihtml5.commands[command], + args = wysihtml5.lang.array(arguments).get(), + method = obj && obj.exec, + result = null; + + // If composer ahs placeholder unset it before command + // Do not apply on commands that are behavioral + if (this.composer.hasPlaceholderSet() && !wysihtml5.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) { + this.composer.element.innerHTML = ""; + this.composer.selection.selectNode(this.composer.element); + } + + this.editor.fire("beforecommand:composer"); + + if (method) { + args.unshift(this.composer); + result = method.apply(obj, args); + } else { + try { + // try/catch for buggy firefox + result = this.doc.execCommand(command, false, value); + } catch(e) {} + } + + this.editor.fire("aftercommand:composer"); + return result; + }, + + remove: function(command, commandValue) { + var obj = wysihtml5.commands[command], + args = wysihtml5.lang.array(arguments).get(), + method = obj && obj.remove; + if (method) { + args.unshift(this.composer); + return method.apply(obj, args); + } + }, + + /** + * Check whether the current command is active + * If the caret is within a bold text, then calling this with command "bold" should return true + * + * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") + * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) + * @return {Boolean} Whether the command is active + * @example + * var isCurrentSelectionBold = commands.state("bold"); + */ + state: function(command, commandValue) { + var obj = wysihtml5.commands[command], + args = wysihtml5.lang.array(arguments).get(), + method = obj && obj.state; + if (method) { + args.unshift(this.composer); + return method.apply(obj, args); + } else { + try { + // try/catch for buggy firefox + return this.doc.queryCommandState(command); + } catch(e) { + return false; + } + } + }, + + /* Get command state parsed value if command has stateValue parsing function */ + stateValue: function(command) { + var obj = wysihtml5.commands[command], + args = wysihtml5.lang.array(arguments).get(), + method = obj && obj.stateValue; + if (method) { + args.unshift(this.composer); + return method.apply(obj, args); + } else { + return false; + } + } +}); +;(function(wysihtml5) { + + var nodeOptions = { + nodeName: "B", + toggle: true + }; + + wysihtml5.commands.bold = { + exec: function(composer, command) { + wysihtml5.commands.formatInline.exec(composer, command, nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; + +}(wysihtml5)); +;(function(wysihtml5) { + + var nodeOptions = { + nodeName: "A", + toggle: false + }; + + function getOptions(value) { + var options = typeof value === 'object' ? value : {'href': value}; + return wysihtml5.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get(); + } + + wysihtml5.commands.createLink = { + exec: function(composer, command, value) { + var opts = getOptions(value); + + if (composer.selection.isCollapsed() && !this.state(composer, command)) { + var textNode = composer.doc.createTextNode(opts.attribute.href); + composer.selection.insertNode(textNode); + composer.selection.selectNode(textNode); + } + wysihtml5.commands.formatInline.exec(composer, command, opts); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; + +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + nodeName: "A" + }; + + wysihtml5.commands.removeLink = { + exec: function(composer, command) { + wysihtml5.commands.formatInline.remove(composer, command, nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; + +})(wysihtml5); +;/** + * Set font size css class + */ +(function(wysihtml5) { + var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g; + + wysihtml5.commands.fontSize = { + exec: function(composer, command, size) { + wysihtml5.commands.formatInline.exec(composer, command, {className: "wysiwyg-font-size-" + size, classRegExp: REG_EXP, toggle: true}); + }, + + state: function(composer, command, size) { + return wysihtml5.commands.formatInline.state(composer, command, {className: "wysiwyg-font-size-" + size}); + } + }; +})(wysihtml5); +;/** + * Set font size by inline style + */ +(function(wysihtml5) { + + wysihtml5.commands.fontSizeStyle = { + exec: function(composer, command, size) { + size = size.size || size; + if (!(/^\s*$/).test(size)) { + wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: "fontSize", styleValue: size, toggle: false}); + } + }, + + state: function(composer, command, size) { + return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: "fontSize", styleValue: size || undefined}); + }, + + remove: function(composer, command) { + return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: "fontSize"}); + }, + + stateValue: function(composer, command) { + var styleStr, + st = this.state(composer, command); + + if (st && wysihtml5.lang.object(st).isArray()) { + st = st[0]; + } + if (st) { + styleStr = st.getAttribute("style"); + if (styleStr) { + return wysihtml5.quirks.styleParser.parseFontSize(styleStr); + } + } + return false; + } + }; +})(wysihtml5); +;/** + * Set color css class + */ +(function(wysihtml5) { + var REG_EXP = /wysiwyg-color-[0-9a-z]+/g; + + wysihtml5.commands.foreColor = { + exec: function(composer, command, color) { + wysihtml5.commands.formatInline.exec(composer, command, {className: "wysiwyg-color-" + color, classRegExp: REG_EXP, toggle: true}); + }, + + state: function(composer, command, color) { + return wysihtml5.commands.formatInline.state(composer, command, {className: "wysiwyg-color-" + color}); + } + }; +})(wysihtml5); +;/** + * Sets text color by inline styles + */ +(function(wysihtml5) { + + wysihtml5.commands.foreColorStyle = { + exec: function(composer, command, color) { + var colorVals, colString; + + if (!color) { return; } + + colorVals = wysihtml5.quirks.styleParser.parseColor("color:" + (color.color || color), "color"); + + if (colorVals) { + colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(", ") : "rgba(" + colorVals.join(', ')) + ')'; + wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: "color", styleValue: colString}); + } + }, + + state: function(composer, command, color) { + var colorVals = color ? wysihtml5.quirks.styleParser.parseColor("color:" + (color.color || color), "color") : null, + colString; + + + if (colorVals) { + colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(", ") : "rgba(" + colorVals.join(', ')) + ')'; + } + + return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: "color", styleValue: colString}); + }, + + remove: function(composer, command) { + return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: "color"}); + }, + + stateValue: function(composer, command, props) { + var st = this.state(composer, command), + colorStr, + val = false; + + if (st && wysihtml5.lang.object(st).isArray()) { + st = st[0]; + } + + if (st) { + colorStr = st.getAttribute("style"); + if (colorStr) { + val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color"); + return wysihtml5.quirks.styleParser.unparseColor(val, props); + } + } + return false; + } + + }; +})(wysihtml5); +;/** + * Sets text background color by inline styles + */ +(function(wysihtml5) { + + wysihtml5.commands.bgColorStyle = { + exec: function(composer, command, color) { + var colorVals = wysihtml5.quirks.styleParser.parseColor("background-color:" + (color.color || color), "background-color"), + colString; + + if (colorVals) { + colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')'; + wysihtml5.commands.formatInline.exec(composer, command, {styleProperty: 'backgroundColor', styleValue: colString}); + } + }, + + state: function(composer, command, color) { + var colorVals = color ? wysihtml5.quirks.styleParser.parseColor("background-color:" + (color.color || color), "background-color") : null, + colString; + + + if (colorVals) { + colString = (colorVals[3] === 1 ? "rgb(" + [colorVals[0], colorVals[1], colorVals[2]].join(', ') : "rgba(" + colorVals.join(', ')) + ')'; + } + + return wysihtml5.commands.formatInline.state(composer, command, {styleProperty: 'backgroundColor', styleValue: colString}); + }, + + remove: function(composer, command) { + return wysihtml5.commands.formatInline.remove(composer, command, {styleProperty: 'backgroundColor'}); + }, + + stateValue: function(composer, command, props) { + var st = this.state(composer, command), + colorStr, + val = false; + + if (st && wysihtml5.lang.object(st).isArray()) { + st = st[0]; + } + + if (st) { + colorStr = st.getAttribute('style'); + if (colorStr) { + val = wysihtml5.quirks.styleParser.parseColor(colorStr, "background-color"); + return wysihtml5.quirks.styleParser.unparseColor(val, props); + } + } + return false; + } + + }; +})(wysihtml5); +;/* Formatblock + * Is used to insert block level elements + * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...) + * +*/ +(function(wysihtml5) { + + var dom = wysihtml5.dom, + // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 + // instead of creating a H4 within a H1 which would result in semantically invalid html + UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre", + BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote", + 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"; + + function correctOptionsForSimilarityCheck(options) { + return { + nodeName: options.nodeName || null, + className: (!options.classRegExp) ? options.className || null : null, + classRegExp: options.classRegExp || null, + styleProperty: options.styleProperty || null + }; + } + + function getRangeNode(node, offset) { + if (node.nodeType === 3) { + return node; + } else { + return node.childNodes[offset] || node; + } + } + + // Returns if node is a line break + function isBr(n) { + return n && n.nodeType === 1 && n.nodeName === "BR"; + } + + // Is block level element + function isBlock(n, composer) { + return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block"; + } + + // 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) + function isBookmark(n) { + return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary'); + } + + // Is line breaking node + function isLineBreaking(n, composer) { + return isBr(n) || isBlock(n, composer); + } + + // Removes empty block level elements + function cleanup(composer, newBlockElements) { + wysihtml5.dom.removeInvisibleSpaces(composer.element); + var container = composer.element, + allElements = container.querySelectorAll(BLOCK_ELEMENTS), + noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '), + uneditables = container.querySelectorAll(noEditQuery), + elements = wysihtml5.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents + nbIdx; + + for (var i = elements.length; i--;) { + if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) { + // If cleanup removes some new block elements. remove them from newblocks array too + nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]); + if (nbIdx > -1) { + newBlockElements.splice(nbIdx, 1); + } + elements[i].parentNode.removeChild(elements[i]); + } + } + + return newBlockElements; + } + + function defaultNodeName(composer) { + return composer.config.useLineBreaks ? "DIV" : "P"; + } + + // The outermost un-nestable block element parent of from node + function findOuterBlock(node, container, allBlocks) { + var n = node, + block = null; + + while (n && container && n !== container) { + if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) { + block = n; + } + n = n.parentNode; + } + + return block; + } + + // Clone for splitting the inner inline element out of its parent inline elements context + // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return + function cloneOuterInlines(node, container) { + var n = node, + innerNode, + parentNode, + el = null, + el2; + + while (n && container && n !== container) { + if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) { + parentNode = n; + if (el === null) { + el = n.cloneNode(false); + innerNode = el; + } else { + el2 = n.cloneNode(false); + el2.appendChild(el); + el = el2; + } + } + n = n.parentNode; + } + + return { + parent: parentNode, + outerNode: el, + innerNode: innerNode + }; + } + + // Formats an element according to options nodeName, className, styleProperty, styleValue + // If element is not defined, creates new element + // if opotions is null, remove format instead + function applyOptionsToElement(element, options, composer) { + + if (!element) { + element = composer.doc.createElement(options.nodeName || defaultNodeName(composer)); + // Add invisible space as otherwise webkit cannot set selection or range to it correctly + element.appendChild(composer.doc.createTextNode(wysihtml5.INVISIBLE_SPACE)); + } + + if (options.nodeName && element.nodeName !== options.nodeName) { + element = dom.renameElement(element, options.nodeName); + } + + // Remove similar classes before applying className + if (options.classRegExp) { + element.className = element.className.replace(options.classRegExp, ""); + } + if (options.className) { + element.classList.add(options.className); + } + + if (options.styleProperty && typeof options.styleValue !== "undefined") { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + } + + return element; + } + + // Unsets element properties by options + // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes) + function removeOptionsFromElement(element, options, composer) { + var style, classes, + prevNode = element.previousSibling, + nextNode = element.nextSibling, + unwrapped = false; + + if (options.styleProperty) { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = ''; + } + if (options.className) { + element.classList.remove(options.className); + } + + if (options.classRegExp) { + element.className = element.className.replace(options.classRegExp, ""); + } + + // Clean up blank class attribute + if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") { + element.removeAttribute('class'); + } + + if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) { + style = element.getAttribute('style'); + if (!style || style.trim() === '') { + dom.unwrap(element); + unwrapped = true; + } else { + element = dom.renameElement(element, defaultNodeName(composer)); + } + } + + // Clean up blank style attribute + if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") { + element.removeAttribute('style'); + } + + if (unwrapped) { + applySurroundingLineBreaks(prevNode, nextNode, composer); + } + } + + // Unwraps block level elements from inside content + // Useful as not all block level elements can contain other block-levels + function unwrapBlocksFromContent(element) { + var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents + nextEl, prevEl; + + for (var i = blocks.length; i--;) { + nextEl = wysihtml5.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}), + prevEl = wysihtml5.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + + if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') { + if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') { + blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl); + } + } + if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') { + if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') { + blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl); + } + } + wysihtml5.dom.unwrap(blocks[i]); + } + } + + // Fix ranges that visually cover whole block element to actually cover the block + function fixRangeCoverage(range, composer) { + var node, + start = range.startContainer, + end = range.endContainer; + + // If range has only one childNode and it is end to end the range, extend the range to contain the container element too + // This ensures the wrapper node is modified and optios added to it + if (start && start.nodeType === 1 && start === end) { + if (start.firstChild === start.lastChild && range.endOffset === 1) { + if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') { + range.setStartBefore(start); + range.setEndAfter(end); + } + } + return; + } + + // 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 + if (start && start.nodeType === 1 && end.nodeType === 3) { + if (start.firstChild === end && range.endOffset === end.data.length) { + if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') { + range.setEndAfter(start); + } + } + return; + } + + // 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 + if (end && end.nodeType === 1 && start.nodeType === 3) { + if (end.firstChild === start && range.startOffset === 0) { + if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') { + range.setStartBefore(end); + } + } + return; + } + + // If range covers a whole textnode and the textnode is the only child of node, extend range to node + if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) { + if (range.endOffset == end.data.length && range.startOffset === 0) { + node = start.parentNode; + if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') { + range.setStartBefore(node); + range.setEndAfter(node); + } + } + return; + } + } + + // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges + // Some places do not allow block level elements inbetween (inside ul and outside li) + // TODO: might need extending for other nodes besides li (maybe dd,dl,dt) + function fixNotPermittedInsertionPoints(ranges) { + var newRanges = [], + lis, j, maxj, tmpRange, rangePos, closestLI; + + for (var i = 0, maxi = ranges.length; i < maxi; i++) { + + // Fixes range start and end positions if inside UL or OL element (outside of LI) + if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) { + ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0); + } + if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) { + closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)]; + if (closestLI.childNodes) { + ranges[i].setEnd(closestLI, closestLI.childNodes.length); + } + } + + // Get all LI eleemnts in selection (fully or partially covered) + // And make sure ranges are either inside LI or outside UL/OL + // Split and add new ranges as needed to cover same range content + // TODO: Needs improvement to accept DL, DD, DT + lis = ranges[i].getNodes([1], function(node) { + return node.nodeName === "LI"; + }); + if (lis.length > 0) { + + for (j = 0, maxj = lis.length; j < maxj; j++) { + rangePos = ranges[i].compareNode(lis[j]); + + // Fixes start of range that crosses LI border + if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) { + // Range starts before and ends inside the node + + tmpRange = ranges[i].cloneRange(); + closestLI = wysihtml5.dom.domNode(lis[j]).prev({nodeTypes: [1]}); + + if (closestLI) { + tmpRange.setEnd(closestLI, closestLI.childNodes.length); + } else if (lis[j].closest('ul, ol')) { + tmpRange.setEndBefore(lis[j].closest('ul, ol')); + } else { + tmpRange.setEndBefore(lis[j]); + } + newRanges.push(tmpRange); + ranges[i].setStart(lis[j], 0); + } + + // Fixes end of range that crosses li border + if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) { + // Range starts inside the node and ends after node + + tmpRange = ranges[i].cloneRange(); + tmpRange.setEnd(lis[j], lis[j].childNodes.length); + newRanges.push(tmpRange); + + // Find next LI in list and if present set range to it, else + closestLI = wysihtml5.dom.domNode(lis[j]).next({nodeTypes: [1]}); + if (closestLI) { + ranges[i].setStart(closestLI, 0); + } else if (lis[j].closest('ul, ol')) { + ranges[i].setStartAfter(lis[j].closest('ul, ol')); + } else { + ranges[i].setStartAfter(lis[j]); + } + } + } + newRanges.push(ranges[i]); + } else { + newRanges.push(ranges[i]); + } + } + return newRanges; + } + + // Return options object with nodeName set if original did not have any + // Node name is set to local or global default + function getOptionsWithNodename(options, defaultName, composer) { + var correctedOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null; + if (correctedOptions) { + correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer); + } + return correctedOptions; + } + + // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted + // Also wraps empty clones of split parent tags around fragment to keep formatting + // 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) + function injectFragmentToRange(fragment, range, composer, firstOuterBlock) { + var rangeStartContainer = range.startContainer, + firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true), + outerInlines, first, last, prev, next; + + if (firstOuterBlock) { + // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between + first = fragment.firstChild; + last = fragment.lastChild; + + composer.selection.splitElementAtCaret(firstOuterBlock, fragment); + + next = wysihtml5.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true}); + prev = wysihtml5.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + + if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) { + first.parentNode.insertBefore(composer.doc.createElement('br'), first); + } + + if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) { + next.parentNode.insertBefore(composer.doc.createElement('br'), next); + } + + } else { + // Ensure node does not get inserted into an inline where it is not allowed + outerInlines = cloneOuterInlines(rangeStartContainer, composer.element); + if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) { + if (fragment.childNodes.length === 1) { + while(fragment.firstChild.firstChild) { + outerInlines.innerNode.appendChild(fragment.firstChild.firstChild); + } + fragment.firstChild.appendChild(outerInlines.outerNode); + } + composer.selection.splitElementAtCaret(outerInlines.parent, fragment); + } else { + var fc = fragment.firstChild, + lc = fragment.lastChild; + + range.insertNode(fragment); + // restore range position as it might get lost in webkit sometimes + range.setStartBefore(fc); + range.setEndAfter(lc); + } + } + } + + // Removes all block formatting from range + function clearRangeBlockFromating(range, closestBlockName, composer) { + var r = range.cloneRange(), + prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling, + nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling, + content = r.extractContents(), + fragment = composer.doc.createDocumentFragment(), + children, blocks, + first = true; + + while(content.firstChild) { + // Iterate over all selection content first level childNodes + if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) { + // If node is a block element + // Split block formating and add new block to wrap caret + + unwrapBlocksFromContent(content.firstChild); + children = wysihtml5.dom.unwrap(content.firstChild); + + // Add line break before if needed + if (children.length > 0) { + if ( + (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) || + (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer))) + ){ + fragment.appendChild(composer.doc.createElement('BR')); + } + } + + for (var c = 0, cmax = children.length; c < cmax; c++) { + fragment.appendChild(children[c]); + } + + // Add line break after if needed + if (children.length > 0) { + if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) { + if (nextNode || fragment.lastChild !== content.lastChild) { + fragment.appendChild(composer.doc.createElement('BR')); + } + } + } + + } else { + fragment.appendChild(content.firstChild); + } + + first = false; + } + blocks = wysihtml5.lang.array(fragment.childNodes).get(); + injectFragmentToRange(fragment, r, composer); + return blocks; + } + + // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself) + function removeSurroundingLineBreaks(prevNode, nextNode, composer) { + var prevPrev = prevNode && wysihtml5.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + if (isBr(nextNode)) { + nextNode.parentNode.removeChild(nextNode); + } + if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) { + prevNode.parentNode.removeChild(prevNode); + } + } + + function applySurroundingLineBreaks(prevNode, nextNode, composer) { + var prevPrev; + + if (prevNode && isBookmark(prevNode)) { + prevNode = prevNode.previousSibling; + } + if (nextNode && isBookmark(nextNode)) { + nextNode = nextNode.nextSibling; + } + + prevPrev = prevNode && prevNode.previousSibling; + + if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) { + prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling); + } + + if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) { + nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode); + } + } + + var isWhitespaceBefore = function (textNode, offset) { + var str = textNode.data ? textNode.data.slice(0, offset) : ""; + return (/^\s*$/).test(str); + } + + var isWhitespaceAfter = function (textNode, offset) { + var str = textNode.data ? textNode.data.slice(offset) : ""; + return (/^\s*$/).test(str); + } + + var trimBlankTextsAndBreaks = function(fragment) { + if (fragment) { + while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) { + fragment.removeChild(fragment.firstChild); + } + + while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) { + fragment.removeChild(fragment.lastChild); + } + + if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) { + fragment.removeChild(fragment.firstChild); + } + + if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) { + fragment.removeChild(fragment.lastChild); + } + } + } + + // Wrap the range with a block level element + // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur + function wrapRangeWithElement(range, options, closestBlockName, composer) { + var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null, + r = range.cloneRange(), + rangeStartContainer = r.startContainer, + startNode = getRangeNode(r.startContainer, r.startOffset), + endNode = getRangeNode(r.endContainer, r.endOffset), + prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml5.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}), + nextNode = ( + ( + r.endContainer.nodeType === 1 && + r.endContainer.childNodes[r.endOffset] === endNode && + ( + endNode.nodeType === 1 || + !isWhitespaceAfter(endNode, r.endOffset) && + !wysihtml5.dom.domNode(endNode).is.rangyBookmark() + ) + ) || ( + r.endContainer === endNode && + endNode.nodeType === 3 && + !isWhitespaceAfter(endNode, r.endOffset) + ) + ) ? endNode : wysihtml5.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}), + content = r.extractContents(), + fragment = composer.doc.createDocumentFragment(), + similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null, + splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"), + firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start + wrapper, blocks, children, + firstc, lastC; + + if (wysihtml5.dom.domNode(nextNode).is.rangyBookmark()) { + endNode = nextNode; + nextNode = endNode.nextSibling; + } + + trimBlankTextsAndBreaks(content); + + if (options && options.nodeName === "BLOCKQUOTE") { + + // If blockquote is to be inserted no quessing just add it as outermost block on line or selection + var tmpEl = applyOptionsToElement(null, options, composer); + tmpEl.appendChild(content); + fragment.appendChild(tmpEl); + blocks = [tmpEl]; + + } else { + + if (!content.firstChild) { + // IF selection is caret (can happen if line is empty) add format around tag + fragment.appendChild(applyOptionsToElement(null, options, composer)); + } else { + + while(content.firstChild) { + // Iterate over all selection content first level childNodes + + if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) { + + // If node is a block element + // Escape(split) block formatting at caret + applyOptionsToElement(content.firstChild, options, composer); + if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) { + unwrapBlocksFromContent(content.firstChild); + } + fragment.appendChild(content.firstChild); + + } else { + + // Wrap subsequent non-block nodes inside new block element + wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer); + while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) { + if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) { + unwrapBlocksFromContent(content.firstChild); + } + wrapper.appendChild(content.firstChild); + } + fragment.appendChild(wrapper); + } + } + } + + blocks = wysihtml5.lang.array(fragment.childNodes).get(); + } + injectFragmentToRange(fragment, r, composer, firstOuterBlock); + removeSurroundingLineBreaks(prevNode, nextNode, composer); + + // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block + // (if it contains rangy bookmark, so selection can be restored later correctly) + if (blocks.length > 0 && + ( + typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml5.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark() + ) + ) { + blocks[blocks.length - 1].appendChild(composer.doc.createElement('br')); + } + return blocks; + } + + // Find closest block level element + function getParentBlockNodeName(element, composer) { + var parentNode = wysihtml5.dom.getParentElement(element, { + query: BLOCK_ELEMENTS + }, null, composer.element); + + return (parentNode) ? parentNode.nodeName : null; + } + + // Expands caret to cover the closest block that: + // * cannot contain other block level elements (h1-6,p, etc) + // * Has the same nodeName that is to be inserted + // * has insertingNodeName + // * is DIV if insertingNodeName is not present + // + // If nothing found selects the current line + function expandCaretToBlock(composer, insertingNodeName) { + var parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, { + query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'), + }, null, composer.element), + range; + + if (parent) { + range = composer.selection.createRange(); + range.selectNode(parent); + composer.selection.setSelection(range); + } else if (!composer.isEmpty()) { + composer.selection.selectLine(); + } + } + + // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element + // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway + function selectElements(newBlockElements, composer) { + var range = composer.selection.createRange(), + lastEl = newBlockElements[newBlockElements.length - 1], + lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0; + + range.setStart(newBlockElements[0], 0); + range.setEnd(lastEl, lastOffset); + range.select(); + } + + // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each + // Return created/modified block level elements + // Method can be either "apply" or "remove" + function formatSelection(method, composer, options) { + var ranges = composer.selection.getOwnRanges(), + newBlockElements = [], + closestBlockName; + + // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th) + ranges = fixNotPermittedInsertionPoints(ranges); + + for (var i = ranges.length; i--;) { + fixRangeCoverage(ranges[i], composer); + closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer); + if (method === "remove") { + newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer)); + } else { + newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer)); + } + } + return newBlockElements; + } + + // If properties is passed as a string, look for tag with that tagName/query + function parseOptions(options) { + if (typeof options === "string") { + options = { + nodeName: options.toUpperCase() + }; + } + return options; + } + + function caretIsOnEmptyLine(composer) { + var caretInfo; + if (composer.selection.isCollapsed()) { + caretInfo = composer.selection.getNodesNearCaret(); + if (caretInfo && caretInfo.caretNode) { + if ( + // caret is allready breaknode + wysihtml5.dom.domNode(caretInfo.caretNode).is.lineBreak() || + // caret is textnode + (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml5.dom.domNode(caretInfo.prevNode).is.lineBreak())) || + // Caret is temprorary rangy selection marker + (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') && + (!caretInfo.prevNode || wysihtml5.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml5.dom.domNode(caretInfo.prevNode).is.block()) && + (!caretInfo.nextNode || wysihtml5.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml5.dom.domNode(caretInfo.nextNode).is.block()) + ) + ) { + return true; + } + } + } + return false; + } + + wysihtml5.commands.formatBlock = { + exec: function(composer, command, options) { + options = parseOptions(options); + var newBlockElements = [], + ranges, range, bookmark, state, closestBlockName; + + // Find if current format state is active if options.toggle is set as true + // In toggle case active state elemets are formatted instead of working directly on selection + if (options && options.toggle) { + state = this.state(composer, command, options); + } + if (state) { + // Remove format from state nodes if toggle set and state on and selection is collapsed + bookmark = rangy.saveSelection(composer.win); + for (var j = 0, jmax = state.length; j < jmax; j++) { + removeOptionsFromElement(state[j], options, composer); + } + + } else { + // If selection is caret expand it to cover nearest suitable block element or row if none found + if (composer.selection.isCollapsed()) { + bookmark = rangy.saveSelection(composer.win); + if (caretIsOnEmptyLine(composer)) { + composer.selection.selectLine(); + } else { + expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined); + } + } + if (options) { + newBlockElements = formatSelection("apply", composer, options); + } else { + // Options == null means block formatting should be removed from selection + newBlockElements = formatSelection("remove", composer); + } + + } + + // Remove empty block elements that may be left behind + // Also remove them from new blocks list + newBlockElements = cleanup(composer, newBlockElements); + + // Restore selection + if (bookmark) { + rangy.restoreSelection(bookmark); + } else { + selectElements(newBlockElements, composer); + } + }, + + // Removes all block formatting from selection + remove: function(composer, command, options) { + options = parseOptions(options); + var newBlockElements, bookmark; + + // If selection is caret expand it to cover nearest suitable block element or row if none found + if (composer.selection.isCollapsed()) { + bookmark = rangy.saveSelection(composer.win); + expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined); + } + + newBlockElements = formatSelection("remove", composer); + newBlockElements = cleanup(composer, newBlockElements); + + // Restore selection + if (bookmark) { + rangy.restoreSelection(bookmark); + } else { + selectElements(newBlockElements, composer); + } + }, + + // If options as null is passed returns status describing all block level elements + state: function(composer, command, options) { + options = parseOptions(options); + + var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection + return wysihtml5.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS }); + }).bind(this)), + parentNodes = composer.selection.getSelectedOwnNodes(), + parent; + + // Finds matching elements that are parents of selection and adds to nodes list + for (var i = 0, maxi = parentNodes.length; i < maxi; i++) { + parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element); + if (parent && nodes.indexOf(parent) === -1) { + nodes.push(parent); + } + } + + return (nodes.length === 0) ? false : nodes; + } + + }; +})(wysihtml5); +;/* Formats block for as a
block + * Useful in conjuction for sytax highlight utility: highlight.js + * + * Usage: + * + * editorInstance.composer.commands.exec("formatCode", "language-html"); +*/ + +(function(wysihtml5){ + wysihtml5.commands.formatCode = { + + exec: function(composer, command, classname) { + var pre = this.state(composer)[0], + code, range, selectedNodes; + + if (pre) { + // caret is already within a
...
+ composer.selection.executeAndRestore(function() { + code = pre.querySelector("code"); + wysihtml5.dom.replaceWithChildNodes(pre); + if (code) { + wysihtml5.dom.replaceWithChildNodes(code); + } + }); + } else { + // Wrap in
...
+ range = composer.selection.getRange(); + selectedNodes = range.extractContents(); + pre = composer.doc.createElement("pre"); + code = composer.doc.createElement("code"); + + if (classname) { + code.className = classname; + } + + pre.appendChild(code); + code.appendChild(selectedNodes); + range.insertNode(pre); + composer.selection.selectNode(pre); + } + }, + + state: function(composer) { + var selectedNode = composer.selection.getSelectedNode(), node; + if (selectedNode && selectedNode.nodeName && selectedNode.nodeName == "PRE"&& + selectedNode.firstChild && selectedNode.firstChild.nodeName && selectedNode.firstChild.nodeName == "CODE") { + return [selectedNode]; + } else { + node = wysihtml5.dom.getParentElement(selectedNode, { query: "pre code" }); + return node ? [node.parentNode] : false; + } + } + }; +}(wysihtml5)); +;/** + * Unifies all inline tags additions and removals + * See https://github.com/Voog/wysihtml/pull/169 for specification of action + */ + +(function(wysihtml5) { + + var defaultTag = "SPAN", + 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", + queryAliasMap = { + "b": "b, strong", + "strong": "b, strong", + "em": "em, i", + "i": "em, i" + }; + + function hasNoClass(element) { + return (/^\s*$/).test(element.className); + } + + function hasNoStyle(element) { + return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style')); + } + + // Associative arrays in javascript are really objects and do not have length defined + // Thus have to check emptyness in a different way + function hasNoAttributes(element) { + var attr = wysihtml5.dom.getAttributes(element); + return wysihtml5.lang.object(attr).isEmpty(); + } + + // compares two nodes if they are semantically the same + // Used in cleanup to find consequent semantically similar elements for merge + function isSameNode(element1, element2) { + var classes1, classes2, + attr1, attr2; + + if (element1.nodeType !== 1 || element2.nodeType !== 1) { + return false; + } + + if (element1.nodeName !== element2.nodeName) { + return false; + } + + classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' '); + classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' '); + if (wysihtml5.lang.array(classes1).without(classes2).length > 0) { + return false; + } + + attr1 = wysihtml5.dom.getAttributes(element1); + attr2 = wysihtml5.dom.getAttributes(element2); + + if (attr1.length !== attr2.length || !wysihtml5.lang.object(wysihtml5.lang.object(attr1).difference(attr2)).isEmpty()) { + return false; + } + + return true; + } + + function createWrapNode(textNode, options) { + var nodeName = options && options.nodeName || defaultTag, + element = textNode.ownerDocument.createElement(nodeName); + + // Remove similar classes before applying className + if (options.classRegExp) { + element.className = element.className.replace(options.classRegExp, ""); + } + + if (options.className) { + element.classList.add(options.className); + } + + if (options.styleProperty && typeof options.styleValue !== "undefined") { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + } + + if (options.attribute) { + if (typeof options.attribute === "object") { + for (var a in options.attribute) { + if (options.attribute.hasOwnProperty(a)) { + element.setAttribute(a, options.attribute[a]); + } + } + } else if (typeof options.attributeValue !== "undefined") { + element.setAttribute(options.attribute, options.attributeValue); + } + } + + return element; + } + + // Tests if attr2 list contains all attributes present in attr1 + // Note: attr 1 can have more attributes than attr2 + function containsSameAttributes(attr1, attr2) { + for (var a in attr1) { + if (attr1.hasOwnProperty(a)) { + if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) { + return false; + } + } + } + return true; + } + + // If attrbutes and values are the same > remove + // if attributes or values + function updateElementAttributes(element, newAttributes, toggle) { + var attr = wysihtml5.dom.getAttributes(element), + fullContain = containsSameAttributes(newAttributes, attr), + attrDifference = wysihtml5.lang.object(attr).difference(newAttributes), + a, b; + + if (fullContain && toggle !== false) { + for (a in newAttributes) { + if (newAttributes.hasOwnProperty(a)) { + element.removeAttribute(a); + } + } + } else { + + /*if (!wysihtml5.lang.object(attrDifference).isEmpty()) { + for (b in attrDifference) { + if (attrDifference.hasOwnProperty(b)) { + element.removeAttribute(b); + } + } + }*/ + + for (a in newAttributes) { + if (newAttributes.hasOwnProperty(a)) { + element.setAttribute(a, newAttributes[a]); + } + } + } + } + + function updateFormatOfElement(element, options) { + var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch; + + if (options.className) { + if (options.toggle !== false && element.classList.contains(options.className)) { + element.classList.remove(options.className); + } else { + if (options.classRegExp) { + element.className = element.className.replace(options.classRegExp, ''); + } + element.classList.add(options.className); + } + if (hasNoClass(element)) { + element.removeAttribute('class'); + } + } + + // change/remove style + if (options.styleProperty) { + if (options.toggle !== false && element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = ''; + } else { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + } + } + if (hasNoStyle(element)) { + element.removeAttribute('style'); + } + + if (options.attribute) { + if (typeof options.attribute === "object") { + newAttributes = options.attribute; + } else { + newAttributes = {}; + newAttributes[options.attribute] = options.attributeValue || ''; + } + updateElementAttributes(element, newAttributes, options.toggle); + } + + + // Handle similar semantically same elements (queryAliasMap) + nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null; + nodeQueryMatch = nodeNameQuery ? wysihtml5.dom.domNode(element).test({ query: nodeNameQuery }) : false; + + // Unwrap element if no attributes present and node name given + // or no attributes and if no nodename set but node is the default + if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) { + if ( + ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) && + hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element) + ) { + wysihtml5.dom.unwrap(element); + } + + } + } + + // Fetch all textnodes in selection + // Empty textnodes are ignored except the one containing text caret + function getSelectedTextNodes(selection, splitBounds) { + var textNodes = []; + + if (!selection.isCollapsed()) { + textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) { + // Exclude empty nodes except caret node + return (!wysihtml5.dom.domNode(node).is.emptyTextNode()); + }, splitBounds)); + } + + return textNodes; + } + + function findSimilarTextNodeWrapper(textNode, options, container, exact) { + var node = textNode, + similarOptions = exact ? options : correctOptionsForSimilarityCheck(options); + + do { + if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) { + return node; + } + node = node.parentNode; + } while (node && node !== container); + + return null; + } + + function correctOptionsForSimilarityCheck(options) { + return { + nodeName: options.nodeName || null, + className: (!options.classRegExp) ? options.className || null : null, + classRegExp: options.classRegExp || null, + styleProperty: options.styleProperty || null + }; + } + + // Finds inline node with similar nodeName/style/className + // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes + function isSimilarNode(node, options) { + var o; + if (options.nodeName) { + var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase(); + return wysihtml5.dom.domNode(node).test({ query: query }); + } else { + o = wysihtml5.lang.object(options).clone(); + o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted + return wysihtml5.dom.domNode(node).test(o); + } + } + + function selectRange(composer, range) { + var d = document.documentElement || document.body, + oldScrollTop = d.scrollTop, + oldScrollLeft = d.scrollLeft, + selection = rangy.getSelection(composer.win); + + rangy.getSelection(composer.win).removeAllRanges(); + + // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again + try { + rangy.getSelection(composer.win).addRange(range); + } catch (e) {} + if (!composer.doc.activeElement || !wysihtml5.dom.contains(composer.element, composer.doc.activeElement)) { + composer.element.focus(); + d.scrollTop = oldScrollTop; + d.scrollLeft = oldScrollLeft; + rangy.getSelection(composer.win).addRange(range); + } + } + + function selectTextNodes(textNodes, composer) { + var range = rangy.createRange(composer.doc), + lastText = textNodes[textNodes.length - 1]; + + if (textNodes[0] && lastText) { + range.setStart(textNodes[0], 0); + range.setEnd(lastText, lastText.length); + selectRange(composer, range); + } + + } + + function selectTextNode(composer, node, start, end) { + var range = rangy.createRange(composer.doc); + if (node) { + range.setStart(node, start); + range.setEnd(node, typeof end !== 'undefined' ? end : start); + selectRange(composer, range); + } + } + + function getState(composer, options, exact) { + var searchNodes = getSelectedTextNodes(composer.selection), + nodes = [], + partial = false, + node, range, caretNode; + + if (composer.selection.isInThisEditable()) { + + if (searchNodes.length === 0 && composer.selection.isCollapsed()) { + caretNode = composer.selection.getSelection().anchorNode; + if (!caretNode) { + // selection not in editor + return { + nodes: [], + partial: false + }; + } + if (caretNode.nodeType === 3) { + searchNodes = [caretNode]; + } + } + + // Handle collapsed selection caret + if (!searchNodes.length) { + range = composer.selection.getOwnRanges()[0]; + if (range) { + searchNodes = [range.endContainer]; + } + } + + for (var i = 0, maxi = searchNodes.length; i < maxi; i++) { + node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact); + if (node) { + nodes.push(node); + } else { + partial = true; + } + } + + } + + return { + nodes: nodes, + partial: partial + }; + } + + // Returns if caret is inside a word in textnode (not on boundary) + // If selection anchornode is not text node, returns false + function caretIsInsideWord(selection) { + var anchor, offset, beforeChar, afterChar; + if (selection) { + anchor = selection.anchorNode; + offset = selection.anchorOffset; + if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) { + beforeChar = anchor.data[offset - 1]; + afterChar = anchor.data[offset]; + return (/\w/).test(beforeChar) && (/\w/).test(afterChar); + } + } + return false; + } + + // Returns a range and textnode containing object from caret position covering a whole word + // wordOffsety describes the original position of caret in the new textNode + // Caret has to be inside a textNode. + function getRangeForWord(selection) { + var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar, + txtNodes = []; + if (selection) { + anchor = selection.anchorNode; + offset = offsetStart = offsetEnd = selection.anchorOffset; + doc = anchor.ownerDocument; + range = rangy.createRange(doc); + + if (anchor && anchor.nodeType === 3) { + + while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) { + offsetStart--; + } + + while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) { + offsetEnd++; + } + + range.setStartAndEnd(anchor, offsetStart, offsetEnd); + range.splitBoundaries(); + txtNodes = range.getNodes([3], function(node) { + return (!wysihtml5.dom.domNode(node).is.emptyTextNode()); + }); + + return { + wordOffset: offset - offsetStart, + range: range, + textNode: txtNodes[0] + }; + + } + } + return false; + } + + // Contents of 2 elements are merged to fitst element. second element is removed as consequence + function mergeContents(element1, element2) { + while (element2.firstChild) { + element1.appendChild(element2.firstChild); + } + element2.parentNode.removeChild(element2); + } + + function mergeConsequentSimilarElements(elements) { + for (var i = elements.length; i--;) { + + if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup + + if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) { + mergeContents(elements[i], elements[i].nextSibling); + } + + if (elements[i].previousSibling && isSameNode(elements[i] , elements[i].previousSibling)) { + mergeContents(elements[i].previousSibling, elements[i]); + } + + } + } + } + + function cleanupAndSetSelection(composer, textNodes, options) { + if (textNodes.length > 0) { + selectTextNodes(textNodes, composer); + } + mergeConsequentSimilarElements(getState(composer, options).nodes); + if (textNodes.length > 0) { + selectTextNodes(textNodes, composer); + } + } + + function cleanupAndSetCaret(composer, textNode, offset, options) { + selectTextNode(composer, textNode, offset); + mergeConsequentSimilarElements(getState(composer, options).nodes); + selectTextNode(composer, textNode, offset); + } + + // Formats a textnode with given options + function formatTextNode(textNode, options) { + var wrapNode = createWrapNode(textNode, options); + + textNode.parentNode.insertBefore(wrapNode, textNode); + wrapNode.appendChild(textNode); + } + + // Changes/toggles format of a textnode + function unformatTextNode(textNode, composer, options) { + var container = composer.element, + wrapNode = findSimilarTextNodeWrapper(textNode, options, container), + newWrapNode; + + if (wrapNode) { + newWrapNode = wrapNode.cloneNode(false); + + wysihtml5.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode); + updateFormatOfElement(newWrapNode, options); + } + } + + // Removes the format around textnode + function removeFormatFromTextNode(textNode, composer, options) { + var container = composer.element, + wrapNode = findSimilarTextNodeWrapper(textNode, options, container); + + if (wrapNode) { + wysihtml5.dom.domNode(textNode).escapeParent(wrapNode); + } + } + + // Creates node around caret formated with options + function formatTextRange(range, composer, options) { + var wrapNode = createWrapNode(range.endContainer, options); + + range.surroundContents(wrapNode); + composer.selection.selectNode(wrapNode); + } + + // Changes/toggles format of whole selection + function updateFormat(composer, textNodes, state, options) { + var exactState = getState(composer, options, true), + selection = composer.selection.getSelection(), + wordObj, textNode, newNode, i; + + if (!textNodes.length) { + // Selection is caret + + + if (options.toggle !== false) { + if (caretIsInsideWord(selection)) { + + // Unformat whole word + wordObj = getRangeForWord(selection); + textNode = wordObj.textNode; + unformatTextNode(wordObj.textNode, composer, options); + cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options); + + } else { + + // Escape caret out of format + textNode = composer.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + newNode = state.nodes[0].cloneNode(false); + newNode.appendChild(textNode); + composer.selection.splitElementAtCaret(state.nodes[0], newNode); + updateFormatOfElement(newNode, options); + cleanupAndSetSelection(composer, [textNode], options); + var s = composer.selection.getSelection(); + if (s.anchorNode && s.focusNode) { + // Has an error in IE when collapsing selection. probably from rangy + try { + s.collapseToEnd(); + } catch (e) {} + } + } + } else { + // In non-toggle mode the closest state element has to be found and the state updated differently + for (i = state.nodes.length; i--;) { + updateFormatOfElement(state.nodes[i], options); + } + } + + } else { + + if (!exactState.partial && options.toggle !== false) { + + // If whole selection (all textnodes) are in the applied format + // remove the format from selection + // Non-toggle mode never removes. Remove has to be called explicitly + for (i = textNodes.length; i--;) { + unformatTextNode(textNodes[i], composer, options); + } + + } else { + + // Selection is partially in format + // change it to new if format if textnode allreafy in similar state + // else just apply + + for (i = textNodes.length; i--;) { + + if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) { + unformatTextNode(textNodes[i], composer, options); + } + + if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) { + formatTextNode(textNodes[i], options); + } + } + + } + + cleanupAndSetSelection(composer, textNodes, options); + } + } + + // Removes format from selection + function removeFormat(composer, textNodes, state, options) { + var textNode, textOffset, newNode, i, + selection = composer.selection.getSelection(); + + if (!textNodes.length) { + textNode = selection.anchorNode; + textOffset = selection.anchorOffset; + + for (i = state.nodes.length; i--;) { + wysihtml5.dom.unwrap(state.nodes[i]); + } + + cleanupAndSetCaret(composer, textNode, textOffset, options); + } else { + for (i = textNodes.length; i--;) { + removeFormatFromTextNode(textNodes[i], composer, options); + } + cleanupAndSetSelection(composer, textNodes, options); + } + } + + // Adds format to selection + function applyFormat(composer, textNodes, options) { + var wordObj, i, + selection = composer.selection.getSelection(); + + if (!textNodes.length) { + // Handle collapsed selection caret and return + if (caretIsInsideWord(selection)) { + + wordObj = getRangeForWord(selection); + formatTextNode(wordObj.textNode, options); + cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options); + + } else { + var r = composer.selection.getOwnRanges()[0]; + if (r) { + formatTextRange(r, composer, options); + } + } + + } else { + // Handle textnodes in selection and apply format + for (i = textNodes.length; i--;) { + formatTextNode(textNodes[i], options); + } + cleanupAndSetSelection(composer, textNodes, options); + } + } + + // If properties is passed as a string, correct options with that nodeName + function fixOptions(options) { + options = (typeof options === "string") ? { nodeName: options } : options; + if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); } + return options; + } + + wysihtml5.commands.formatInline = { + + // Basics: + // In case of plain text or inline state not set wrap all non-empty textnodes with + // 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) + // In case of changing mode every textnode is addressed separatly + exec: function(composer, command, options) { + options = fixOptions(options); + + // Join adjactent textnodes first + composer.element.normalize(); + + var textNodes = getSelectedTextNodes(composer.selection, true), + state = getState(composer, options); + if (state.nodes.length > 0) { + // Text allready has the format applied + updateFormat(composer, textNodes, state, options); + } else { + // Selection is not in the applied format + applyFormat(composer, textNodes, options); + } + composer.element.normalize(); + }, + + remove: function(composer, command, options) { + options = fixOptions(options); + composer.element.normalize(); + + var textNodes = getSelectedTextNodes(composer.selection, true), + state = getState(composer, options); + + if (state.nodes.length > 0) { + // Text allready has the format applied + removeFormat(composer, textNodes, state, options); + } + + composer.element.normalize(); + }, + + state: function(composer, command, options) { + options = fixOptions(options); + var nodes = getState(composer, options, true).nodes; + return (nodes.length === 0) ? false : nodes; + } + }; + +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + nodeName: "BLOCKQUOTE", + toggle: true + }; + + wysihtml5.commands.insertBlockQuote = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; + +})(wysihtml5); +;(function(wysihtml5){ + wysihtml5.commands.insertHTML = { + exec: function(composer, command, html) { + composer.selection.insertHTML(html); + }, + + state: function() { + return false; + } + }; +}(wysihtml5)); +;(function(wysihtml5) { + var NODE_NAME = "IMG"; + + wysihtml5.commands.insertImage = { + /** + * Inserts an + * If selection is already an image link, it removes it + * + * @example + * // either ... + * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg"); + * // ... or ... + * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" }); + */ + exec: function(composer, command, value) { + value = typeof(value) === "object" ? value : { src: value }; + + var doc = composer.doc, + image = this.state(composer), + textNode, + parent; + + // If image is selected and src ie empty, set the caret before it and delete the image + if (image && !value.src) { + composer.selection.setBefore(image); + parent = image.parentNode; + parent.removeChild(image); + + // and it's parent too if it hasn't got any other relevant child nodes + wysihtml5.dom.removeEmptyTextNodes(parent); + if (parent.nodeName === "A" && !parent.firstChild) { + composer.selection.setAfter(parent); + parent.parentNode.removeChild(parent); + } + + // firefox and ie sometimes don't remove the image handles, even though the image got removed + wysihtml5.quirks.redraw(composer.element); + return; + } + + // If image selected change attributes accordingly + if (image) { + for (var key in value) { + if (value.hasOwnProperty(key)) { + image.setAttribute(key === "className" ? "class" : key, value[key]); + } + } + return; + } + + // Otherwise lets create the image + image = doc.createElement(NODE_NAME); + + for (var i in value) { + image.setAttribute(i === "className" ? "class" : i, value[i]); + } + + composer.selection.insertNode(image); + if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) { + textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + composer.selection.insertNode(textNode); + composer.selection.setAfter(textNode); + } else { + composer.selection.setAfter(image); + } + }, + + state: function(composer) { + var doc = composer.doc, + selectedNode, + text, + imagesInSelection; + + if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) { + return false; + } + + selectedNode = composer.selection.getSelectedNode(); + if (!selectedNode) { + return false; + } + + if (selectedNode.nodeName === NODE_NAME) { + // This works perfectly in IE + return selectedNode; + } + + if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) { + return false; + } + + text = composer.selection.getText(); + text = wysihtml5.lang.string(text).trim(); + if (text) { + return false; + } + + imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) { + return node.nodeName === "IMG"; + }); + + if (imagesInSelection.length !== 1) { + return false; + } + + return imagesInSelection[0]; + } + }; +})(wysihtml5); +;(function(wysihtml5) { + var LINE_BREAK = "
" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); + + wysihtml5.commands.insertLineBreak = { + exec: function(composer, command) { + composer.selection.insertHTML(LINE_BREAK); + }, + + state: function() { + return false; + } + }; +})(wysihtml5); +;(function(wysihtml5){ + wysihtml5.commands.insertOrderedList = { + exec: function(composer, command) { + wysihtml5.commands.insertList.exec(composer, command, "OL"); + }, + + state: function(composer, command) { + return wysihtml5.commands.insertList.state(composer, command, "OL"); + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.insertUnorderedList = { + exec: function(composer, command) { + wysihtml5.commands.insertList.exec(composer, command, "UL"); + }, + + state: function(composer, command) { + return wysihtml5.commands.insertList.state(composer, command, "UL"); + } + }; +}(wysihtml5)); +;wysihtml5.commands.insertList = (function(wysihtml5) { + + var isNode = function(node, name) { + if (node && node.nodeName) { + if (typeof name === 'string') { + name = [name]; + } + for (var n = name.length; n--;) { + if (node.nodeName === name[n]) { + return true; + } + } + } + return false; + }; + + var findListEl = function(node, nodeName, composer) { + var ret = { + el: null, + other: false + }; + + if (node) { + var parentLi = wysihtml5.dom.getParentElement(node, { query: "li" }, false, composer.element), + otherNodeName = (nodeName === "UL") ? "OL" : "UL"; + + if (isNode(node, nodeName)) { + ret.el = node; + } else if (isNode(node, otherNodeName)) { + ret = { + el: node, + other: true + }; + } else if (parentLi) { + if (isNode(parentLi.parentNode, nodeName)) { + ret.el = parentLi.parentNode; + } else if (isNode(parentLi.parentNode, otherNodeName)) { + ret = { + el : parentLi.parentNode, + other: true + }; + } + } + } + + // do not count list elements outside of composer + if (ret.el && !composer.element.contains(ret.el)) { + ret.el = null; + } + + return ret; + }; + + var handleSameTypeList = function(el, nodeName, composer) { + var otherNodeName = (nodeName === "UL") ? "OL" : "UL", + otherLists, innerLists; + // Unwrap list + //
  • foo
  • bar
+ // becomes: + // foo
bar
+ + composer.selection.executeAndRestoreRangy(function() { + otherLists = getListsInSelection(otherNodeName, composer); + if (otherLists.length) { + for (var l = otherLists.length; l--;) { + wysihtml5.dom.renameElement(otherLists[l], nodeName.toLowerCase()); + } + } else { + innerLists = getListsInSelection(['OL', 'UL'], composer); + for (var i = innerLists.length; i--;) { + wysihtml5.dom.resolveList(innerLists[i], composer.config.useLineBreaks); + } + if (innerLists.length === 0) { + wysihtml5.dom.resolveList(el, composer.config.useLineBreaks); + } + } + }); + }; + + var handleOtherTypeList = function(el, nodeName, composer) { + var otherNodeName = (nodeName === "UL") ? "OL" : "UL"; + // Turn an ordered list into an unordered list + //
  1. foo
  2. bar
+ // becomes: + //
  • foo
  • bar
+ // Also rename other lists in selection + composer.selection.executeAndRestoreRangy(function() { + var renameLists = [el].concat(getListsInSelection(otherNodeName, composer)); + + // All selection inner lists get renamed too + for (var l = renameLists.length; l--;) { + wysihtml5.dom.renameElement(renameLists[l], nodeName.toLowerCase()); + } + }); + }; + + var getListsInSelection = function(nodeName, composer) { + var ranges = composer.selection.getOwnRanges(), + renameLists = []; + + for (var r = ranges.length; r--;) { + renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) { + return isNode(node, nodeName); + })); + } + + return renameLists; + }; + + var createListFallback = function(nodeName, composer) { + var sel = rangy.saveSelection(composer.win); + + // Fallback for Create list + var tempClassName = "_wysihtml5-temp-" + new Date().getTime(), + isEmpty, list; + + composer.commands.exec("formatBlock", { + "nodeName": "div", + "className": tempClassName + }); + + var tempElement = composer.element.querySelector("." + tempClassName); + + // This space causes new lists to never break on enter + var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; + tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + if (tempElement) { + isEmpty = (/^(\s|(
))+$/i).test(tempElement.innerHTML); + list = wysihtml5.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer); + if (sel) { + rangy.restoreSelection(sel); + } + if (isEmpty) { + composer.selection.selectNode(list.querySelector("li"), true); + } + } + }; + + return { + exec: function(composer, command, nodeName) { + var doc = composer.doc, + cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList", + s = composer.selection.getSelection(), + anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, + fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode, + selectedNode, list; + + if (s.isBackwards()) { + // swap variables + anode = [fnode, fnode = anode][0]; + } + + if (wysihtml5.dom.domNode(fnode).is.emptyTextNode(true) && fnode) { + fnode = wysihtml5.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + if (wysihtml5.dom.domNode(anode).is.emptyTextNode(true) && anode) { + anode = wysihtml5.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + + if (anode && fnode) { + if (anode === fnode) { + selectedNode = anode; + } else { + selectedNode = wysihtml5.dom.domNode(anode).commonAncestor(fnode, composer.element); + } + } else { + selectedNode = composer.selection.getSelectedNode(); + } + + list = findListEl(selectedNode, nodeName, composer); + + if (!list.el) { + if (composer.commands.support(cmd)) { + doc.execCommand(cmd, false, null); + } else { + createListFallback(nodeName, composer); + } + } else if (list.other) { + handleOtherTypeList(list.el, nodeName, composer); + } else { + handleSameTypeList(list.el, nodeName, composer); + } + }, + + state: function(composer, command, nodeName) { + var selectedNode = composer.selection.getSelectedNode(), + list = findListEl(selectedNode, nodeName, composer); + + return (list.el && !list.other) ? list.el : false; + } + }; + +})(wysihtml5); +;(function(wysihtml5){ + + var nodeOptions = { + nodeName: "I", + toggle: true + }; + + wysihtml5.commands.italic = { + exec: function(composer, command) { + wysihtml5.commands.formatInline.exec(composer, command, nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; + +}(wysihtml5)); +;(function(wysihtml5) { + + var nodeOptions = { + className: "wysiwyg-text-align-center", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + + wysihtml5.commands.justifyCenter = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; + +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + className: "wysiwyg-text-align-left", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + + wysihtml5.commands.justifyLeft = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + className: "wysiwyg-text-align-right", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + + wysihtml5.commands.justifyRight = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + className: "wysiwyg-text-align-justify", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + + wysihtml5.commands.justifyFull = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "right", + toggle: true + }; + + wysihtml5.commands.alignRightStyle = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "left", + toggle: true + }; + + wysihtml5.commands.alignLeftStyle = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; + +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "center", + toggle: true + }; + + wysihtml5.commands.alignCenterStyle = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; + +})(wysihtml5); +;(function(wysihtml5) { + + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "justify", + toggle: true + }; + + wysihtml5.commands.alignJustifyStyle = { + exec: function(composer, command) { + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); + } + }; +})(wysihtml5); +;(function(wysihtml5){ + wysihtml5.commands.redo = { + exec: function(composer) { + return composer.undoManager.redo(); + }, + + state: function(composer) { + return false; + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + + var nodeOptions = { + nodeName: "U", + toggle: true + }; + + wysihtml5.commands.underline = { + exec: function(composer, command) { + wysihtml5.commands.formatInline.exec(composer, command, nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; + +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.undo = { + exec: function(composer) { + return composer.undoManager.undo(); + }, + + state: function(composer) { + return false; + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.createTable = { + exec: function(composer, command, value) { + var col, row, html; + if (value && value.cols && value.rows && parseInt(value.cols, 10) > 0 && parseInt(value.rows, 10) > 0) { + if (value.tableStyle) { + html = ""; + } else { + html = "
"; + } + html += ""; + for (row = 0; row < value.rows; row ++) { + html += ''; + for (col = 0; col < value.cols; col ++) { + html += ""; + } + html += ''; + } + html += "

"; + composer.commands.exec("insertHTML", html); + //composer.selection.insertHTML(html); + } + }, + + state: function(composer, command) { + return false; + } + }; + +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.mergeTableCells = { + exec: function(composer, command) { + if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) { + if (this.state(composer, command)) { + wysihtml5.dom.table.unmergeCell(composer.tableSelection.start); + } else { + wysihtml5.dom.table.mergeCellsBetween(composer.tableSelection.start, composer.tableSelection.end); + } + } + }, + + state: function(composer, command) { + if (composer.tableSelection) { + var start = composer.tableSelection.start, + end = composer.tableSelection.end; + if (start && end && start == end && + (( + wysihtml5.dom.getAttribute(start, "colspan") && + parseInt(wysihtml5.dom.getAttribute(start, "colspan"), 10) > 1 + ) || ( + wysihtml5.dom.getAttribute(start, "rowspan") && + parseInt(wysihtml5.dom.getAttribute(start, "rowspan"), 10) > 1 + )) + ) { + return [start]; + } + } + return false; + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.addTableCells = { + exec: function(composer, command, value) { + if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) { + + // switches start and end if start is bigger than end (reverse selection) + var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end); + if (value == "before" || value == "above") { + wysihtml5.dom.table.addCells(tableSelect.start, value); + } else if (value == "after" || value == "below") { + wysihtml5.dom.table.addCells(tableSelect.end, value); + } + setTimeout(function() { + composer.tableSelection.select(tableSelect.start, tableSelect.end); + },0); + } + }, + + state: function(composer, command) { + return false; + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.deleteTableCells = { + exec: function(composer, command, value) { + if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) { + var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end), + idx = wysihtml5.dom.table.indexOf(tableSelect.start), + selCell, + table = composer.tableSelection.table; + + wysihtml5.dom.table.removeCells(tableSelect.start, value); + setTimeout(function() { + // move selection to next or previous if not present + selCell = wysihtml5.dom.table.findCell(table, idx); + + if (!selCell){ + if (value == "row") { + selCell = wysihtml5.dom.table.findCell(table, { + "row": idx.row - 1, + "col": idx.col + }); + } + + if (value == "column") { + selCell = wysihtml5.dom.table.findCell(table, { + "row": idx.row, + "col": idx.col - 1 + }); + } + } + if (selCell) { + composer.tableSelection.select(selCell, selCell); + } + }, 0); + } + }, + + state: function(composer, command) { + return false; + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + wysihtml5.commands.indentList = { + exec: function(composer, command, value) { + var listEls = composer.selection.getSelectionParentsByTag('LI'); + if (listEls) { + return this.tryToPushLiLevel(listEls, composer.selection); + } + return false; + }, + + state: function(composer, command) { + return false; + }, + + tryToPushLiLevel: function(liNodes, selection) { + var listTag, list, prevLi, liNode, prevLiList, + found = false; + + selection.executeAndRestoreRangy(function() { + + for (var i = liNodes.length; i--;) { + liNode = liNodes[i]; + listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL'; + list = liNode.ownerDocument.createElement(listTag); + prevLi = wysihtml5.dom.domNode(liNode).prev({nodeTypes: [wysihtml5.ELEMENT_NODE]}); + prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null; + + if (prevLi) { + if (prevLiList) { + prevLiList.appendChild(liNode); + } else { + list.appendChild(liNode); + prevLi.appendChild(list); + } + found = true; + } + } + + }); + return found; + } + }; +}(wysihtml5)); +;(function(wysihtml5){ + + wysihtml5.commands.outdentList = { + exec: function(composer, command, value) { + var listEls = composer.selection.getSelectionParentsByTag('LI'); + if (listEls) { + return this.tryToPullLiLevel(listEls, composer); + } + return false; + }, + + state: function(composer, command) { + return false; + }, + + tryToPullLiLevel: function(liNodes, composer) { + var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList, + found = false, + that = this; + + composer.selection.executeAndRestoreRangy(function() { + + for (var i = liNodes.length; i--;) { + liNode = liNodes[i]; + if (liNode.parentNode) { + listNode = liNode.parentNode; + + if (listNode.tagName === 'OL' || listNode.tagName === 'UL') { + found = true; + + outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element); + outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element); + + if (outerListNode && outerLiNode) { + + if (liNode.nextSibling) { + afterList = that.getAfterList(listNode, liNode); + liNode.appendChild(afterList); + } + outerListNode.insertBefore(liNode, outerLiNode.nextSibling); + + } else { + + if (liNode.nextSibling) { + afterList = that.getAfterList(listNode, liNode); + liNode.appendChild(afterList); + } + + for (var j = liNode.childNodes.length; j--;) { + listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling); + } + + listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling); + liNode.parentNode.removeChild(liNode); + + } + + // cleanup + if (listNode.childNodes.length === 0) { + listNode.parentNode.removeChild(listNode); + } + } + } + } + + }); + return found; + }, + + getAfterList: function(listNode, liNode) { + var nodeName = listNode.nodeName, + newList = document.createElement(nodeName); + + while (liNode.nextSibling) { + newList.appendChild(liNode.nextSibling); + } + return newList; + } + + }; +}(wysihtml5)); +;(function(wysihtml5){ + + var nodeOptions = { + nodeName: "SUB", + toggle: true + }; + + wysihtml5.commands.subscript = { + exec: function(composer, command) { + wysihtml5.commands.formatInline.exec(composer, command, nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; +}(wysihtml5)); +;(function(wysihtml5) { + + var nodeOptions = { + nodeName: "SUP", + toggle: true + }; + + wysihtml5.commands.superscript = { + exec: function(composer, command) { + wysihtml5.commands.formatInline.exec(composer, command, nodeOptions); + }, + + state: function(composer, command) { + return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + } + }; +}(wysihtml5)); +;/** + * Undo Manager for wysihtml5 + * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface + */ +(function(wysihtml5) { + var Z_KEY = 90, + Y_KEY = 89, + BACKSPACE_KEY = 8, + DELETE_KEY = 46, + MAX_HISTORY_ENTRIES = 25, + DATA_ATTR_NODE = "data-wysihtml5-selection-node", + DATA_ATTR_OFFSET = "data-wysihtml5-selection-offset", + UNDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '', + REDO_HTML = '' + wysihtml5.INVISIBLE_SPACE + '', + dom = wysihtml5.dom; + + function cleanTempElements(doc) { + var tempElement; + while (tempElement = doc.querySelector("._wysihtml5-temp")) { + tempElement.parentNode.removeChild(tempElement); + } + } + + wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend( + /** @scope wysihtml5.UndoManager.prototype */ { + constructor: function(editor) { + this.editor = editor; + this.composer = editor.composer; + this.element = this.composer.element; + + this.position = 0; + this.historyStr = []; + this.historyDom = []; + + this.transact(); + + this._observe(); + }, + + _observe: function() { + var that = this, + doc = this.composer.sandbox.getDocument(), + lastKey; + + // Catch CTRL+Z and CTRL+Y + dom.observe(this.element, "keydown", function(event) { + if (event.altKey || (!event.ctrlKey && !event.metaKey)) { + return; + } + + var keyCode = event.keyCode, + isUndo = keyCode === Z_KEY && !event.shiftKey, + isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); + + if (isUndo) { + that.undo(); + event.preventDefault(); + } else if (isRedo) { + that.redo(); + event.preventDefault(); + } + }); + + // Catch delete and backspace + dom.observe(this.element, "keydown", function(event) { + var keyCode = event.keyCode; + if (keyCode === lastKey) { + return; + } + + lastKey = keyCode; + + if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { + that.transact(); + } + }); + + this.editor + .on("newword:composer", function() { + that.transact(); + }) + + .on("beforecommand:composer", function() { + that.transact(); + }); + }, + + transact: function() { + var previousHtml = this.historyStr[this.position - 1], + currentHtml = this.composer.getValue(false, false), + composerIsVisible = this.element.offsetWidth > 0 && this.element.offsetHeight > 0, + range, node, offset, element, position; + + if (currentHtml === previousHtml) { + return; + } + + var length = this.historyStr.length = this.historyDom.length = this.position; + if (length > MAX_HISTORY_ENTRIES) { + this.historyStr.shift(); + this.historyDom.shift(); + this.position--; + } + + this.position++; + + if (composerIsVisible) { + // Do not start saving selection if composer is not visible + range = this.composer.selection.getRange(); + node = (range && range.startContainer) ? range.startContainer : this.element; + offset = (range && range.startOffset) ? range.startOffset : 0; + + if (node.nodeType === wysihtml5.ELEMENT_NODE) { + element = node; + } else { + element = node.parentNode; + position = this.getChildNodeIndex(element, node); + } + + element.setAttribute(DATA_ATTR_OFFSET, offset); + if (typeof(position) !== "undefined") { + element.setAttribute(DATA_ATTR_NODE, position); + } + } + + var clone = this.element.cloneNode(!!currentHtml); + this.historyDom.push(clone); + this.historyStr.push(currentHtml); + + if (element) { + element.removeAttribute(DATA_ATTR_OFFSET); + element.removeAttribute(DATA_ATTR_NODE); + } + + }, + + undo: function() { + this.transact(); + + if (!this.undoPossible()) { + return; + } + + this.set(this.historyDom[--this.position - 1]); + this.editor.fire("undo:composer"); + }, + + redo: function() { + if (!this.redoPossible()) { + return; + } + + this.set(this.historyDom[++this.position - 1]); + this.editor.fire("redo:composer"); + }, + + undoPossible: function() { + return this.position > 1; + }, + + redoPossible: function() { + return this.position < this.historyStr.length; + }, + + set: function(historyEntry) { + this.element.innerHTML = ""; + + var i = 0, + childNodes = historyEntry.childNodes, + length = historyEntry.childNodes.length; + + for (; i"; + }, + + getValue: function(parse, clearInternals) { + var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element); + if (parse !== false) { + value = this.parent.parse(value, (clearInternals === false) ? false : true); + } + return value; + }, + + setValue: function(html, parse) { + if (parse !== false) { + html = this.parent.parse(html); + } + + try { + this.element.innerHTML = html; + } catch (e) { + this.element.innerText = html; + } + }, + + cleanUp: function(rules) { + var bookmark; + if (this.selection && this.selection.isInThisEditable()) { + bookmark = rangy.saveSelection(this.win); + } + this.parent.parse(this.element, undefined, rules); + if (bookmark) { + rangy.restoreSelection(bookmark); + } + }, + + show: function() { + this.editableArea.style.display = this._displayStyle || ""; + + if (!this.config.noTextarea && !this.textarea.element.disabled) { + // Firefox needs this, otherwise contentEditable becomes uneditable + this.disable(); + this.enable(); + } + }, + + hide: function() { + this._displayStyle = dom.getStyle("display").from(this.editableArea); + if (this._displayStyle === "none") { + this._displayStyle = null; + } + this.editableArea.style.display = "none"; + }, + + disable: function() { + this.parent.fire("disable:composer"); + this.element.removeAttribute("contentEditable"); + }, + + enable: function() { + this.parent.fire("enable:composer"); + this.element.setAttribute("contentEditable", "true"); + }, + + focus: function(setToEnd) { + // IE 8 fires the focus event after .focus() + // This is needed by our simulate_placeholder.js to work + // therefore we clear it ourselves this time + if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { + this.clear(); + } + + this.base(); + + var lastChild = this.element.lastChild; + if (setToEnd && lastChild && this.selection) { + if (lastChild.nodeName === "BR") { + this.selection.setBefore(this.element.lastChild); + } else { + this.selection.setAfter(this.element.lastChild); + } + } + }, + + getScrollPos: function() { + if (this.doc && this.win) { + var pos = {}; + + if (typeof this.win.pageYOffset !== "undefined") { + pos.y = this.win.pageYOffset; + } else { + pos.y = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollTop; + } + + if (typeof this.win.pageXOffset !== "undefined") { + pos.x = this.win.pageXOffset; + } else { + pos.x = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollLeft; + } + + return pos; + } + }, + + setScrollPos: function(pos) { + if (pos && typeof pos.x !== "undefined" && typeof pos.y !== "undefined") { + this.win.scrollTo(pos.x, pos.y); + } + }, + + getTextContent: function() { + return dom.getTextContent(this.element); + }, + + hasPlaceholderSet: function() { + return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet; + }, + + isEmpty: function() { + var innerHTML = this.element.innerHTML.toLowerCase(); + return (/^(\s|
|<\/br>|

|<\/p>)*$/i).test(innerHTML) || + innerHTML === "" || + innerHTML === "
" || + innerHTML === "

" || + innerHTML === "


" || + this.hasPlaceholderSet(); + }, + + _initContentEditableArea: function() { + var that = this; + if (this.config.noTextarea) { + this.sandbox = new dom.ContentEditableArea(function() { + that._create(); + }, { + className: this.config.classNames.sandbox + }, this.editableArea); + } else { + this.sandbox = new dom.ContentEditableArea(function() { + that._create(); + }, { + className: this.config.classNames.sandbox + }); + this.editableArea = this.sandbox.getContentEditable(); + dom.insert(this.editableArea).after(this.textarea.element); + this._createWysiwygFormField(); + } + }, + + _initSandbox: function() { + var that = this; + this.sandbox = new dom.Sandbox(function() { + that._create(); + }, { + stylesheets: this.config.stylesheets, + className: this.config.classNames.sandbox + }); + this.editableArea = this.sandbox.getIframe(); + + var textareaElement = this.textarea.element; + dom.insert(this.editableArea).after(textareaElement); + + this._createWysiwygFormField(); + }, + + // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor + _createWysiwygFormField: function() { + if (this.textarea.element.form) { + var hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "_wysihtml5_mode"; + hiddenField.value = 1; + dom.insert(hiddenField).after(this.textarea.element); + } + }, + + _create: function() { + var that = this; + this.doc = this.sandbox.getDocument(); + this.win = this.sandbox.getWindow(); + this.element = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body; + if (!this.config.noTextarea) { + this.textarea = this.parent.textarea; + this.element.innerHTML = this.textarea.getValue(true, false); + } else { + this.cleanUp(); // cleans contenteditable on initiation as it may contain html + } + + // Make sure our selection handler is ready + this.selection = new wysihtml5.Selection(this.parent, this.element, this.config.classNames.uneditableContainer); + + // Make sure commands dispatcher is ready + this.commands = new wysihtml5.Commands(this.parent); + + if (!this.config.noTextarea) { + dom.copyAttributes([ + "className", "spellcheck", "title", "lang", "dir", "accessKey" + ]).from(this.textarea.element).to(this.element); + } + + this._initAutoLinking(); + + dom.addClass(this.element, this.config.classNames.composer); + // + // Make the editor look like the original textarea, by syncing styles + if (this.config.style && !this.config.contentEditableMode) { + this.style(); + } + + this.observe(); + + var name = this.config.name; + if (name) { + dom.addClass(this.element, name); + if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); } + } + + this.enable(); + + if (!this.config.noTextarea && this.textarea.element.disabled) { + this.disable(); + } + + // Simulate html5 placeholder attribute on contentEditable element + var placeholderText = typeof(this.config.placeholder) === "string" + ? this.config.placeholder + : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")); + if (placeholderText) { + dom.simulatePlaceholder(this.parent, this, placeholderText, this.config.classNames.placeholder); + } + + // Make sure that the browser avoids using inline styles whenever possible + this.commands.exec("styleWithCSS", false); + + this._initObjectResizing(); + this._initUndoManager(); + this._initLineBreaking(); + + // Simulate html5 autofocus on contentEditable element + // This doesn't work on IOS (5.1.1) + if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) { + setTimeout(function() { that.focus(true); }, 100); + } + + // IE sometimes leaves a single paragraph, which can't be removed by the user + if (!browser.clearsContentEditableCorrectly()) { + wysihtml5.quirks.ensureProperClearing(this); + } + + // Set up a sync that makes sure that textarea and editor have the same content + if (this.initSync && this.config.sync) { + this.initSync(); + } + + // Okay hide the textarea, we are ready to go + if (!this.config.noTextarea) { this.textarea.hide(); } + + // Fire global (before-)load event + this.parent.fire("beforeload").fire("load"); + }, + + _initAutoLinking: function() { + var that = this, + supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(), + supportsAutoLinking = browser.doesAutoLinkingInContentEditable(); + + if (supportsDisablingOfAutoLinking) { + this.commands.exec("AutoUrlDetect", false, false); + } + + if (!this.config.autoLink) { + return; + } + + // Only do the auto linking by ourselves when the browser doesn't support auto linking + // OR when he supports auto linking but we were able to turn it off (IE9+) + if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { + this.parent.on("newword:composer", function() { + if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) { + var nodeWithSelection = that.selection.getSelectedNode(), + uneditables = that.element.querySelectorAll("." + that.config.classNames.uneditableContainer), + isInUneditable = false; + + for (var i = uneditables.length; i--;) { + if (wysihtml5.dom.contains(uneditables[i], nodeWithSelection)) { + isInUneditable = true; + } + } + + if (!isInUneditable) dom.autoLink(nodeWithSelection, [that.config.classNames.uneditableContainer]); + } + }); + + dom.observe(this.element, "blur", function() { + dom.autoLink(that.element, [that.config.classNames.uneditableContainer]); + }); + } + + // Assuming we have the following: + //
http://www.google.de + // If a user now changes the url in the innerHTML we want to make sure that + // it's synchronized with the href attribute (as long as the innerHTML is still a url) + var // Use a live NodeList to check whether there are any links in the document + links = this.sandbox.getDocument().getElementsByTagName("a"), + // The autoLink helper method reveals a reg exp to detect correct urls + urlRegExp = dom.autoLink.URL_REG_EXP, + getTextContent = function(element) { + var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim(); + if (textContent.substr(0, 4) === "www.") { + textContent = "http://" + textContent; + } + return textContent; + }; + + dom.observe(this.element, "keydown", function(event) { + if (!links.length) { + return; + } + + var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), + link = dom.getParentElement(selectedNode, { query: "a" }, 4), + textContent; + + if (!link) { + return; + } + + textContent = getTextContent(link); + // keydown is fired before the actual content is changed + // therefore we set a timeout to change the href + setTimeout(function() { + var newTextContent = getTextContent(link); + if (newTextContent === textContent) { + return; + } + + // Only set href when new href looks like a valid url + if (newTextContent.match(urlRegExp)) { + link.setAttribute("href", newTextContent); + } + }, 0); + }); + }, + + _initObjectResizing: function() { + this.commands.exec("enableObjectResizing", true); + + // IE sets inline styles after resizing objects + // The following lines make sure that the width/height css properties + // are copied over to the width/height attributes + if (browser.supportsEvent("resizeend")) { + var properties = ["width", "height"], + propertiesLength = properties.length, + element = this.element; + + dom.observe(element, "resizeend", function(event) { + var target = event.target || event.srcElement, + style = target.style, + i = 0, + property; + + if (target.nodeName !== "IMG") { + return; + } + + for (; i p:first-child { margin-top: 0; }", + "._wysihtml5-temp { display: none; }", + wysihtml5.browser.isGecko ? + "body.placeholder { color: graytext !important; }" : + "body.placeholder { color: #a9a9a9 !important; }", + // Ensure that user see's broken images and can delete them + "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" + ]; + + /** + * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: + * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx + * + * Other browsers need a more hacky way: (pssst don't tell my mama) + * In order to prevent the element being scrolled into view when focusing it, we simply + * move it out of the scrollable area, focus it, and reset it's position + */ + var focusWithoutScrolling = function(element) { + if (element.setActive) { + // Following line could cause a js error when the textarea is invisible + // See https://github.com/xing/wysihtml5/issues/9 + try { element.setActive(); } catch(e) {} + } else { + var elementStyle = element.style, + originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, + originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, + originalStyles = { + position: elementStyle.position, + top: elementStyle.top, + left: elementStyle.left, + WebkitUserSelect: elementStyle.WebkitUserSelect + }; + + dom.setStyles({ + position: "absolute", + top: "-99999px", + left: "-99999px", + // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother + WebkitUserSelect: "none" + }).on(element); + + element.focus(); + + dom.setStyles(originalStyles).on(element); + + if (win.scrollTo) { + // Some browser extensions unset this method to prevent annoyances + // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 + // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 + win.scrollTo(originalScrollLeft, originalScrollTop); + } + } + }; + + + wysihtml5.views.Composer.prototype.style = function() { + var that = this, + originalActiveElement = doc.querySelector(":focus"), + textareaElement = this.textarea.element, + hasPlaceholder = textareaElement.hasAttribute("placeholder"), + originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"), + originalDisplayValue = textareaElement.style.display, + originalDisabled = textareaElement.disabled, + displayValueForCopying; + + this.focusStylesHost = HOST_TEMPLATE.cloneNode(false); + this.blurStylesHost = HOST_TEMPLATE.cloneNode(false); + this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false); + + // Remove placeholder before copying (as the placeholder has an affect on the computed style) + if (hasPlaceholder) { + textareaElement.removeAttribute("placeholder"); + } + + if (textareaElement === originalActiveElement) { + textareaElement.blur(); + } + + // enable for copying styles + textareaElement.disabled = false; + + // set textarea to display="none" to get cascaded styles via getComputedStyle + textareaElement.style.display = displayValueForCopying = "none"; + + if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") || + (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) { + textareaElement.style.display = displayValueForCopying = originalDisplayValue; + } + + // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost); + + // --------- editor styles --------- + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); + + // --------- apply standard rules --------- + dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); + + // --------- :disabled styles --------- + textareaElement.disabled = true; + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost); + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost); + textareaElement.disabled = originalDisabled; + + // --------- :focus styles --------- + textareaElement.style.display = originalDisplayValue; + focusWithoutScrolling(textareaElement); + textareaElement.style.display = displayValueForCopying; + + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); + + // reset textarea + textareaElement.style.display = originalDisplayValue; + + dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea); + + // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus + // this is needed for when the change_view event is fired where the iframe is hidden and then + // the blur event fires and re-displays it + var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); + + // --------- restore focus --------- + if (originalActiveElement) { + originalActiveElement.focus(); + } else { + textareaElement.blur(); + } + + // --------- restore placeholder --------- + if (hasPlaceholder) { + textareaElement.setAttribute("placeholder", originalPlaceholder); + } + + // --------- Sync focus/blur styles --------- + this.parent.on("focus:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea); + dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); + }); + + this.parent.on("blur:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea); + dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); + }); + + this.parent.observe("disable:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea); + dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element); + }); + + this.parent.observe("enable:composer", function() { + dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea); + dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); + }); + + return this; + }; +})(wysihtml5); +;/** + * Taking care of events + * - Simulating 'change' event on contentEditable element + * - Handling drag & drop logic + * - Catch paste events + * - Dispatch proprietary newword:composer event + * - Keyboard shortcuts + */ +(function(wysihtml5) { + var dom = wysihtml5.dom, + domNode = dom.domNode, + browser = wysihtml5.browser, + /** + * Map keyCodes to query commands + */ + shortcuts = { + "66": "bold", // B + "73": "italic", // I + "85": "underline" // U + }; + + var actions = { + + // Adds multiple eventlisteners to target, bound to one callback + // TODO: If needed elsewhere make it part of wysihtml5.dom or sth + addListeners: function (target, events, callback) { + for(var i = 0, max = events.length; i < max; i++) { + target.addEventListener(events[i], callback, false); + } + }, + + // Removes multiple eventlisteners from target, bound to one callback + // TODO: If needed elsewhere make it part of wysihtml5.dom or sth + removeListeners: function (target, events, callback) { + for(var i = 0, max = events.length; i < max; i++) { + target.removeEventListener(events[i], callback, false); + } + }, + + // Override for giving user ability to delete last line break in table cell + fixLastBrDeletionInTable: function(composer, force) { + if (composer.selection.caretIsLastInSelection()) { + var sel = composer.selection.getSelection(), + aNode = sel.anchorNode; + if (aNode && aNode.nodeType === 1 && (wysihtml5.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) { + var nextNode = aNode.childNodes[sel.anchorOffset]; + if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") { + nextNode.parentNode.removeChild(nextNode); + return true; + } + } + } + return false; + }, + + // If found an uneditable before caret then notify it before deletion + handleUneditableDeletion: function(composer) { + var before = composer.selection.getBeforeSelection(true); + if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) { + if (actions.fixLastBrDeletionInTable(composer, true)) { + return true; + } + try { + var ev = new CustomEvent("wysihtml5:uneditable:delete", {bubbles: true, cancelable: false}); + before.node.dispatchEvent(ev); + } catch (err) {} + before.node.parentNode.removeChild(before.node); + return true; + } + return false; + }, + + // Deletion with caret in the beginning of headings and other block elvel elements needs special attention + // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit) + fixDeleteInTheBeginningOfBlock: function(composer) { + var selection = composer.selection, + prevNode = selection.getPreviousNode(); + + if (selection.caretIsFirstInSelection(wysihtml5.browser.usesControlRanges()) && prevNode) { + if (prevNode.nodeType === 1 && + wysihtml5.dom.domNode(prevNode).is.block() && + !domNode(prevNode).test({ + query: "ol, ul, table, tr, dl" + }) + ) { + if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) { + // If heading is empty remove the heading node + prevNode.parentNode.removeChild(prevNode); + return true; + } else { + if (prevNode.lastChild) { + var selNode = prevNode.lastChild, + selectedNode = selection.getSelectedNode(), + commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element), + curNode = wysihtml5.dom.getParentElement(selectedNode, { + query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" + }, false, commonAncestorNode || composer.element); + + if (curNode) { + domNode(curNode).transferContentTo(prevNode, true); + selection.setAfter(selNode); + return true; + } else if (wysihtml5.browser.usesControlRanges()) { + selectedNode = selection.getCaretNode(); + domNode(selectedNode).transferContentTo(prevNode, true); + selection.setAfter(selNode); + return true; + } + } + } + } + } + return false; + }, + + /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */ + /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */ + fixDeleteInTheBeginningOfLi: function(composer) { + if (wysihtml5.browser.hasLiDeletingProblem()) { + var selection = composer.selection.getSelection(), + aNode = selection.anchorNode, + listNode, prevNode, firstNode, + isInBeginnig = composer.selection.caretIsFirstInSelection(); + + // Fix caret at the beginnig of first textNode in LI + if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) { + aNode = aNode.parentNode; + isInBeginnig = true; + } + + if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") { + prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) { + prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + } + if (prevNode) { + firstNode = aNode.firstChild; + domNode(aNode).transferContentTo(prevNode, true); + if (firstNode) { + composer.selection.setBefore(firstNode); + } else if (prevNode) { + if (prevNode.nodeType === 1) { + if (prevNode.lastChild) { + composer.selection.setAfter(prevNode.lastChild); + } else { + composer.selection.selectNode(prevNode); + } + } else { + composer.selection.setAfter(prevNode); + } + } + return true; + } + } + } + return false; + }, + + fixDeleteInTheBeginningOfControlSelection: function(composer) { + var selection = composer.selection, + prevNode = selection.getPreviousNode(), + selectedNode = selection.getSelectedNode(), + afterCaretNode; + + if (selection.caretIsFirstInSelection()) { + if (selectedNode.nodeType === 3) { + selectedNode = selectedNode.parentNode; + } + afterCaretNode = selectedNode.firstChild; + domNode(selectedNode).transferContentTo(prevNode, true); + if (afterCaretNode) { + composer.selection.setBefore(afterCaretNode); + } + return true; + } + return false; + }, + + // Table management + // If present enableObjectResizing and enableInlineTableEditing command should be called with false to prevent native table handlers + initTableHandling: function() { + var hideHandlers = function() { + window.removeEventListener('load', hideHandlers); + this.doc.execCommand("enableObjectResizing", false, "false"); + this.doc.execCommand("enableInlineTableEditing", false, "false"); + }.bind(this), + iframeInitiator = (function() { + hideHandlers.call(this); + actions.removeListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); + }).bind(this); + + if( this.doc.execCommand && + wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && + wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) + { + if (this.sandbox.getIframe) { + actions.addListeners(this.sandbox.getIframe(), ["focus", "mouseup", "mouseover"], iframeInitiator); + } else { + window.addEventListener('load', hideHandlers); + } + } + this.tableSelection = wysihtml5.quirks.tableCellsSelection(this.element, this.parent); + }, + + // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature) + // Returns true if some corrections is applied so events know when to prevent default + doLineBreaksModeEnterWithCaret: function(composer) { + var breakNodes = "p, pre, div, blockquote", + caretInfo, parent, txtNode, + ret = false; + + caretInfo = composer.selection.getNodesNearCaret(); + if (caretInfo) { + + if (caretInfo.caretNode || caretInfo.nextNode) { + parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2); + if (parent === composer.element) { + parent = undefined; + } + } + + if (parent && caretInfo.caretNode) { + if (domNode(caretInfo.caretNode).is.lineBreak()) { + + if (composer.config.doubleLineBreakEscapesBlock) { + // Double enter (enter on blank line) exits block element in useLineBreaks mode. + ret = true; + caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode); + + // Ensure surplous line breaks are not added to preceding element + if (domNode(caretInfo.nextNode).is.lineBreak()) { + caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode); + } + + var brNode = composer.doc.createElement('br'); + if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) { + parent.parentNode.insertBefore(brNode, parent.nextSibling); + } else { + composer.selection.splitElementAtCaret(parent, brNode); + } + + // Ensure surplous blank lines are not added to preceding element + if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) { + // Replaces blank lines at the beginning of textnode + caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, ''); + } + composer.selection.setBefore(brNode); + } + + } else if (caretInfo.caretNode.nodeType === 3 && wysihtml5.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) { + + // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens. + // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br + ret = true; + var br1 = composer.doc.createElement('br'), + br2 = composer.doc.createElement('br'), + f = composer.doc.createDocumentFragment(); + f.appendChild(br1); + f.appendChild(br2); + composer.selection.insertNode(f); + composer.selection.setBefore(br2); + + } + } + } + return ret; + } + }; + + var handleDeleteKeyPress = function(event, composer) { + var selection = composer.selection, + element = composer.element; + + if (selection.isCollapsed()) { + if (actions.handleUneditableDeletion(composer)) { + event.preventDefault(); + return; + } + if (actions.fixDeleteInTheBeginningOfLi(composer)) { + event.preventDefault(); + return; + } + if (actions.fixDeleteInTheBeginningOfBlock(composer)) { + event.preventDefault(); + return; + } + if (actions.fixLastBrDeletionInTable(composer)) { + event.preventDefault(); + return; + } + if (wysihtml5.browser.usesControlRanges()) { + if (actions.fixDeleteInTheBeginningOfControlSelection(composer)) { + event.preventDefault(); + return; + } + } + } else { + if (selection.containsUneditable()) { + event.preventDefault(); + selection.deleteContents(); + } + } + }; + + var handleEnterKeyPress = function(event, composer) { + if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) { + // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature) + + var breakNodes = "p, pre, div, blockquote", + caretInfo, parent, txtNode; + + if (composer.selection.isCollapsed()) { + if (actions.doLineBreaksModeEnterWithCaret(composer)) { + event.preventDefault(); + } + } + } + }; + + var handleTabKeyDown = function(composer, element, shiftKey) { + if (!composer.selection.isCollapsed()) { + composer.selection.deleteContents(); + } else if (composer.selection.caretIsInTheBeginnig('li')) { + if (shiftKey) { + if (composer.commands.exec('outdentList')) return; + } else { + if (composer.commands.exec('indentList')) return; + } + } + + // Is   close enough to tab. Could not find enough counter arguments for now. + composer.commands.exec("insertHTML", " "); + }; + + var handleDomNodeRemoved = function(event) { + if (this.domNodeRemovedInterval) { + clearInterval(domNodeRemovedInterval); + } + this.parent.fire("destroy:composer"); + }; + + // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires + var handleUserInteraction = function (event) { + this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event); + setTimeout((function() { + this.parent.fire("interaction", event).fire("interaction:composer", event); + }).bind(this), 0); + }; + + var handleFocus = function(event) { + this.parent.fire("focus", event).fire("focus:composer", event); + + // Delay storing of state until all focus handler are fired + // especially the one which resets the placeholder + setTimeout((function() { + this.focusState = this.getValue(false, false); + }).bind(this), 0); + }; + + var handleBlur = function(event) { + if (this.focusState !== this.getValue(false, false)) { + //create change event if supported (all except IE8) + var changeevent = event; + if(typeof Object.create == 'function') { + changeevent = Object.create(event, { type: { value: 'change' } }); + } + this.parent.fire("change", changeevent).fire("change:composer", changeevent); + } + this.parent.fire("blur", event).fire("blur:composer", event); + }; + + var handlePaste = function(event) { + this.parent.fire(event.type, event).fire(event.type + ":composer", event); + if (event.type === "paste") { + setTimeout((function() { + this.parent.fire("newword:composer"); + }).bind(this), 0); + } + }; + + var handleCopy = function(event) { + if (this.config.copyedFromMarking) { + // If supported the copied source can be based directly on selection + // 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. + if (wysihtml5.browser.supportsModernPaste()) { + event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml()); + event.clipboardData.setData("text/plain", this.selection.getPlainText()); + event.preventDefault(); + } + this.parent.fire(event.type, event).fire(event.type + ":composer", event); + } + }; + + var handleKeyUp = function(event) { + var keyCode = event.keyCode; + if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { + this.parent.fire("newword:composer"); + } + }; + + var handleMouseDown = function(event) { + if (!browser.canSelectImagesInContentEditable()) { + // Make sure that images are selected when clicking on them + var target = event.target, + allImages = this.element.querySelectorAll('img'), + notMyImages = this.element.querySelectorAll('.' + this.config.classNames.uneditableContainer + ' img'), + myImages = wysihtml5.lang.array(allImages).without(notMyImages); + + if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) { + this.selection.selectNode(target); + } + } + + // Saves mousedown position for IE controlSelect fix + if (wysihtml5.browser.usesControlRanges()) { + this.selection.lastMouseDownPos = {x: event.clientX, y: event.clientY}; + setTimeout(function() { + delete this.selection.lastMouseDownPos; + }.bind(this), 0); + } + }; + + // IE has this madness of control selects of overflowed and some other elements (weird box around element on selection and second click selects text) + // This fix handles the second click problem by adding cursor to the right position under cursor inside when controlSelection is made + var handleIEControlSelect = function(event) { + var target = event.target, + pos = this.selection.lastMouseDownPos; + if (pos) { + var caretPosition = document.body.createTextRange(); + setTimeout(function() { + try { + caretPosition.moveToPoint(pos.x, pos.y); + caretPosition.select(); + } catch (e) {} + }.bind(this), 0); + } + }; + + var handleClick = function(event) { + if (this.config.classNames.uneditableContainer) { + // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text) + // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour + var uneditable = wysihtml5.dom.getParentElement(event.target, { query: "." + this.config.classNames.uneditableContainer }, false, this.element); + if (uneditable) { + this.selection.setAfter(uneditable); + } + } + }; + + var handleDrop = function(event) { + if (!browser.canSelectImagesInContentEditable()) { + // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case + setTimeout((function() { + this.selection.getSelection().removeAllRanges(); + }).bind(this), 0); + } + }; + + var handleKeyDown = function(event) { + var keyCode = event.keyCode, + command = shortcuts[keyCode], + target, parent; + + // Select all (meta/ctrl + a) + if ((event.ctrlKey || event.metaKey) && !event.altKey && keyCode === 65) { + this.selection.selectAll(); + event.preventDefault(); + return; + } + + // Shortcut logic + if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { + this.commands.exec(command); + event.preventDefault(); + } + + if (keyCode === wysihtml5.BACKSPACE_KEY) { + // Delete key override for special cases + handleDeleteKeyPress(event, this); + } + + // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor + if (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY) { + target = this.selection.getSelectedNode(true); + if (target && target.nodeName === "IMG") { + event.preventDefault(); + parent = target.parentNode; + parent.removeChild(target);// delete the + // And it's parent too if it hasn't got any other child nodes + if (parent.nodeName === "A" && !parent.firstChild) { + parent.parentNode.removeChild(parent); + } + setTimeout((function() { + wysihtml5.quirks.redraw(this.element); + }).bind(this), 0); + } + } + + if (this.config.handleTabKey && keyCode === wysihtml5.TAB_KEY) { + // TAB key handling + event.preventDefault(); + handleTabKeyDown(this, this.element, event.shiftKey); + } + + if (keyCode === wysihtml5.ENTER_KEY) { + handleEnterKeyPress(event, this); + } + + }; + + var handleIframeFocus = function(event) { + setTimeout((function() { + if (this.doc.querySelector(":focus") !== this.element) { + this.focus(); + } + }).bind(this), 0); + }; + + var handleIframeBlur = function(event) { + setTimeout((function() { + this.selection.getSelection().removeAllRanges(); + }).bind(this), 0); + }; + + // Testing requires actions to be accessible from out of scope + wysihtml5.views.Composer.prototype.observeActions = actions; + + wysihtml5.views.Composer.prototype.observe = function() { + var that = this, + container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(), + element = this.element, + focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow(); + + this.focusState = this.getValue(false, false); + + // --------- destroy:composer event --------- + container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false); + + // DOMNodeRemoved event is not supported in IE 8 + // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed + if (!browser.supportsMutationEvents()) { + this.domNodeRemovedInterval = setInterval(function() { + if (!dom.contains(document.documentElement, container)) { + handleDomNodeRemoved.call(this); + } + }, 250); + } + + // --------- User interactions -- + if (this.config.handleTables) { + // If handleTables option is true, table handling functions are bound + actions.initTableHandling.call(this); + } + + actions.addListeners(focusBlurElement, ["drop", "paste", "mouseup", "focus", "keyup"], handleUserInteraction.bind(this)); + focusBlurElement.addEventListener("focus", handleFocus.bind(this), false); + focusBlurElement.addEventListener("blur", handleBlur.bind(this), false); + + actions.addListeners(this.element, ["drop", "paste", "beforepaste"], handlePaste.bind(this), false); + this.element.addEventListener("copy", handleCopy.bind(this), false); + this.element.addEventListener("mousedown", handleMouseDown.bind(this), false); + this.element.addEventListener("click", handleClick.bind(this), false); + this.element.addEventListener("drop", handleDrop.bind(this), false); + this.element.addEventListener("keyup", handleKeyUp.bind(this), false); + this.element.addEventListener("keydown", handleKeyDown.bind(this), false); + + // IE controlselect madness fix + if (wysihtml5.browser.usesControlRanges()) { + this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false); + } + + this.element.addEventListener("dragenter", (function() { + this.parent.fire("unset_placeholder"); + }).bind(this), false); + + }; +})(wysihtml5); +;/** + * Class that takes care that the value of the composer and the textarea is always in sync + */ +(function(wysihtml5) { + var INTERVAL = 400; + + wysihtml5.views.Synchronizer = Base.extend( + /** @scope wysihtml5.views.Synchronizer.prototype */ { + + constructor: function(editor, textarea, composer) { + this.editor = editor; + this.textarea = textarea; + this.composer = composer; + + this._observe(); + }, + + /** + * Sync html from composer to textarea + * Takes care of placeholders + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea + */ + fromComposerToTextarea: function(shouldParseHtml) { + this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml); + }, + + /** + * Sync value of textarea to composer + * Takes care of placeholders + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer + */ + fromTextareaToComposer: function(shouldParseHtml) { + var textareaValue = this.textarea.getValue(false, false); + if (textareaValue) { + this.composer.setValue(textareaValue, shouldParseHtml); + } else { + this.composer.clear(); + this.editor.fire("set_placeholder"); + } + }, + + /** + * Invoke syncing based on view state + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea + */ + sync: function(shouldParseHtml) { + if (this.editor.currentView.name === "textarea") { + this.fromTextareaToComposer(shouldParseHtml); + } else { + this.fromComposerToTextarea(shouldParseHtml); + } + }, + + /** + * Initializes interval-based syncing + * also makes sure that on-submit the composer's content is synced with the textarea + * immediately when the form gets submitted + */ + _observe: function() { + var interval, + that = this, + form = this.textarea.element.form, + startInterval = function() { + interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL); + }, + stopInterval = function() { + clearInterval(interval); + interval = null; + }; + + startInterval(); + + if (form) { + // If the textarea is in a form make sure that after onreset and onsubmit the composer + // has the correct state + wysihtml5.dom.observe(form, "submit", function() { + that.sync(true); + }); + wysihtml5.dom.observe(form, "reset", function() { + setTimeout(function() { that.fromTextareaToComposer(); }, 0); + }); + } + + this.editor.on("change_view", function(view) { + if (view === "composer" && !interval) { + that.fromTextareaToComposer(true); + startInterval(); + } else if (view === "textarea") { + that.fromComposerToTextarea(true); + stopInterval(); + } + }); + + this.editor.on("destroy:composer", stopInterval); + } + }); +})(wysihtml5); +;(function(wysihtml5) { + + wysihtml5.views.SourceView = Base.extend( + /** @scope wysihtml5.views.SourceView.prototype */ { + + constructor: function(editor, composer) { + this.editor = editor; + this.composer = composer; + + this._observe(); + }, + + switchToTextarea: function(shouldParseHtml) { + var composerStyles = this.composer.win.getComputedStyle(this.composer.element), + width = parseFloat(composerStyles.width), + height = Math.max(parseFloat(composerStyles.height), 100); + + if (!this.textarea) { + this.textarea = this.composer.doc.createElement('textarea'); + this.textarea.className = "wysihtml5-source-view"; + } + this.textarea.style.width = width + 'px'; + this.textarea.style.height = height + 'px'; + this.textarea.value = this.editor.getValue(shouldParseHtml, true); + this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element); + this.editor.currentView = "source"; + this.composer.element.style.display = 'none'; + }, + + switchToComposer: function(shouldParseHtml) { + var textareaValue = this.textarea.value; + if (textareaValue) { + this.composer.setValue(textareaValue, shouldParseHtml); + } else { + this.composer.clear(); + this.editor.fire("set_placeholder"); + } + this.textarea.parentNode.removeChild(this.textarea); + this.editor.currentView = this.composer; + this.composer.element.style.display = ''; + }, + + _observe: function() { + this.editor.on("change_view", function(view) { + if (view === "composer") { + this.switchToComposer(true); + } else if (view === "textarea") { + this.switchToTextarea(true); + } + }.bind(this)); + } + + }); + +})(wysihtml5); +;wysihtml5.views.Textarea = wysihtml5.views.View.extend( + /** @scope wysihtml5.views.Textarea.prototype */ { + name: "textarea", + + constructor: function(parent, textareaElement, config) { + this.base(parent, textareaElement, config); + + this._observe(); + }, + + clear: function() { + this.element.value = ""; + }, + + getValue: function(parse) { + var value = this.isEmpty() ? "" : this.element.value; + if (parse !== false) { + value = this.parent.parse(value); + } + return value; + }, + + setValue: function(html, parse) { + if (parse !== false) { + html = this.parent.parse(html); + } + this.element.value = html; + }, + + cleanUp: function(rules) { + var html = this.parent.parse(this.element.value, undefined, rules); + this.element.value = html; + }, + + hasPlaceholderSet: function() { + var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element), + placeholderText = this.element.getAttribute("placeholder") || null, + value = this.element.value, + isEmpty = !value; + return (supportsPlaceholder && isEmpty) || (value === placeholderText); + }, + + isEmpty: function() { + return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); + }, + + _observe: function() { + var element = this.element, + parent = this.parent, + eventMapping = { + focusin: "focus", + focusout: "blur" + }, + /** + * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events + * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai + */ + events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; + + parent.on("beforeload", function() { + wysihtml5.dom.observe(element, events, function(event) { + var eventName = eventMapping[event.type] || event.type; + parent.fire(eventName).fire(eventName + ":textarea"); + }); + + wysihtml5.dom.observe(element, ["paste", "drop"], function() { + setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); + }); + }); + } +}); +;/** + * WYSIHTML5 Editor + * + * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface + * @param {Object} [config] See defaultConfig object below for explanation of each individual config option + * + * @events + * load + * beforeload (for internal use only) + * focus + * focus:composer + * focus:textarea + * blur + * blur:composer + * blur:textarea + * change + * change:composer + * change:textarea + * paste + * paste:composer + * paste:textarea + * newword:composer + * destroy:composer + * undo:composer + * redo:composer + * beforecommand:composer + * aftercommand:composer + * enable:composer + * disable:composer + * change_view + */ +(function(wysihtml5) { + var undef; + + var defaultConfig = { + // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body + name: undef, + // Whether the editor should look like the textarea (by adopting styles) + style: true, + // Id of the toolbar element, pass falsey value if you don't want any toolbar logic + toolbar: undef, + // Whether toolbar is displayed after init by script automatically. + // Can be set to false if toolobar is set to display only on editable area focus + showToolbarAfterInit: true, + // With default toolbar it shows dialogs in toolbar when their related text format state becomes active (click on link in text opens link dialogue) + showToolbarDialogsOnSelection: true, + // Whether urls, entered by the user should automatically become clickable-links + autoLink: true, + // Includes table editing events and cell selection tracking + handleTables: true, + // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation + handleTabKey: true, + // Object which includes parser rules to apply when html gets cleaned + // See parser_rules/*.js for examples + parserRules: { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} }, + // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead + pasteParserRulesets: null, + // Parser method to use when the user inserts content + parser: wysihtml5.dom.parse, + // By default wysihtml5 will insert a
for line breaks, set this to false to use

+ useLineBreaks: true, + // Double enter (enter on blank line) exits block element in useLineBreaks mode. + // It enables a way of escaping out of block elements and splitting block elements + doubleLineBreakEscapesBlock: true, + // Array (or single string) of stylesheet urls to be loaded in the editor's iframe + stylesheets: [], + // Placeholder text to use, defaults to the placeholder attribute on the textarea element + placeholderText: undef, + // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5) + supportTouchDevices: true, + // Whether senseless elements (empty or without attributes) should be removed/replaced with their content + cleanUp: true, + // Whether to use div instead of secure iframe + contentEditableMode: false, + classNames: { + // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option + composer: "wysihtml5-editor", + // Class name to add to the body when the wysihtml5 editor is supported + body: "wysihtml5-supported", + // classname added to editable area element (iframe/div) on creation + sandbox: "wysihtml5-sandbox", + // class on editable area with placeholder + placeholder: "wysihtml5-placeholder", + // Classname of container that editor should not touch and pass through + uneditableContainer: "wysihtml5-uneditable-container" + }, + // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste) + // Also copied source is based directly on selection - + // (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). + // If falsy value is passed source override is also disabled + copyedFromMarking: '' + }; + + wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( + /** @scope wysihtml5.Editor.prototype */ { + constructor: function(editableElement, config) { + this.editableElement = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement; + this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get(); + this._isCompatible = wysihtml5.browser.supported(); + + // merge classNames + if (config && config.classNames) { + wysihtml5.lang.object(this.config.classNames).merge(config.classNames); + } + + if (this.editableElement.nodeName.toLowerCase() != "textarea") { + this.config.contentEditableMode = true; + this.config.noTextarea = true; + } + if (!this.config.noTextarea) { + this.textarea = new wysihtml5.views.Textarea(this, this.editableElement, this.config); + this.currentView = this.textarea; + } + + // Sort out unsupported/unwanted browsers here + if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) { + var that = this; + setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); + return; + } + + // Add class name to body, to indicate that the editor is supported + wysihtml5.dom.addClass(document.body, this.config.classNames.body); + + this.composer = new wysihtml5.views.Composer(this, this.editableElement, this.config); + this.currentView = this.composer; + + if (typeof(this.config.parser) === "function") { + this._initParser(); + } + + this.on("beforeload", this.handleBeforeLoad); + }, + + handleBeforeLoad: function() { + if (!this.config.noTextarea) { + this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); + } else { + this.sourceView = new wysihtml5.views.SourceView(this, this.composer); + } + if (this.config.toolbar) { + this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar, this.config.showToolbarAfterInit); + } + }, + + isCompatible: function() { + return this._isCompatible; + }, + + clear: function() { + this.currentView.clear(); + return this; + }, + + getValue: function(parse, clearInternals) { + return this.currentView.getValue(parse, clearInternals); + }, + + setValue: function(html, parse) { + this.fire("unset_placeholder"); + + if (!html) { + return this.clear(); + } + + this.currentView.setValue(html, parse); + return this; + }, + + cleanUp: function(rules) { + this.currentView.cleanUp(rules); + }, + + focus: function(setToEnd) { + this.currentView.focus(setToEnd); + return this; + }, + + /** + * Deactivate editor (make it readonly) + */ + disable: function() { + this.currentView.disable(); + return this; + }, + + /** + * Activate editor + */ + enable: function() { + this.currentView.enable(); + return this; + }, + + isEmpty: function() { + return this.currentView.isEmpty(); + }, + + hasPlaceholderSet: function() { + return this.currentView.hasPlaceholderSet(); + }, + + destroy: function() { + if (this.composer && this.composer.sandbox) { + this.composer.sandbox.destroy(); + } + if (this.toolbar) { + this.toolbar.destroy(); + } + this.off(); + }, + + parse: function(htmlOrElement, clearInternals, customRules) { + var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null); + var returnValue = this.config.parser(htmlOrElement, { + "rules": customRules || this.config.parserRules, + "cleanUp": this.config.cleanUp, + "context": parseContext, + "uneditableClass": this.config.classNames.uneditableContainer, + "clearInternals" : clearInternals + }); + if (typeof(htmlOrElement) === "object") { + wysihtml5.quirks.redraw(htmlOrElement); + } + return returnValue; + }, + + /** + * Prepare html parser logic + * - Observes for paste and drop + */ + _initParser: function() { + var oldHtml; + + if (wysihtml5.browser.supportsModernPaste()) { + this.on("paste:composer", function(event) { + event.preventDefault(); + oldHtml = wysihtml5.dom.getPastedHtml(event); + if (oldHtml) { + this._cleanAndPaste(oldHtml); + } + }.bind(this)); + + } else { + this.on("beforepaste:composer", function(event) { + event.preventDefault(); + var scrollPos = this.composer.getScrollPos(); + + wysihtml5.dom.getPastedHtmlWithDiv(this.composer, function(pastedHTML) { + if (pastedHTML) { + this._cleanAndPaste(pastedHTML); + } + this.composer.setScrollPos(scrollPos); + }.bind(this)); + + }.bind(this)); + } + }, + + _cleanAndPaste: function (oldHtml) { + var cleanHtml = wysihtml5.quirks.cleanPastedHTML(oldHtml, { + "referenceNode": this.composer.element, + "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}], + "uneditableClass": this.config.classNames.uneditableContainer + }); + this.composer.selection.deleteContents(); + this.composer.selection.insertHTML(cleanHtml); + } + }); +})(wysihtml5); +;/** + * Toolbar Dialog + * + * @param {Element} link The toolbar link which causes the dialog to show up + * @param {Element} container The dialog container + * + * @example + * + * insert an image + * + * + *

+ * + * + *
+ * + * + */ +(function(wysihtml5) { + var dom = wysihtml5.dom, + CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened", + SELECTOR_FORM_ELEMENTS = "input, select, textarea", + SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]", + ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field"; + + + wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend( + /** @scope wysihtml5.toolbar.Dialog.prototype */ { + constructor: function(link, container) { + this.link = link; + this.container = container; + }, + + _observe: function() { + if (this._observed) { + return; + } + + var that = this, + callbackWrapper = function(event) { + var attributes = that._serialize(); + that.fire("save", attributes); + that.hide(); + event.preventDefault(); + event.stopPropagation(); + }; + + dom.observe(that.link, "click", function() { + if (dom.hasClass(that.link, CLASS_NAME_OPENED)) { + setTimeout(function() { that.hide(); }, 0); + } + }); + + dom.observe(this.container, "keydown", function(event) { + var keyCode = event.keyCode; + if (keyCode === wysihtml5.ENTER_KEY) { + callbackWrapper(event); + } + if (keyCode === wysihtml5.ESCAPE_KEY) { + that.cancel(); + } + }); + + dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper); + + dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) { + that.cancel(); + event.preventDefault(); + event.stopPropagation(); + }); + + this._observed = true; + }, + + /** + * Grabs all fields in the dialog and puts them in key=>value style in an object which + * then gets returned + */ + _serialize: function() { + var data = {}, + fields = this.container.querySelectorAll(SELECTOR_FIELDS), + length = fields.length, + i = 0; + + for (; ifoo + * + * and we have the following dialog: + * + * + * + * after calling _interpolate() the dialog will look like this + * + * + * + * Basically it adopted the attribute values into the corresponding input fields + * + */ + _interpolate: function(avoidHiddenFields) { + var field, + fieldName, + newValue, + focusedElement = document.querySelector(":focus"), + fields = this.container.querySelectorAll(SELECTOR_FIELDS), + length = fields.length, + i = 0; + for (; i