diff --git a/README.md b/README.md
index 3450385..b5ba87a 100644
--- a/README.md
+++ b/README.md
@@ -38,16 +38,19 @@ Require it in your JS manifest's file `application.js`:
//= require wysihtml
```
-or if you need wysihtml with built-in toolbar:
+or if you also need toolbar, table editing features or all commands:
```js
-//= require wysihtml-toolbar
+//= require wysihtml
+//= require wysihtml/toolbar
+//= require wysihtml/all_commands
+//= require wysihtml/table_editing
```
Additionally include predefined `simple`, `advanced` or `advanced_unwrap` parsing rules in your `application.js`:
```js
-//= require parser_rules/advanced_unwrap
+//= require wysihtml/parser_rules/advanced_unwrap
```
Additionally include predefined `wysihtml` stiles in your `application.css.scss` file:
@@ -60,10 +63,10 @@ The simple initialise:
```html
diff --git a/lib/wysihtml/rails/version.rb b/lib/wysihtml/rails/version.rb
index a11cbab..1b7b94b 100644
--- a/lib/wysihtml/rails/version.rb
+++ b/lib/wysihtml/rails/version.rb
@@ -1,5 +1,5 @@
module Wysihtml
module Rails
- VERSION = "0.5.5"
+ VERSION = "0.6.0.beta"
end
end
diff --git a/vendor/assets/javascripts/wysihtml-toolbar.js b/vendor/assets/javascripts/wysihtml-toolbar.js
deleted file mode 100644
index 26bafae..0000000
--- a/vendor/assets/javascripts/wysihtml-toolbar.js
+++ /dev/null
@@ -1,19308 +0,0 @@
-/**
- * @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):
-
-
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 (; i0 && (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- * - * - * - * - * - *- *
- */ -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- eminem
- *- dr. dre
- *- 50 Cent
- *
at the end of inline elements and move them behind them - for (i=0; iif 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 - * - * var userHTML = 'foo bar'; - * wysihtml5.dom.parse(userHTML); - * // => 'I'm a table!' - * - * var userHTML = '
I'm a table! foobar'; - * wysihtml5.dom.parse(userHTML, { - * tags: { - * div: undefined, - * br: true - * } - * }); - * // => '' - * - * var userHTML = '
foobarfoobar'; - * 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 (; i0) { - 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
- *- *
- */ -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 - *- eminem
- *- dr. dre
- *- 50 Cent
- *- * 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- eminem
- *- dr. dre
- *- 50 Cent
- *
- * 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 ablock - * 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 - //- // becomes: - // foo
- foo
- bar
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 - //- // 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|(
- foo
- bar
))+$/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 += '
"; - 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'; - for (col = 0; col < value.cols; col ++) { - html += " '; - } - html += ""; - } - html += ' "; - }, - - 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 (; i
- * - * 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 (; ifoo = 11 - * - * Note that it sends the recorded audio to the google speech recognition api: - * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec - * - * Current HTML5 draft can be found here - * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html - * - * "Accessing Google Speech API Chrome 11" - * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ - */ -(function(wysihtml5) { - var dom = wysihtml5.dom; - - var linkStyles = { - position: "relative" - }; - - var wrapperStyles = { - left: 0, - margin: 0, - opacity: 0, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 0, - zIndex: 1 - }; - - var inputStyles = { - cursor: "inherit", - fontSize: "50px", - height: "50px", - marginTop: "-25px", - outline: 0, - padding: 0, - position: "absolute", - right: "-4px", - top: "50%" - }; - - var inputAttributes = { - "x-webkit-speech": "", - "speech": "" - }; - - wysihtml5.toolbar.Speech = function(parent, link) { - var input = document.createElement("input"); - if (!wysihtml5.browser.supportsSpeechApiOn(input)) { - link.style.display = "none"; - return; - } - var lang = parent.editor.textarea.element.getAttribute("lang"); - if (lang) { - inputAttributes.lang = lang; - } - - var wrapper = document.createElement("div"); - - wysihtml5.lang.object(wrapperStyles).merge({ - width: link.offsetWidth + "px", - height: link.offsetHeight + "px" - }); - - dom.insert(input).into(wrapper); - dom.insert(wrapper).into(link); - - dom.setStyles(inputStyles).on(input); - dom.setAttributes(inputAttributes).on(input); - - dom.setStyles(wrapperStyles).on(wrapper); - dom.setStyles(linkStyles).on(link); - - var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange"; - dom.observe(input, eventName, function() { - parent.execCommand("insertText", input.value); - input.value = ""; - }); - - dom.observe(input, "click", function(event) { - if (dom.hasClass(link, "wysihtml5-command-disabled")) { - event.preventDefault(); - } - - event.stopPropagation(); - }); - }; -})(wysihtml5); -;/** - * Toolbar - * - * @param {Object} parent Reference to instance of Editor instance - * @param {Element} container Reference to the toolbar container element - * - * @example - * - * insert link - * insert h1 - *- * - * - */ -(function(wysihtml5) { - var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled", - CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled", - CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active", - CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active", - dom = wysihtml5.dom; - - wysihtml5.toolbar.Toolbar = Base.extend( - /** @scope wysihtml5.toolbar.Toolbar.prototype */ { - constructor: function(editor, container, showOnInit) { - this.editor = editor; - this.container = typeof(container) === "string" ? document.getElementById(container) : container; - this.composer = editor.composer; - - this._getLinks("command"); - this._getLinks("action"); - - this._observe(); - if (showOnInit) { this.show(); } - - if (editor.config.classNameCommandDisabled != null) { - CLASS_NAME_COMMAND_DISABLED = editor.config.classNameCommandDisabled; - } - if (editor.config.classNameCommandsDisabled != null) { - CLASS_NAME_COMMANDS_DISABLED = editor.config.classNameCommandsDisabled; - } - if (editor.config.classNameCommandActive != null) { - CLASS_NAME_COMMAND_ACTIVE = editor.config.classNameCommandActive; - } - if (editor.config.classNameActionActive != null) { - CLASS_NAME_ACTION_ACTIVE = editor.config.classNameActionActive; - } - - var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"), - length = speechInputLinks.length, - i = 0; - for (; ielement or wrap current selection in - * toolbar.execCommand("formatBlock", "blockquote"); - */ - execCommand: function(command, commandValue) { - if (this.commandsDisabled) { - return; - } - - this._execCommand(command, commandValue); - }, - - _execCommand: function(command, commandValue) { - // Make sure that composer is focussed (false => don't move caret to the end) - this.editor.focus(false); - - this.composer.commands.exec(command, commandValue); - this._updateLinkStates(); - }, - - execAction: function(action) { - var editor = this.editor; - if (action === "change_view") { - if (editor.currentView === editor.textarea || editor.currentView === "source") { - editor.fire("change_view", "composer"); - } else { - editor.fire("change_view", "textarea"); - } - } - if (action == "showSource") { - editor.fire("showSource"); - } - }, - - _observe: function() { - var that = this, - editor = this.editor, - container = this.container, - links = this.commandLinks.concat(this.actionLinks), - length = links.length, - i = 0; - - for (; i>>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(); +wysihtml.polyfills = function(win, doc) { - return fBound; - }; - } + // TODO: in future try to replace most inline compability checks with polyfills for code readability - // closest and matches polyfill + // closest, matches, and remove polyfill // https://github.com/jonathantneal/closest (function (ELEMENT) { ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) { @@ -164,245 +77,86 @@ var wysihtml5 = { 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"; + ELEMENT.remove = ELEMENT.remove || function remove() { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; - if (method) { - this[method](token); - } + }(win.Element.prototype)); - if (force === true || force === false) { - return force; - } else { - return !result; - } - }; - classListProto.toString = function() { - return this.join(" "); - }; + if (!('classList' in doc.documentElement) && win.Object.defineProperty && typeof HTMLElement !== 'undefined') { + win.Object.defineProperty(HTMLElement.prototype, 'classList', { + get: function() { + var self = this; + function update(fn) { + return function(value) { + var classes = self.className.split(/\s+/), + index = classes.indexOf(value); - if (objCtr.defineProperty) { - var classListPropDesc = { - get: classListGetter, - enumerable: true, - configurable: true + fn(classes, index, value); + self.className = classes.join(' '); }; - 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; + var ret = { + add: update(function(classes, index, value) { + ~index || classes.push(value); + }), - for (i = 0; i < len; i++) { - token = arguments[i]; - original.call(this, token); - } - }; - }; - createMethod('add'); - createMethod('remove'); - } + remove: update(function(classes, index) { + ~index && classes.splice(index, 1); + }), - testElement.classList.toggle("c3", false); + toggle: update(function(classes, index, value) { + ~index ? classes.splice(index, 1) : classes.push(value); + }), - // 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; + contains: function(value) { + return !!~self.className.split(/\s+/).indexOf(value); + }, - win.DOMTokenList.prototype.toggle = function(token, force) { - if (1 in arguments && !this.contains(token) === !force) { - return force; - } else { - return _toggle.call(this, token); + item: function(i) { + return self.className.split(/\s+/)[i] || null; } }; - } - - testElement = null; - }()); - - } + win.Object.defineProperty(ret, 'length', { + get: function() { + return self.className.split(/\s+/).length; + } + }); + return ret; + } + }); } // 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(), + if ("createRange" in doc && "getSelection" in win) { + var e = doc.createElement('div'), + t1 = doc.createTextNode('a'), + t2 = doc.createTextNode('a'), + t3 = doc.createTextNode('a'), + r = doc.createRange(), s, ret; e.setAttribute('contenteditable', 'true'); e.appendChild(t1); e.appendChild(t2); e.appendChild(t3); - document.body.appendChild(e); + doc.body.appendChild(e); r.setStart(t2, 1); r.setEnd(t2, 1); - s = window.getSelection(); + s = win.getSelection(); s.removeAllRanges(); s.addRange(r); e.normalize(); - s = window.getSelection(); + s = win.getSelection(); ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2); e.parentNode.removeChild(e); @@ -438,7 +192,7 @@ var wysihtml5 = { }; var normalizeFix = function() { - var f = Node.prototype.normalize; + var f = win.Node.prototype.normalize; var nf = function() { var texts = getTextNodes(this), s = this.ownerDocument.defaultView.getSelection(), @@ -464,7 +218,7 @@ var wysihtml5 = { aelement = undefined; } - if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & Node.DOCUMENT_POSITION_CONTAINS))) { + if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_CONTAINS))) { fnode = [anode, anode = fnode][0]; foffset = [aoffset, aoffset = foffset][0]; } @@ -503,18 +257,18 @@ var wysihtml5 = { s.addRange(r); } }; - Node.prototype.normalize = nf; + win.Node.prototype.normalize = nf; }; var F = function() { - window.removeEventListener("load", F); - if ("Node" in window && "normalize" in Node.prototype && normalizeHasCaretError()) { + win.removeEventListener("load", F); + if ("Node" in win && "normalize" in win.Node.prototype && normalizeHasCaretError()) { normalizeFix(); } }; if (doc.readyState !== "complete") { - window.addEventListener("load", F); + win.addEventListener("load", F); } else { F(); } @@ -531,7 +285,7 @@ var wysihtml5 = { // Polyfills CustomEvent object for IE9 and up (function() { - if (!customEventSupported && "CustomEvent" in window) { + if (!customEventSupported && "CustomEvent" in win) { function CustomEvent(event, params) { params = params || {bubbles: false, cancelable: false, detail: undefined}; var evt = doc.createEvent('CustomEvent'); @@ -545,2453 +299,2584 @@ var wysihtml5 = { })(); }; -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"); - } +wysihtml.polyfills(window, document); - /*----------------------------------------------------------------------------------------------------------------*/ +/* + Base.js, version 1.1a + Copyright 2006-2010, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php +*/ - // 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; - }:*/ +var Base = function() { + // dummy +}; - function(arr, val) { - var i = arr.length; - while (i--) { - if (arr[i] === val) { - return true; - } - } - return false; - }; +Base.extend = function(_instance, _static) { // subclass + var extend = Base.prototype.extend; + + // build the prototype + Base._prototyping = true; + var proto = new this; + extend.call(proto, _instance); + proto.base = function() { + // call this method from any other method to invoke that method's ancestor + }; + delete Base._prototyping; + + // create the wrapper for the constructor function + //var constructor = proto.constructor.valueOf(); //-dean + var constructor = proto.constructor; + var klass = proto.constructor = function() { + if (!Base._prototyping) { + if (this._constructing || this.constructor == klass) { // instantiation + this._constructing = true; + constructor.apply(this, arguments); + delete this._constructing; + } else if (arguments[0] != null) { // casting + return (arguments[0].extend || extend).call(arguments[0], proto); + } + } + }; + + // build the class interface + klass.ancestor = this; + klass.extend = this.extend; + klass.forEach = this.forEach; + klass.implement = this.implement; + klass.prototype = proto; + klass.toString = this.toString; + klass.valueOf = function(type) { + //return (type == "object") ? klass : constructor; //-dean + return (type == "object") ? klass : constructor.valueOf(); + }; + extend.call(klass, _static); + // class initialisation + if (typeof klass.init == "function") klass.init(); + return klass; +}; - // 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"); - } +Base.prototype = { + extend: function(source, value) { + if (arguments.length > 1) { // extending with a name/value pair + var ancestor = this[source]; + if (ancestor && (typeof value == "function") && // overriding a method? + // the valueOf() comparison is to avoid circular references + (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && + /\bbase\b/.test(value)) { + // get the underlying method + var method = value.valueOf(); + // override + value = function() { + var previous = this.base || Base.prototype.base; + this.base = ancestor; + var returnValue = method.apply(this, arguments); + this.base = previous; + return returnValue; + }; + // point to the underlying method + value.valueOf = function(type) { + return (type == "object") ? value : method; + }; + value.toString = Base.toString; + } + this[source] = value; + } else if (source) { // extending with an object literal + var extend = Base.prototype.extend; + // if this object has a customised extend method then use it + if (!Base._prototyping && typeof this != "function") { + extend = this.extend || extend; + } + var proto = {toSource: null}; + // do the "toString" and other methods manually + var hidden = ["constructor", "toString", "valueOf"]; + // if we are prototyping then include the constructor + var i = Base._prototyping ? 0 : 1; + while (key = hidden[i++]) { + if (source[key] != proto[key]) { + extend.call(this, key, source[key]); - function parentElement(node) { - var parent = node.parentNode; - return (parent.nodeType == 1) ? parent : null; - } + } + } + // copy each of the source object's properties to this object + for (var key in source) { + if (!proto[key]) extend.call(this, key, source[key]); + } + } + return this; + } +}; - function getNodeIndex(node) { - var i = 0; - while( (node = node.previousSibling) ) { - ++i; - } - return i; - } +// initialise +Base = Base.extend({ + constructor: function() { + this.extend(arguments[0]); + } +}, { + ancestor: Object, + version: "1.1", + + forEach: function(object, block, context) { + for (var key in object) { + if (this.prototype[key] === undefined) { + block.call(context, object[key], key, object); + } + } + }, + + implement: function() { + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] == "function") { + // if it's a function, call it + arguments[i](this.prototype); + } else { + // add the interface using the extend method + this.prototype.extend(arguments[i]); + } + } + return this; + }, + + toString: function() { + return String(this.valueOf()); + } +}); +/** + * 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.1-dev + * Build date: 20 May 2015 + * + * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges) + */ +var rangy; - 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() { + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; - function getCommonAncestor(node1, node2) { - var ancestors = [], n; - for (n = node1; n; n = n.parentNode) { - ancestors.push(n); - } + // 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"]; - for (n = node2; n; n = n.parentNode) { - if (arrayContains(ancestors, n)) { - return n; - } - } + // 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"]; - return null; - } + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; - function isAncestorOf(ancestor, descendant, selfIsAncestor) { - var n = selfIsAncestor ? descendant : descendant.parentNode; - while (n) { - if (n === ancestor) { - return true; - } else { - n = n.parentNode; + // 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 false; - } + return true; + }; + } - function isOrIsAncestorOf(ancestor, descendant) { - return isAncestorOf(ancestor, descendant, 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 getClosestAncestorIn(node, ancestor, selfIsAncestor) { - var p, n = selfIsAncestor ? node : node.parentNode; - while (n) { - p = n.parentNode; - if (p === ancestor) { - return n; - } - n = p; + 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); } - return null; - } + }; - function isCharacterDataNode(node) { - var t = node.nodeType; - return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + 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.1-dev", + 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 isTextOrCommentNode(node) { - if (!node) { - return false; - } - var t = node.nodeType; - return t == 3 || t == 8 ; // Text or Comment + function consoleLog(msg) { + if (typeof console != UNDEFINED && isHostMethod(console, "log")) { + console.log(msg); } + } - function insertAfter(node, precedingNode) { - var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; - if (nextNode) { - parent.insertBefore(node, nextNode); - } else { - parent.appendChild(node); - } - return node; + function alertOrLog(msg, shouldAlert) { + if (isBrowser && shouldAlert) { + alert(msg); + } else { + consoleLog(msg); } + } - // 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); + function fail(reason) { + api.initialized = true; + api.supported = false; + alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); + } - // 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; + 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; } } - 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"); + // 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; + }; - 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"); + 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) {} } - 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"); - } + if (!toArray) { + toArray = function(arrayLike) { + var arr = []; + for (var i = 0, len = arrayLike.length; i < len; ++i) { + arr[i] = arrayLike[i]; + } + return arr; + }; } - 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"); - } + 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"); } - // This looks bad. Is it worth it? - function isWindow(obj) { - return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); + 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; - function getContentDocument(obj, module, methodName) { - var doc; + // First, perform basic feature tests - if (!obj) { - doc = document; + if (isHostMethod(document, "createRange")) { + testRange = document.createRange(); + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { + implementsDomRange = true; } + } - // 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); - } + var body = getBody(document); + if (!body || body.nodeName.toLowerCase() != "body") { + fail("No body element found"); + return; + } - // Test if the doc parameter appears to be a Window object - else if (isWindow(obj)) { - doc = obj.document; + if (body && isHostMethod(body, "createTextRange")) { + testRange = body.createTextRange(); + if (isTextRange(testRange)) { + implementsTextRange = true; } + } - if (!doc) { - throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); - } + if (!implementsDomRange && !implementsTextRange) { + fail("Neither Range nor TextRange are available"); + return; + } - return doc; + 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); + } } - function getRootContainer(node) { - var parent; - while ( (parent = node.parentNode) ) { - node = parent; + // 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); } - 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"); - } + function deprecationNotice(deprecated, replacement, module) { + if (module) { + deprecated += " in module " + module.name; + } + api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " + + replacement + " instead."); + } - // 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); + function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) { + owner[deprecated] = function() { + deprecationNotice(deprecated, replacement, module); + return owner[replacement].apply(owner, util.toArray(arguments)); + }; + } - 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; - } - } - } + 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 = []; - // 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; + api.addShimListener = function(listener) { + shimListeners.push(listener); + }; - function isBrokenNode(node) { - var n; - try { - n = node.parentNode; - return false; - } catch (e) { - return true; - } + function shim(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = shimListeners.length; i < len; ++i) { + shimListeners[i](win); } + } - (function() { - var el = document.createElement("b"); - el.innerHTML = "1"; - var textNode = el.firstChild; - el.innerHTML = "
"; - crashyTextNodes = isBrokenNode(textNode); + if (isBrowser) { + api.shim = api.createMissingNativeApi = shim; + createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); + } - api.features.crashyTextNodes = crashyTextNodes; - })(); + 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]; - 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) + "]"; + 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"); + } } - return node.nodeName; + + // 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 fragmentFromNodeChildren(node) { - var fragment = getDocument(node).createDocumentFragment(), child; - while ( (child = node.firstChild) ) { - fragment.appendChild(child); + 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); + } + } } - return fragment; - } + }); + modules[name] = newModule; + return newModule; + } - 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] : ""; - }; + 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 { - module.fail("No means of obtaining computed style properties found"); + initFunc = arguments[2]; + dependencies = arguments[1]; } - function createTestElement(doc, html, contentEditable) { - var body = getBody(doc); - var el = doc.createElement("div"); - el.contentEditable = "" + !!contentEditable; - if (html) { - el.innerHTML = html; - } + var module = createModule(name, dependencies, initFunc); - // 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); - } + // Initialize the module immediately if the core is already initialized + if (api.initialized && api.supported) { + module.init(); + } + }; - return el; + 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"); } - function removeNode(node) { - return node.parentNode.removeChild(node); + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); } - function NodeIterator(root) { - this.root = root; - this._next = root; + var el = document.createElement("div"); + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { + module.fail("Incomplete Element implementation"); } - NodeIterator.prototype = { - _current: null, + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } - hasNext: function() { - return !!this._next; - }, + 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"); + } - 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; + /*----------------------------------------------------------------------------------------------------------------*/ + + // 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 this._current; - }, - - detach: function() { - this._current = this._next = this.root = null; - } - }; + return false; + }; - function createIterator(root) { - return new NodeIterator(root); + // 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 DomPosition(node, offset) { - this.node = node; - this.offset = offset; + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; } - 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 getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + ++i; } - }; - - function DOMException(codeName) { - this.code = this[codeName]; - this.codeName = codeName; - this.message = "DOMException: " + this.codeName; + return i; } - 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; + function getNodeLength(node) { + switch (node.nodeType) { + case 7: + case 10: + return 0; + case 3: + case 8: + return node.length; + default: + return node.childNodes.length; + } + } - var removeNode = dom.removeNode; + 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; + } + } - // Utility functions + return null; + } - function isNonTextPartiallySelected(node, range) { - return (node.nodeType != 3) && - (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); + 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 getRangeDocument(range) { - return range.document || getDocument(range.startContainer); + function isOrIsAncestorOf(ancestor, descendant) { + return isAncestorOf(ancestor, descendant, true); } - function getRangeRoot(range) { - return getRootContainer(range.startContainer); + 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 getBoundaryBeforeNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node)); + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment } - function getBoundaryAfterNode(node) { - return new DomPosition(node.parentNode, getNodeIndex(node) + 1); + function isTextOrCommentNode(node) { + if (!node) { + return false; + } + var t = node.nodeType; + return t == 3 || t == 8 ; // Text or Comment } - 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); + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); } else { - n.insertBefore(node, n.childNodes[o]); + parent.appendChild(node); } - return firstNodeInserted; + return node; } - function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { - assertRangeValid(rangeA); - assertRangeValid(rangeB); + // 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); - if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { - throw new DOMException("WRONG_DOCUMENT_ERR"); + // 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; + } + } } - - 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; + return newNode; } - 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); + 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"); } - 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 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 deleteSubtree(iterator) { - var subIterator; - while (iterator.next()) { - if (iterator.isPartiallySelectedSubtree()) { - subIterator = iterator.getSubtreeIterator(); - deleteSubtree(subIterator); - subIterator.detach(); - } else { - iterator.remove(); - } + 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 extractSubtree(iterator) { - for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + 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"); + } + } - 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); + // 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; } - return frag; + + // 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 getNodesInRange(range, nodeTypes, filter) { - var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; - var filterExists = !!filter; - if (filterNodeTypes) { - regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; } + return node; + } - 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; + 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"); } - var ec = range.endContainer; - if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { - return; - } + // 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); - nodes.push(node); - }); - return nodes; + 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; + } + } + } } - 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 + ")]"; + /*----------------------------------------------------------------------------------------------------------------*/ + + // 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; + })(); + /*----------------------------------------------------------------------------------------------------------------*/ - // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + 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 RangeIterator(range, clonePartiallySelectedTextNodes) { - this.range = range; - this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + 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"); + } - if (!range.collapsed) { - this.sc = range.startContainer; - this.so = range.startOffset; - this.ec = range.endContainer; - this.eo = range.endOffset; - var root = range.commonAncestorContainer; + function createTestElement(doc, html, contentEditable) { + var body = getBody(doc); + var el = doc.createElement("div"); + el.contentEditable = "" + !!contentEditable; + if (html) { + el.innerHTML = html; + } - 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); - } + // 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; } - RangeIterator.prototype = { - _current: null, - _next: null, - _first: null, - _last: null, - isSingleCharacterDataNode: false, + function removeNode(node) { + return node.parentNode.removeChild(node); + } - reset: function() { - this._current = null; - this._next = this._first; - }, + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, 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); + 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; + }, - return current; - }, + detach: function() { + this._current = this._next = this.root = null; + } + }; - remove: function() { - var current = this._current, start, end; + function createIterator(root) { + return new NodeIterator(root); + } - 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 { - } - } + 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; }, - // Checks if the current node is partially selected - isPartiallySelectedSubtree: function() { - var current = this._current; - return isNonTextPartiallySelected(current, this.range); + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; }, - 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); + toString: function() { + return this.inspect(); + } + }; - if (isOrIsAncestorOf(current, this.sc)) { - startContainer = this.sc; - startOffset = this.so; - } - if (isOrIsAncestorOf(current, this.ec)) { - endContainer = this.ec; - endOffset = this.eo; - } + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } - updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); - } - return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); - }, + 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 + }; - detach: function() { - this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; - } + 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 + }; - 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]; + api.DOMException = DOMException; + }); - 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] ); + // 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; - function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { - if (getDocTypeNotationEntityAncestor(node, allowSelf)) { - throw new DOMException("INVALID_NODE_TYPE_ERR"); - } - } + 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; - function assertValidNodeType(node, invalidTypes) { - if (!arrayContains(invalidTypes, node.nodeType)) { - throw new DOMException("INVALID_NODE_TYPE_ERR"); - } - } + var removeNode = dom.removeNode; - 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"); - } + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); } - function assertNodeNotReadOnly(node) { - if (getReadonlyAncestor(node, true)) { - throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); - } + function getRangeDocument(range) { + return range.document || getDocument(range.startContainer); } - function assertNode(node, codeName) { - if (!node) { - throw new DOMException(codeName); - } + function getRangeRoot(range) { + return getRootContainer(range.startContainer); } - function isValidOffset(node, offset) { - return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node)); } - 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 getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, getNodeIndex(node) + 1); } - function assertRangeValid(range) { - if (!isRangeValid(range)) { - throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); + 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; } - /*----------------------------------------------------------------------------------------------------------------*/ - - // 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 - } + function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { + assertRangeValid(rangeA); + assertRangeValid(rangeB); - api.features.htmlParsingConforms = htmlParsingConforms; + if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } - var createContextualFragment = htmlParsingConforms ? + var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), + endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); - // 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); + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + } - // "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"); + 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(); } - // "Let element be as follows, depending on node's interface:" - // Document, Document Fragment: null - var el = null; + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } - // "Element: node" - if (node.nodeType == 1) { - el = node; + 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; + } + } + } + } + } - // "Text, Comment: node's parentElement" - } else if (isCharacterDataNode(node)) { - el = dom.parentElement(node); + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(); + } else { + iterator.remove(); } + } + } - // "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) - )) { + function extractSubtree(iterator) { + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { - // "let element be a new Element with "body" as its local name and the HTML - // namespace as its namespace."" - el = doc.createElement("body"); + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(); } else { - el = el.cloneNode(false); + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); } + frag.appendChild(node); + } + return frag; + } - // "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; + function getNodesInRange(range, nodeTypes, filter) { + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } - // "If this raises an exception, then abort these steps. Otherwise, let new - // children be the nodes returned." + 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; + } - // "Let fragment be a new DocumentFragment." - // "Append all new children to fragment." - // "Return fragment." - return dom.fragmentFromNodeChildren(el); - } : + var ec = range.endContainer; + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { + return; + } - // 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; + nodes.push(node); + }); + return nodes; + } - return dom.fragmentFromNodeChildren(el); - }; + 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 + ")]"; + } - function splitRangeBoundaries(range, positionsToPreserve) { - assertRangeValid(range); + /*----------------------------------------------------------------------------------------------------------------*/ - var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; - var startEndSame = (sc === ec); + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) - if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { - splitDataNode(ec, eo, positionsToPreserve); - } + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; - 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++; + + 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); } - 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; + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, - util.extend(api.rangePrototype, { - compareBoundaryPoints: function(how, range) { - assertRangeValid(this); - assertSameDocumentOrFragment(this.startContainer, range.startContainer); + reset: function() { + this._current = null; + this._next = this._first; + }, - 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); + hasNext: function() { + return !!this._next; }, - insertNode: function(node) { - assertRangeValid(this); - assertValidNodeType(node, insertableNodeTypes); - assertNodeNotReadOnly(this.startContainer); + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; - if (isOrIsAncestorOf(node, this.startContainer)) { - throw new DOMException("HIERARCHY_REQUEST_ERR"); + // 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); + } + } } - // 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); + return current; }, - cloneContents: function() { - assertRangeValid(this); + remove: function() { + var current = this._current, start, end; - var clone, frag; - if (this.collapsed) { - return getRangeDocument(this).createDocumentFragment(); + 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 (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; + if (current.parentNode) { + removeNode(current); } 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; + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); }, - surroundContents: function(node) { - assertValidNodeType(node, surroundNodeTypes); + 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 (!this.canSurroundContents()) { - throw new DOMException("INVALID_STATE_ERR"); + 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); + }, - // Extract the contents - var content = this.extractContents(); + detach: function() { + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; - // Clear the children of the node - if (node.hasChildNodes()) { - while (node.lastChild) { - node.removeChild(node.lastChild); + /*----------------------------------------------------------------------------------------------------------------*/ + + 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; + }; + } - // Insert the new node and add the extracted contents - insertNodeAtPosition(node, this.startContainer, this.startOffset); - node.appendChild(content); + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); - this.selectNode(node); - }, + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } - 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; - }, + function assertValidNodeType(node, invalidTypes) { + if (!arrayContains(invalidTypes, node.nodeType)) { + throw new DOMException("INVALID_NODE_TYPE_ERR"); + } + } - 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(""); - } - }, + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } - // The methods below are all non-standard. The following batch were introduced by Mozilla but have since - // been removed from Mozilla. + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } - compareNode: function(node) { - assertRangeValid(this); + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } - var parent = node.parentNode; - var nodeIndex = getNodeIndex(node); + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } - if (!parent) { - throw new DOMException("NOT_FOUND_ERR"); - } + function isValidOffset(node, offset) { + return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); + } - var startComparison = this.comparePoint(parent, nodeIndex), - endComparison = this.comparePoint(parent, nodeIndex + 1); + 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)); + } - if (startComparison < 0) { // Node starts before - return (endComparison > 0) ? n_b_a : n_b; - } else { - return (endComparison > 0) ? n_a : n_i; - } - }, + function assertRangeValid(range) { + if (!isRangeValid(range)) { + throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); + } + } - 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; - }, + // 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 + } - createContextualFragment: createContextualFragment, + api.features.htmlParsingConforms = htmlParsingConforms; - toHtml: function() { - return rangeToHtml(this); - }, + var createContextualFragment = htmlParsingConforms ? - // 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; - } + // 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); - var parent = node.parentNode, offset = getNodeIndex(node); - if (!parent) { - return true; + // "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"); } - 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; - }, + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; - isPointInRange: function(node, offset) { - assertRangeValid(this); - assertNode(node, "HIERARCHY_REQUEST_ERR"); - assertSameDocumentOrFragment(node, this.startContainer); + // "Element: node" + if (node.nodeType == 1) { + el = node; - return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && - (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); - }, + // "Text, Comment: node's parentElement" + } else if (isCharacterDataNode(node)) { + el = dom.parentElement(node); + } - // The methods below are non-standard and invented by me. + // "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) + )) { - // Sharing a boundary start-to-end or end-to-start does not count as intersection. - intersectsRange: function(range) { - return rangesIntersect(this, range, false); - }, + // "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); + } - // Sharing a boundary start-to-end or end-to-start does count as intersection. - intersectsOrTouchesRange: function(range) { - return rangesIntersect(this, range, true); - }, + // "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; - 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); + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." - 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; - }, + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : - 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"); - } - }, + // 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; - containsNode: function(node, allowPartial) { - if (allowPartial) { - return this.intersectsNode(node, false); - } else { - return this.compareNode(node) == n_i; - } - }, + return dom.fragmentFromNodeChildren(el); + }; - containsNodeContents: function(node) { - return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; - }, + function splitRangeBoundaries(range, positionsToPreserve) { + assertRangeValid(range); - containsRange: function(range) { - var intersection = this.intersection(range); - return intersection !== null && range.equals(intersection); - }, + var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; + var startEndSame = (sc === ec); - 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); + 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); + } - getNodes: function(nodeTypes, filter) { - assertRangeValid(this); - return getNodesInRange(this, nodeTypes, filter); - }, + function rangeToHtml(range) { + assertRangeValid(range); + var container = range.commonAncestorContainer.parentNode.cloneNode(false); + container.appendChild( range.cloneContents() ); + return container.innerHTML; + } - getDocument: function() { - return getRangeDocument(this); - }, + /*----------------------------------------------------------------------------------------------------------------*/ - collapseBefore: function(node) { - this.setEndBefore(node); - this.collapse(false); - }, + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; - collapseAfter: function(node) { - this.setStartAfter(node); - this.collapse(true); + 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); }, - 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; + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (isOrIsAncestorOf(node, this.startContainer)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); } - return { - start: start, - end: end, - containerNode: containerNode - }; + // 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); }, - 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; + cloneContents: function() { + assertRangeValid(this); - 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; + 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 { - childNodes = node.childNodes; - i = childNodes.length; - while (i--) { - nodeStack.push(childNodes[i]); - } + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); } + return clone; } }, - getName: function() { - return "DomRange"; - }, + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); - equals: function(range) { - return Range.rangesEqual(this, range); + // 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; }, - isValid: function() { - return isRangeValid(this); - }, + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); - inspect: function() { - return inspect(this); - }, + if (!this.canSurroundContents()) { + throw new DOMException("INVALID_STATE_ERR"); + } - detach: function() { - // In DOM4, detach() is now a no-op. - } - }); + // Extract the contents + var content = this.extractContents(); - function copyComparisonConstantsToObject(obj) { - obj.START_TO_START = s2s; - obj.START_TO_END = s2e; - obj.END_TO_END = e2e; - obj.END_TO_START = e2s; + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } - obj.NODE_BEFORE = n_b; - obj.NODE_AFTER = n_a; - obj.NODE_BEFORE_AND_AFTER = n_b_a; - obj.NODE_INSIDE = n_i; - } + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); - function copyComparisonConstants(constructor) { - copyComparisonConstantsToObject(constructor); - copyComparisonConstantsToObject(constructor.prototype); - } + this.selectNode(node); + }, - function createRangeContentRemover(remover, boundaryUpdater) { - return function() { + 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; + }, - 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; + 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(""); } + }, - // Check none of the range is read-only - iterateSubtree(iterator, assertNodeNotReadOnly); + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. - iterator.reset(); + compareNode: function(node) { + assertRangeValid(this); - // Remove the content - var returnValue = remover(iterator); - iterator.detach(); + var parent = node.parentNode; + var nodeIndex = getNodeIndex(node); - // Move to the new position - boundaryUpdater(this, sc, so, sc, so); + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } - return returnValue; - }; - } + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); - function createPrototypeRange(constructor, boundaryUpdater) { - function createBeforeAfterNodeSetter(isBefore, isStart) { - return function(node) { - assertValidNodeType(node, beforeAfterNodeTypes); - assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, - var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); - (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); - }; - } + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); - 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); + 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; + }, - 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); - } - } + createContextualFragment: createContextualFragment, - // Set up inheritance - var F = function() {}; - F.prototype = api.rangePrototype; - constructor.prototype = new F(); + toHtml: function() { + return rangeToHtml(this); + }, - util.extend(constructor.prototype, { - setStart: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); + // 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; + } - setRangeStart(this, node, offset); - }, + var parent = node.parentNode, offset = getNodeIndex(node); + if (!parent) { + return true; + } - setEnd: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); + var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); - setRangeEnd(this, node, offset); - }, + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, - /** - * 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; + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); - switch (args.length) { - case 3: - eo = args[2]; - break; - case 4: - ec = args[2]; - eo = args[3]; - break; - } + return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, - boundaryUpdater(this, sc, so, ec, eo); - }, + // The methods below are non-standard and invented by me. - setBoundary: function(node, offset, isStart) { - this["set" + (isStart ? "Start" : "End")](node, offset); - }, + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range) { + return rangesIntersect(this, range, false); + }, - setStartBefore: createBeforeAfterNodeSetter(true, true), - setStartAfter: createBeforeAfterNodeSetter(false, true), - setEndBefore: createBeforeAfterNodeSetter(true, false), - setEndAfter: createBeforeAfterNodeSetter(false, false), + // Sharing a boundary start-to-end or end-to-start does count as intersection. + intersectsOrTouchesRange: function(range) { + return rangesIntersect(this, range, true); + }, - 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); - } - }, + 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); - selectNodeContents: function(node) { - assertNoDocTypeNotationEntityAncestor(node, true); + 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; + }, - boundaryUpdater(this, node, 0, node, getNodeLength(node)); - }, + 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"); + } + }, - selectNode: function(node) { - assertNoDocTypeNotationEntityAncestor(node, false); - assertValidNodeType(node, beforeAfterNodeTypes); + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, - var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); - boundaryUpdater(this, start.node, start.offset, end.node, end.offset); - }, + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; + }, - extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + containsRange: function(range) { + var intersection = this.intersection(range); + return intersection !== null && range.equals(intersection); + }, - deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + 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); + } + }, - canSurroundContents: function() { - assertRangeValid(this); - assertNodeNotReadOnly(this.startContainer); - assertNodeNotReadOnly(this.endContainer); + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, - // 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; - }, + getDocument: function() { + return getRangeDocument(this); + }, - splitBoundaries: function() { - splitRangeBoundaries(this); - }, + collapseBefore: function(node) { + this.setEndBefore(node); + this.collapse(false); + }, - splitBoundariesPreservingPositions: function(positionsToPreserve) { - splitRangeBoundaries(this, positionsToPreserve); - }, + collapseAfter: function(node) { + this.setStartAfter(node); + this.collapse(true); + }, - normalizeBoundaries: function() { - assertRangeValid(this); + 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; + } - var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + return { + start: start, + end: end, + containerNode: containerNode + }; + }, - 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); - } - }; + 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; - 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--; - } - } + 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; } - }; - - 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; - } + if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { + this.setEnd(node, bookmark.end - charIndex); + stop = true; } + charIndex = nextCharIndex; } 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); - } - } + childNodes = node.childNodes; + i = childNodes.length; + while (i--) { + nodeStack.push(childNodes[i]); } - } else { - sc = ec; - so = eo; } + } + }, - boundaryUpdater(this, sc, so, ec, eo); - }, + getName: function() { + return "DomRange"; + }, - collapseToPoint: function(node, offset) { - assertNoDocTypeNotationEntityAncestor(node, true); - assertValidOffset(node, offset); - this.setStartAndEnd(node, offset); - } - }); + equals: function(range) { + return Range.rangesEqual(this, range); + }, - copyComparisonConstants(constructor); - } + isValid: function() { + return isRangeValid(this); + }, - /*----------------------------------------------------------------------------------------------------------------*/ + inspect: function() { + return inspect(this); + }, - // 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); - } + detach: function() { + // In DOM4, detach() is now a no-op. + } + }); - function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { - range.startContainer = startContainer; - range.startOffset = startOffset; - range.endContainer = endContainer; - range.endOffset = endOffset; - range.document = dom.getDocument(startContainer); + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; - updateCollapsedAndCommonAncestor(range); + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; } - function Range(doc) { - this.startContainer = doc; - this.startOffset = 0; - this.endContainer = doc; - this.endOffset = 0; - this.document = doc; - updateCollapsedAndCommonAncestor(this); + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); } - createPrototypeRange(Range, updateBoundaries); + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); - 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; - } - }); + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; - 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; + 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(); - 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 + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); - (function() { - var rangeProto; - var rangeProperties = DomRange.rangeProperties; + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); - function updateRangeProperties(range) { - var i = rangeProperties.length, prop; - while (i--) { - prop = rangeProperties[i]; - range[prop] = range.nativeRange[prop]; + 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; } - // Fix for broken collapsed property in IE 9. - range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + boundaryUpdater(range, node, offset, ec, eo); } + } - 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); + 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); } + } - var createBeforeAfterNodeSetter; + // Set up inheritance + var F = function() {}; + F.prototype = api.rangePrototype; + constructor.prototype = new F(); - WrappedRange = function(range) { - if (!range) { - throw module.createError("WrappedRange: Range must be specified"); - } - this.nativeRange = range; - updateRangeProperties(this); - }; + util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - DomRange.createPrototypeRange(WrappedRange, updateNativeRange); + setRangeStart(this, node, offset); + }, - rangeProto = WrappedRange.prototype; + setEnd: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); - rangeProto.selectNode = function(node) { - this.nativeRange.selectNode(node); - updateRangeProperties(this); - }; + setRangeEnd(this, node, offset); + }, - rangeProto.cloneContents = function() { - return this.nativeRange.cloneContents(); - }; + /** + * 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; - // 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. + switch (args.length) { + case 3: + eo = args[2]; + break; + case 4: + ec = args[2]; + eo = args[3]; + break; + } - rangeProto.surroundContents = function(node) { - this.nativeRange.surroundContents(node); - updateRangeProperties(this); - }; + boundaryUpdater(this, sc, so, ec, eo); + }, - rangeProto.collapse = function(isStart) { - this.nativeRange.collapse(isStart); - updateRangeProperties(this); - }; + setBoundary: function(node, offset, isStart) { + this["set" + (isStart ? "Start" : "End")](node, offset); + }, - rangeProto.cloneRange = function() { - return new WrappedRange(this.nativeRange.cloneRange()); - }; + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), - rangeProto.refresh = function() { - updateRangeProperties(this); - }; + 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); + } + }, - rangeProto.toString = function() { - return this.nativeRange.toString(); - }; + selectNodeContents: function(node) { + assertNoDocTypeNotationEntityAncestor(node, true); - // Create test range and node for feature detection + boundaryUpdater(this, node, 0, node, getNodeLength(node)); + }, - var testTextNode = document.createTextNode("test"); - getBody(document).appendChild(testTextNode); - var range = document.createRange(); + 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); + }, - // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and - // correct for it + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), - range.setStart(testTextNode, 0); - range.setEnd(testTextNode, 0); + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), - try { - range.setStart(testTextNode, 1); + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); - rangeProto.setStart = function(node, offset) { - this.nativeRange.setStart(node, offset); - updateRangeProperties(this); - }; + // 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; + }, - rangeProto.setEnd = function(node, offset) { - this.nativeRange.setEnd(node, offset); - updateRangeProperties(this); - }; + splitBoundaries: function() { + splitRangeBoundaries(this); + }, - createBeforeAfterNodeSetter = function(name) { - return function(node) { - this.nativeRange[name](node); - updateRangeProperties(this); - }; - }; + splitBoundariesPreservingPositions: function(positionsToPreserve) { + splitRangeBoundaries(this, positionsToPreserve); + }, - } catch(ex) { + normalizeBoundaries: function() { + assertRangeValid(this); - 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); - }; + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; - rangeProto.setEnd = function(node, offset) { - try { - this.nativeRange.setEnd(node, offset); - } catch (ex) { - this.nativeRange.setStart(node, offset); - this.nativeRange.setEnd(node, offset); + 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); } - updateRangeProperties(this); }; - createBeforeAfterNodeSetter = function(name, oppositeName) { - return function(node) { - try { - this.nativeRange[name](node); - } catch (ex) { - this.nativeRange[oppositeName](node); - this.nativeRange[name](node); + 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--; + } } - updateRangeProperties(this); - }; + } }; - } - rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); - rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); - rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); - rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); - - /*--------------------------------------------------------------------------------------------------------*/ + var normalizeStart = true; + var sibling; - // 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)); - }; + 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; + } - // 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 + boundaryUpdater(this, sc, so, ec, eo); + }, - range.selectNodeContents(testTextNode); - range.setEnd(testTextNode, 3); + collapseToPoint: function(node, offset) { + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + this.setStartAndEnd(node, offset); + } + }); - var range2 = document.createRange(); - range2.selectNodeContents(testTextNode); - range2.setEnd(testTextNode, 4); - range2.setStart(testTextNode, 2); + copyComparisonConstants(constructor); + } - 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); - }; - } + // 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); - // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. + updateCollapsedAndCommonAncestor(range); + } - var el = document.createElement("div"); - el.innerHTML = "123"; - var textNode = el.firstChild; - var body = getBody(document); - body.appendChild(el); + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this.document = doc; + updateCollapsedAndCommonAncestor(this); + } - range.setStart(textNode, 1); - range.setEnd(textNode, 2); - range.deleteContents(); + createPrototypeRange(Range, updateBoundaries); - 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); - }; + 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; + } + }); - rangeProto.extractContents = function() { - var frag = this.nativeRange.extractContents(); - updateRangeProperties(this); - return frag; - }; - } else { - } + api.DomRange = Range; + }); - body.removeChild(el); - body = null; + /*----------------------------------------------------------------------------------------------------------------*/ - /*--------------------------------------------------------------------------------------------------------*/ + // 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; - // 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); + 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 - rangeProto.getName = function() { - return "WrappedRange"; - }; + (function() { + var rangeProto; + var rangeProperties = DomRange.rangeProperties; - api.WrappedRange = WrappedRange; + 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"); @@ -3336,10 +3221,10 @@ wysihtml5.polyfills(window, document); } 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) { @@ -4359,2360 +4244,2201 @@ wysihtml5.polyfills(window, document); 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) { +(function(wysihtml) { 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"]), + IGNORE_URLS_IN = wysihtml.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]), /** * revision 1: * /(\S+\.{1}[^\s\,\.\!]+)/g @@ -7556,9 +7287,9 @@ wysihtml5.browser = (function() { * for the given document object */ function _getTempElement(context) { - var tempElement = context._wysihtml5_tempElement; + var tempElement = context._wysihtml_tempElement; if (!tempElement) { - tempElement = context._wysihtml5_tempElement = context.createElement("div"); + tempElement = context._wysihtml_tempElement = context.createElement("div"); } return tempElement; } @@ -7568,7 +7299,7 @@ wysihtml5.browser = (function() { */ function _wrapMatchesInNode(textNode) { var parentNode = textNode.parentNode, - nodeValue = wysihtml5.lang.string(textNode.data).escapeHTML(), + nodeValue = wysihtml.lang.string(textNode.data).escapeHTML(), tempElement = _getTempElement(parentNode.ownerDocument); // We need to insert an empty/temporary to fix IE quirks @@ -7588,7 +7319,7 @@ wysihtml5.browser = (function() { while (node.parentNode) { node = node.parentNode; nodeName = node.nodeName; - if (node.className && wysihtml5.lang.array(node.className.split(' ')).contains(ignoreInClasses)) { + if (node.className && wysihtml.lang.array(node.className.split(' ')).contains(ignoreInClasses)) { return true; } if (IGNORE_URLS_IN.contains(nodeName)) { @@ -7605,16 +7336,16 @@ wysihtml5.browser = (function() { return; } - if (element.className && wysihtml5.lang.array(element.className.split(' ')).contains(ignoreInClasses)) { + if (element.className && wysihtml.lang.array(element.className.split(' ')).contains(ignoreInClasses)) { return; } - if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) { + if (element.nodeType === wysihtml.TEXT_NODE && element.data.match(URL_REG_EXP)) { _wrapMatchesInNode(element); return; } - var childNodes = wysihtml5.lang.array(element.childNodes).get(), + var childNodes = wysihtml.lang.array(element.childNodes).get(), childNodesLength = childNodes.length, i = 0; @@ -7625,13 +7356,14 @@ wysihtml5.browser = (function() { return element; } - wysihtml5.dom.autoLink = autoLink; + wysihtml.dom.autoLink = autoLink; // Reveal url reg exp to the outside - wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; -})(wysihtml5); -;(function(wysihtml5) { - var api = wysihtml5.dom; + wysihtml.dom.autoLink.URL_REG_EXP = URL_REG_EXP; +})(wysihtml); + +(function(wysihtml) { + var api = wysihtml.dom; api.addClass = function(element, className) { var classList = element.classList; @@ -7662,62 +7394,196 @@ wysihtml5.browser = (function() { var elementClassName = element.className; return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }; -})(wysihtml5); -;wysihtml5.dom.contains = (function() { +})(wysihtml); + +wysihtml.dom.compareDocumentPosition = (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) { + if (documentElement.compareDocumentPosition) { return function(container, element) { - // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition - return !!(container.compareDocumentPosition(element) & 16); + return container.compareDocumentPosition(element); }; - } -})(); -;/** - * 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- * - * - * - * - * - *- *
- */ -wysihtml5.dom.convertToList = (function() { - function _createListItem(doc, list) { - var listItem = doc.createElement("li"); - list.appendChild(listItem); - return listItem; - } + } 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; - function _createList(doc, type) { - return doc.createElement(type); - } + 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 && wysihtml.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 && wysihtml.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 = wysihtml.lang.array(parents).indexOf( point ); + if( location_index !== -1) { + var smallest_common_ancestor = parents[ location_index ]; + var this_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] ); + var other_index = wysihtml.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; + }; + } +})(); + +wysihtml.dom.contains = (function() { + var documentElement = document.documentElement; + if (documentElement.contains) { + return function(container, element) { + if (element.nodeType !== wysihtml.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); + }; + } +})(); + +(function(wysihtml) { + var doc = document; + wysihtml.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 || wysihtml.EMPTY_FUNCTION; + this.config = wysihtml.lang.object({}).merge(config).get(); + if (!this.config.className) { + this.config.className = "wysihtml-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 + " wysihtml-sandbox" : "wysihtml-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 ''; + } + + }); +})(wysihtml); + +/** + * 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- eminem
- *- dr. dre
- *- 50 Cent
- *
+ * dr. dre + *50 Cent+ * + * + * + * + * + *+ *
+ */ +wysihtml.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") { @@ -7743,25 +7609,25 @@ wysihtml5.dom.convertToList = (function() { for (i=0; i- eminem
+ *- dr. dre
+ *- 50 Cent
+ *if empty, otherwise create a new one currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; currentListItem.appendChild(childNode); @@ -7788,7 +7654,8 @@ wysihtml5.dom.convertToList = (function() { return convertToList; })(); -;/** + +/** * Copy a set of attributes from one element to another * * @param {Array} attributesToCopy List of attributes which should be copied @@ -7800,10 +7667,10 @@ wysihtml5.dom.convertToList = (function() { * 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); + * wysihtml.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); * */ -wysihtml5.dom.copyAttributes = function(attributesToCopy) { +wysihtml.dom.copyAttributes = function(attributesToCopy) { return { from: function(elementToCopyFrom) { return { @@ -7823,7 +7690,8 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } }; }; -;/** + +/** * Copy a set of styles from one element to another * Please note that this only works properly across browsers when the element from which to copy the styles * is in the dom @@ -7839,7 +7707,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { * var textarea = document.querySelector("textarea"), * div = document.querySelector("div[contenteditable=true]"), * anotherDiv = document.querySelector("div.preview"); - * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); + * wysihtml.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); * */ (function(dom) { @@ -7874,7 +7742,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { return { from: function(element) { if (shouldIgnoreBoxSizingBorderBox(element)) { - stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); + stylesToCopy = wysihtml.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); } var cssText = "", @@ -7895,17 +7763,18 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } }; }; -})(wysihtml5.dom); -;/** +})(wysihtml.dom); + +/** * Event Delegation * * @example - * wysihtml5.dom.delegate(document.body, "a", "click", function() { + * wysihtml.dom.delegate(document.body, "a", "click", function() { * // foo * }); */ -(function(wysihtml5) { - wysihtml5.dom.delegate = function(container, selector, eventName, handler) { +(function(wysihtml) { + wysihtml.dom.delegate = function(container, selector, eventName, handler) { var callback = function(event) { var target = event.target, element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes @@ -7925,9 +7794,10 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } }; }; -})(wysihtml5); -;// TODO: Refactor dom tree traversing here -(function(wysihtml5) { +})(wysihtml); + +// TODO: Refactor dom tree traversing here +(function(wysihtml) { // Finds parents of a node, returning the outermost node first in Array // if contain node is given parents search is stopped at the container @@ -7942,15 +7812,15 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { return nodes; } - wysihtml5.dom.domNode = function(node) { - var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE]; + wysihtml.dom.domNode = function(node) { + var defaultNodeTypes = [wysihtml.ELEMENT_NODE, wysihtml.TEXT_NODE]; return { is: { emptyTextNode: function(ignoreWhitespace) { var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g); - return node && node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data); + return node && node.nodeType === wysihtml.TEXT_NODE && (regx).test(node.data); }, // 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) @@ -7959,7 +7829,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { }, visible: function() { - var isVisible = !(/^\s*$/g).test(wysihtml5.dom.getTextContent(node)); + var isVisible = !(/^\s*$/g).test(wysihtml.dom.getTextContent(node)); if (!isVisible) { if (node.nodeType === 1 && node.querySelector('img, br, hr, object, embed, canvas, input, textarea')) { @@ -7978,13 +7848,13 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { // In most cases browsers should solve the cases for you when you try to insert content into those, // but IE does not and it is not nice to do so anyway. voidElement: function() { - return wysihtml5.dom.domNode(node).test({ - query: wysihtml5.VOID_ELEMENTS + return wysihtml.dom.domNode(node).test({ + query: wysihtml.VOID_ELEMENTS }); } }, - // var node = wysihtml5.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); + // var node = wysihtml.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); prev: function(options) { var prevNode = node.previousSibling, types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes; @@ -7994,17 +7864,17 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if ( - wysihtml5.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) - (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check. - (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set + wysihtml.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) + (!wysihtml.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check. + (options && options.ignoreBlankTexts && wysihtml.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set ) { - return wysihtml5.dom.domNode(prevNode).prev(options); + return wysihtml.dom.domNode(prevNode).prev(options); } return prevNode; }, - // var node = wysihtml5.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true}); + // var node = wysihtml.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true}); next: function(options) { var nextNode = node.nextSibling, types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes; @@ -8014,11 +7884,11 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if ( - wysihtml5.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) - (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check. - (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set + wysihtml.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) + (!wysihtml.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check. + (options && options.ignoreBlankTexts && wysihtml.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set ) { - return wysihtml5.dom.domNode(nextNode).next(options); + return wysihtml.dom.domNode(nextNode).next(options); } return nextNode; @@ -8027,7 +7897,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { // Finds the common acnestor container of two nodes // If container given stops search at the container // If no common ancestor found returns null - // var node = wysihtml5.dom.domNode(element).commonAncestor(node2, container); + // var node = wysihtml.dom.domNode(element).commonAncestor(node2, container); commonAncestor: function(node2, container) { var parents1 = parents(node, container), parents2 = parents(node2, container); @@ -8068,24 +7938,24 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { // Returns if element is of of options.leafClasses leaf if (options && options.leafClasses) { for (var i = options.leafClasses.length; i--;) { - if (wysihtml5.dom.hasClass(node, options.leafClasses[i])) { + if (wysihtml.dom.hasClass(node, options.leafClasses[i])) { return node; } } } - return wysihtml5.dom.domNode(lastChild).lastLeafNode(options); + return wysihtml.dom.domNode(lastChild).lastLeafNode(options); }, // Splits element at childnode and extracts the childNode out of the element context // Example: - // var node = wysihtml5.dom.domNode(node).escapeParent(parentNode); + // var node = wysihtml.dom.domNode(node).escapeParent(parentNode); escapeParent: function(element, newWrapper) { var parent, split2, nodeWrap, curNode = node; // Stop if node is not a descendant of element - if (!wysihtml5.dom.contains(element, node)) { + if (!wysihtml.dom.contains(element, node)) { throw new Error("Child is not a descendant of node."); } @@ -8140,7 +8010,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { transferContentTo: function(targetNode, removeOldWrapper) { if (node.nodeType === 1) { - if (wysihtml5.dom.domNode(targetNode).is.voidElement() || targetNode.nodeType === 3) { + if (wysihtml.dom.domNode(targetNode).is.voidElement() || targetNode.nodeType === 3) { while (node.lastChild) { targetNode.parentNode.insertBefore(node.lastChild, targetNode.nextSibling); } @@ -8153,7 +8023,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { node.parentNode.removeChild(node); } } else if (node.nodeType === 3 || node.nodeType === 8){ - if (wysihtml5.dom.domNode(targetNode).is.voidElement()) { + if (wysihtml.dom.domNode(targetNode).is.voidElement()) { targetNode.parentNode.insertBefore(node, targetNode.nextSibling); } else { targetNode.appendChild(node); @@ -8178,12 +8048,12 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } Example: - var node = wysihtml5.dom.domNode(element).test({}) + var node = wysihtml.dom.domNode(element).test({}) */ test: function(properties) { var prop; - // retuern false if properties object is not defined + // return false if properties object is not defined if (!properties) { return false; } @@ -8221,7 +8091,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { 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]); + prop = wysihtml.browser.fixStyleKey(styles[j]); if (node.style[prop]) { if (properties.styleValue) { // Style value as additional parameter @@ -8256,7 +8126,7 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { } if (properties.attribute) { - var attr = wysihtml5.dom.getAttributes(node), + var attr = wysihtml.dom.getAttributes(node), attrList = [], hasOneAttribute = false; @@ -8291,8 +8161,9 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { }; }; -})(wysihtml5); -;/** +})(wysihtml); + +/** * Returns the given html wrapped in a div element * * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly @@ -8302,9 +8173,9 @@ wysihtml5.dom.copyAttributes = function(attributesToCopy) { * @param {Obejct} [context] Document object of the context the html belongs to * * @example - * wysihtml5.dom.getAsDom(" foo "); + * wysihtml.dom.getAsDom("foo "); */ -wysihtml5.dom.getAsDom = (function() { +wysihtml.dom.getAsDom = (function() { var _innerHTMLShiv = function(html, context) { var tempElement = context.createElement("div"); @@ -8320,13 +8191,13 @@ wysihtml5.dom.getAsDom = (function() { * 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) { + if (context._wysihtml_supportsHTML5Tags) { return; } for (var i=0, length=HTML5_ELEMENTS.length; i"1" in IE + * + * Therefore we have to check the element's outerHTML for the attribute +*/ + +wysihtml.dom.getAttribute = function(node, attributeName) { + var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(); + attributeName = attributeName.toLowerCase(); + var nodeName = node.nodeName; + if (nodeName == "IMG" && attributeName == "src" && wysihtml.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 +*/ + +wysihtml.dom.getAttributes = function(node) { + var HAS_GET_ATTRIBUTE_BUG = !wysihtml.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" && wysihtml.dom.isLoadedImage(node) === true) { + attributes['src'] = node.src; + } else if (wysihtml.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; +}; + +/** * Walks the dom tree from the given node up until it finds a match * * @param {Element} node The from which to check the parent nodes @@ -8373,12 +8311,12 @@ wysihtml5.dom.getAsDom = (function() { * @return {null|Element} Returns the first element that matched the desiredNodeName(s) */ -wysihtml5.dom.getParentElement = (function() { +wysihtml.dom.getParentElement = (function() { return function(node, properties, levels, container) { levels = levels || 50; while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) { - if (wysihtml5.dom.domNode(node).test(properties)) { + if (wysihtml.dom.domNode(node).test(properties)) { return node; } node = node.parentNode; @@ -8387,32 +8325,85 @@ wysihtml5.dom.getParentElement = (function() { }; })(); -;/** - * Get element's style for a specific css property - * - * @param {Element} element The element on which to retrieve the style - * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) - * - * @example - * wysihtml5.dom.getStyle("display").from(document.body); - * // => "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(); - }); +/* + * 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. +**/ +wysihtml.dom.getPastedHtml = function(event) { + var html; + if (wysihtml.browser.supportsModernPaste() && event.clipboardData) { + if (wysihtml.lang.array(event.clipboardData.types).contains('text/html')) { + html = event.clipboardData.getData('text/html'); + } else if (wysihtml.lang.array(event.clipboardData.types).contains('text/plain')) { + html = wysihtml.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true); + } } + return html; +}; - return function(property) { +/* Older temprorary contenteditable as paste source catcher method for fallbacks */ +wysihtml.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); +}; + +/** + * Get element's style for a specific css property + * + * @param {Element} element The element on which to retrieve the style + * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) + * + * @example + * wysihtml.dom.getStyle("display").from(document.body); + * // => "block" + */ +wysihtml.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) { + if (element.nodeType !== wysihtml.ELEMENT_NODE) { return; } @@ -8460,7 +8451,8 @@ wysihtml5.dom.getStyle = (function() { }; }; })(); -;wysihtml5.dom.getTextNodes = function(node, ingoreEmpty){ + +wysihtml.dom.getTextNodes = function(node, ingoreEmpty){ var all = []; for (node=node.firstChild;node;node=node.nextSibling){ if (node.nodeType == 3) { @@ -8468,74 +8460,77 @@ wysihtml5.dom.getStyle = (function() { all.push(node); } } else { - all = all.concat(wysihtml5.dom.getTextNodes(node, ingoreEmpty)); + all = all.concat(wysihtml.dom.getTextNodes(node, ingoreEmpty)); } } return all; }; -;/** - * High performant way to check whether an element with a specific tag name is in the given document + +/** + * 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.hasElementWithTagName(document, "IMG"); + * wysihtml.dom.hasElementWithClassName(document, "foobar"); */ -wysihtml5.dom.hasElementWithTagName = (function() { +(function(wysihtml) { var LIVE_CACHE = {}, DOCUMENT_IDENTIFIER = 1; function _getDocumentIdentifier(doc) { - return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); + return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++); } - return function(doc, tagName) { - var key = _getDocumentIdentifier(doc) + ":" + tagName, + wysihtml.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 (!wysihtml.browser.supportsNativeGetElementsByClassName()) { + return !!doc.querySelector("." + className); + } + + var key = _getDocumentIdentifier(doc) + ":" + className, cacheEntry = LIVE_CACHE[key]; if (!cacheEntry) { - cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); + cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); } return cacheEntry.length > 0; }; -})(); -;/** - * High performant way to check whether an element with a specific class name is in the given document +})(wysihtml); + +/** + * 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.hasElementWithClassName(document, "foobar"); + * wysihtml.dom.hasElementWithTagName(document, "IMG"); */ -(function(wysihtml5) { +wysihtml.dom.hasElementWithTagName = (function() { var LIVE_CACHE = {}, DOCUMENT_IDENTIFIER = 1; function _getDocumentIdentifier(doc) { - return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); + return doc._wysihtml_identifier || (doc._wysihtml_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, + return function(doc, tagName) { + var key = _getDocumentIdentifier(doc) + ":" + tagName, cacheEntry = LIVE_CACHE[key]; if (!cacheEntry) { - cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); + cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); } return cacheEntry.length > 0; }; -})(wysihtml5); -;wysihtml5.dom.insert = function(elementToInsert) { +})(); + +wysihtml.dom.insert = function(elementToInsert) { return { after: function(element) { element.parentNode.insertBefore(elementToInsert, element.nextSibling); @@ -8550,7 +8545,8 @@ wysihtml5.dom.hasElementWithTagName = (function() { } }; }; -;wysihtml5.dom.insertCSS = function(rules) { + +wysihtml.dom.insertCSS = function(rules) { rules = rules.join("\n"); return { @@ -8577,9 +8573,25 @@ wysihtml5.dom.hasElementWithTagName = (function() { } }; }; -;// TODO: Refactor dom tree traversing here -(function(wysihtml5) { - wysihtml5.dom.lineBreaks = function(node) { + +/** + * Check whether the given node is a proper loaded image + * FIXME: Returns undefined when unknown (Chrome, Safari) +*/ + +wysihtml.dom.isLoadedImage = function (node) { + try { + return node.complete && !node.mozMatchesSelector(":-moz-broken"); + } catch(e) { + if (node.complete && node.readyState === "complete") { + return true; + } + } +}; + +// TODO: Refactor dom tree traversing here +(function(wysihtml) { + wysihtml.dom.lineBreaks = function(node) { function _isLineBreak(n) { return n.nodeName === "BR"; @@ -8594,7 +8606,7 @@ wysihtml5.dom.hasElementWithTagName = (function() { return true; } - if (wysihtml5.dom.getStyle("display").from(element) === "block") { + if (wysihtml.dom.getStyle("display").from(element) === "block") { return true; } @@ -8603,31 +8615,31 @@ wysihtml5.dom.hasElementWithTagName = (function() { return { - /* wysihtml5.dom.lineBreaks(element).add(); + /* wysihtml.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}); + nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}), + previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true}); if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { - wysihtml5.dom.insert(doc.createElement("br")).after(node); + wysihtml.dom.insert(doc.createElement("br")).after(node); } if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { - wysihtml5.dom.insert(doc.createElement("br")).before(node); + wysihtml.dom.insert(doc.createElement("br")).before(node); } }, - /* wysihtml5.dom.lineBreaks(element).remove(); + /* wysihtml.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}); + var nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}), + previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true}); if (nextSibling && _isLineBreak(nextSibling)) { nextSibling.parentNode.removeChild(nextSibling); @@ -8638,13 +8650,14 @@ wysihtml5.dom.hasElementWithTagName = (function() { } }; }; -})(wysihtml5);;/** +})(wysihtml); +/** * Method to set dom events * * @example - * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); + * wysihtml.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); */ -wysihtml5.dom.observe = function(element, eventNames, handler) { +wysihtml.dom.observe = function(element, eventNames, handler) { eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; var handlerWrapper, @@ -8689,7 +8702,8 @@ wysihtml5.dom.observe = function(element, eventNames, handler) { } }; }; -;/** + +/** * HTML Sanitizer * Rewrites the HTML based on given rules * @@ -8703,7 +8717,7 @@ wysihtml5.dom.observe = function(element, eventNames, handler) { * * @example * var userHTML = ''; - * wysihtml5.dom.parse(userHTML, { + * wysihtml.dom.parse(userHTML, { * tags { * p: "div", // Rename p tags to div tags * font: "span" // Rename font tags to span tags @@ -8714,11 +8728,11 @@ wysihtml5.dom.observe = function(element, eventNames, handler) { * // =>foo
* * var userHTML = 'foo bar'; - * wysihtml5.dom.parse(userHTML); + * wysihtml.dom.parse(userHTML); * // => 'I'm a table!' * * var userHTML = '
I'm a table! foobar'; - * wysihtml5.dom.parse(userHTML, { + * wysihtml.dom.parse(userHTML, { * tags: { * div: undefined, * br: true @@ -8727,7 +8741,7 @@ wysihtml5.dom.observe = function(element, eventNames, handler) { * // => '' * * var userHTML = '
foobarfoobar'; - * wysihtml5.dom.parse(userHTML, { + * wysihtml.dom.parse(userHTML, { * classes: { * red: 1, * green: 1 @@ -8741,7 +8755,7 @@ wysihtml5.dom.observe = function(element, eventNames, handler) { * // => 'foo
bar
' */ -wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { +wysihtml.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 */ @@ -8771,7 +8785,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { * which later replaces the entire body content */ function parse(elementOrHtml, config) { - wysihtml5.lang.object(currentRules).merge(defaultRules).merge(config.rules).get(); + wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get(); var context = config.context || elementOrHtml.ownerDocument || document, fragment = context.createDocumentFragment(), @@ -8786,7 +8800,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { } if (isString) { - element = wysihtml5.dom.getAsDom(elementOrHtml, context); + element = wysihtml.dom.getAsDom(elementOrHtml, context); } else { element = elementOrHtml; } @@ -8808,7 +8822,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { if (config.unjoinNbsps) { // replace joined non-breakable spaces with unjoined - var txtnodes = wysihtml5.dom.getTextNodes(fragment); + var txtnodes = wysihtml.dom.getTextNodes(fragment); for (var n = txtnodes.length; n--;) { txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 "); } @@ -8820,7 +8834,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { // Insert new DOM tree element.appendChild(fragment); - return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; + return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element; } function _convert(oldNode, cleanUp, clearInternals, uneditableClass) { @@ -8835,7 +8849,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { nodeDisplay; // Passes directly elemets with uneditable class - if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) { + if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) { return oldNode; } @@ -8859,18 +8873,18 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { } } - nodeDisplay = wysihtml5.dom.getStyle("display").from(oldNode); + nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode); if (nodeDisplay === '') { // Handle display style when element not in dom - nodeDisplay = wysihtml5.lang.array(blockElements).contains(oldNode.tagName) ? "block" : ""; + nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : ""; } - if (wysihtml5.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) { + if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) { fragment.appendChild(oldNode.ownerDocument.createElement("br")); } // TODO: try to minimize surplus spaces - if (wysihtml5.lang.array([ + if (wysihtml.lang.array([ "div", "pre", "p", "table", "td", "th", "ul", "ol", "li", @@ -8911,7 +8925,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { 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"))) || + ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) || !newNode.attributes.length) ) { fragment = newNode.ownerDocument.createDocumentFragment(); @@ -8935,7 +8949,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { for (sel in selectorRules) { if (selectorRules.hasOwnProperty(sel)) { - if (wysihtml5.lang.object(selectorRules[sel]).isFunction()) { + if (wysihtml.lang.object(selectorRules[sel]).isFunction()) { method = selectorRules[sel]; } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) { method = elementHandlingMethods[selectorRules[sel]]; @@ -8960,12 +8974,12 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { * We already parsed that element * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) */ - if (oldNode._wysihtml5) { + if (oldNode._wysihtml) { return null; } - oldNode._wysihtml5 = 1; + oldNode._wysihtml = 1; - if (oldNode.className === "wysihtml5-temp") { + if (oldNode.className === "wysihtml-temp") { return null; } @@ -8984,7 +8998,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { * Adoesn'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() && + if (!wysihtml.browser.autoClosesUnclosedTags() && oldNode.nodeName === "P" && oldNode.outerHTML.slice(-4).toLowerCase() !== "") { nodeName = "div"; @@ -9035,7 +9049,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { 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")) { + if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) { return true; } @@ -9099,7 +9113,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { 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()) ) { + if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) { return true; } } @@ -9112,7 +9126,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { if (definition.attrs) { for (a in definition.attrs) { if (definition.attrs.hasOwnProperty(a)) { - attr = wysihtml5.dom.getAttribute(oldNode, a); + attr = wysihtml.dom.getAttribute(oldNode, a); if (typeof(attr) === "string") { if (attr.search(definition.attrs[a]) > -1) { return true; @@ -9156,7 +9170,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { } function _checkAttribute(attributeName, attributeValue, methodName, nodeName) { - var method = wysihtml5.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName], + var method = wysihtml.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName], newAttributeValue; if (method) { @@ -9170,10 +9184,10 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { } 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(), + var globalAttributes = wysihtml.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes + checkAttributes = wysihtml.lang.object(globalAttributes).merge( wysihtml.lang.object(local_attributes || {}).clone()).get(), attributes = {}, - oldAttributes = wysihtml5.dom.getAttributes(oldNode), + oldAttributes = wysihtml.dom.getAttributes(oldNode), attributeName, newValue, matchingAttributes; for (attributeName in checkAttributes) { @@ -9219,11 +9233,11 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { method; if (setAttributes) { - attributes = wysihtml5.lang.object(setAttributes).clone(); + attributes = wysihtml.lang.object(setAttributes).clone(); } // check/convert values of attributes - attributes = wysihtml5.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get(); + attributes = wysihtml.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get(); if (setClass) { classes.push(setClass); @@ -9235,7 +9249,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { if (!method) { continue; } - newClass = method(wysihtml5.dom.getAttribute(oldNode, attributeName)); + newClass = method(wysihtml.dom.getAttribute(oldNode, attributeName)); if (typeof(newClass) === "string") { classes.push(newClass); } @@ -9249,7 +9263,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { continue; } - newStyle = method(wysihtml5.dom.getAttribute(oldNode, attributeName)); + newStyle = method(wysihtml.dom.getAttribute(oldNode, attributeName)); if (typeof(newStyle) === "string") { styles.push(newStyle); } @@ -9274,7 +9288,7 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { } if (newClasses.length) { - attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" "); + attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" "); } } else { @@ -9282,13 +9296,13 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { } } else { if(classes && classes.length > 0) { - attributes["class"] = wysihtml5.lang.array(classes).unique().join(" "); + attributes["class"] = wysihtml.lang.array(classes).unique().join(" "); } } } else { - // make sure that wysihtml5 temp class doesn't get stripped out + // make sure that wysihtml temp class doesn't get stripped out if (!clearInternals) { - allowedClasses["_wysihtml5-temp-placeholder"] = 1; + allowedClasses["_wysihtml-temp-placeholder"] = 1; allowedClasses["_rangySelectionBoundary"] = 1; allowedClasses["wysiwyg-tmp-selected-cell"] = 1; } @@ -9296,2010 +9310,888 @@ wysihtml5.dom.parse = function(elementOrHtml_current, config_current) { // add old classes last oldClasses = oldNode.getAttribute("class"); if (oldClasses) { - classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); - } - classesLength = classes.length; - for (; iunder 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
- *- *
- */ -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 - *- eminem
- *- dr. dre
- *- 50 Cent
- *- * 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- eminem
- *- dr. dre
- *- 50 Cent
- *
- * 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; + 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) {} } - return ret; - } - function removeElement(el) { - el.parentNode.removeChild(el); + // 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 insertAfter(referenceNode, newNode) { - referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); + function _handleText(oldNode) { + var nextSibling = oldNode.nextSibling; + if (nextSibling && nextSibling.nodeType === wysihtml.TEXT_NODE) { + // Concatenate text nodes + nextSibling.data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); + } else { + // \uFEFF = wysihtml.INVISIBLE_SPACE (used as a hack in certain rich text editing situations) + var data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); + return oldNode.ownerDocument.createTextNode(data); + } } - function nextNode(node, tag) { - var element = node.nextSibling; - while (element.nodeType !=1) { - element = element.nextSibling; - if (!tag || tag == element.tagName.toLowerCase()) { - return element; - } + function _handleComment(oldNode) { + if (currentRules.comments) { + return oldNode.ownerDocument.createComment(oldNode.nodeValue); } - 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; + // ------------ 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(); + }); + }; + })(), - spanCollect.push(map[rr][cc]); + 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(); + }); + }; + })(), - 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; + 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(); + }); + }; + })(), - 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); + alt: (function() { + var REG_EXP = /[^ a-z0-9_\-]/gi; + return function(attributeValue, nodeName) { + if (!attributeValue) { + if (nodeName === "IMG") { + return ""; } else { - map[ridx][c] = new MapCell(cell); - c++; + return null; } } - } - this.map = map; - return map; - }, + return attributeValue.replace(REG_EXP, ""); + }; + })(), - 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; + // 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; + }; + })(), - return tableCells; - }, + // 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; + }; + })(), - 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; + any: (function() { + return function(attributeValue) { + if (!attributeValue) { + return null; + } + return attributeValue; + }; + })() + }; - return tableRows; - }, + // ------------ 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()]; + }; + })(), + }; - getMapIndex: function(cell) { - var r_length = this.map.length, - c_length = (this.map && this.map[0]) ? this.map[0].length : 0; + // ------------ 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()]; + }; + })(), - 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; - }, + 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()]; + }; + })(), - 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; - }, + 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()]; + }; + })(), - getMapElsTo: function(to_cell) { - var els = []; - this.setTableMap(); - this.idx_start = this.getMapIndex(this.cell); - this.idx_end = this.getMapIndex(to_cell); + 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)]; + }; + })() + }; - // 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; - } + // 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; + } - 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); + // matches list of visible dimensioned elements + for (var i = visibleElements.length; i--;) { + if (el.querySelector(visibleElements[i])) { + return true; } } - } - return els; - }, - orderSelectionEnds: function(secondcell) { - this.setTableMap(); - this.idx_start = this.getMapIndex(this.cell); - this.idx_end = this.getMapIndex(secondcell); + // 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) { + wysihtml.dom.unwrap(element); + }, - // 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; - } + remove: function (element) { + element.parentNode.removeChild(element); + } + }; - 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 - }; - }, + return parse(elementOrHtml_current, config_current); +}; - 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]); - } - } - } +// does a selector query on element or array of elements +wysihtml.dom.query = function(elements, query) { + var ret = [], + q; - // add non breaking space - cell.appendChild(document.createTextNode("\u00a0")); - frag.appendChild(cell); - } - return frag; - }, + if (elements.nodeType) { + elements = [elements]; + } - // 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++; + 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 corrIdx; - }, + } + return ret; +}; - getLastNewCellOnRow: function(row, rowLimit) { - var cells = this.getRowCells(row), - cell, idx; +/** + * Checks for empty text node childs and removes them + * + * @param {Element} node The element in which to cleanup + * @example + * wysihtml.dom.removeEmptyTextNodes(element); + */ +wysihtml.dom.removeEmptyTextNodes = function(node) { + var childNode, + childNodes = wysihtml.lang.array(node.childNodes).get(), + childNodesLength = childNodes.length, + i = 0; - 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; - }, + for (; i 1) { - var newCells = this.createCells(cType, colspan -1); - insertAfter(cell.el, newCells); - } - cell.el.removeAttribute('colspan'); - } - }, +/** + * Renames an element (eg. a 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
+ *+ *
+ */ +wysihtml.dom.renameElement = function(element, newNodeName) { + var newElement = element.ownerDocument.createElement(newNodeName), + firstChild; + while (firstChild = element.firstChild) { + newElement.appendChild(firstChild); + } + wysihtml.dom.copyAttributes(["align", "className"]).from(element).to(newElement); + + if (element.parentNode) { + element.parentNode.replaceChild(newElement, element); + } - getRealRowEl: function(force, idx) { - var r = null, - c = null; + return newElement; +}; - idx = idx || this.idx; +/** + * 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 + *- eminem
+ *- dr. dre
+ *- 50 Cent
+ *+ * hello + *+ * + */ +wysihtml.dom.replaceWithChildNodes = function(node) { + if (!node.parentNode) { + return; + } - 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; - } - } - } + while (node.firstChild) { + node.parentNode.insertBefore(node.firstChild, node); + } + node.parentNode.removeChild(node); +}; - if (r === null && force) { - r = api.getParentElement(this.map[idx.row][idx.col].el, { query: "tr" }) || null; - } +/** + * Unwraps an unordered/ordered list + * + * @param {Element} element The list element which should be unwrapped + * + * @example + * + *+ *
+ * + * + * + * + * eminem- eminem
+ *- dr. dre
+ *- 50 Cent
+ *
+ * dr. dre
+ * 50 Cent
+ */ +(function(dom) { + function _isBlockElement(node) { + return dom.getStyle("display").from(node) === "block"; + } - return r; - }, + function _isLineBreak(node) { + return node.nodeName === "BR"; + } - injectRowAt: function(row, col, colspan, cType, c) { - var r = this.getRealRowEl(false, {'row': row, 'col': col}), - new_cells = this.createCells(cType, colspan); + function _appendLineBreak(element) { + var lineBreak = element.ownerDocument.createElement("br"); + element.appendChild(lineBreak); + } - 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); - } - }, + function resolveList(list, useLineBreaks) { + if (!list.nodeName.match(/^(MENU|UL|OL)$/)) { + return; + } - canMerge: function(to) { - this.to = to; - this.setTableMap(); - this.idx_start = this.getMapIndex(this.cell); - this.idx_end = this.getMapIndex(this.to); + var doc = list.ownerDocument, + fragment = doc.createDocumentFragment(), + previousSibling = wysihtml.dom.domNode(list).prev({ignoreBlankTexts: true}), + nextSibling = wysihtml.dom.domNode(list).next({ignoreBlankTexts: true}), + firstChild, + lastChild, + isLastChild, + shouldAppendLineBreak, + paragraph, + listItem, + lastListItem = list.lastElementChild || list.lastChild, + isLastItem; - // 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 (useLineBreaks) { + // Insert line break if list is after a non-block element + if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) { + _appendLineBreak(fragment); } - 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; + 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); } } - } - 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; + listItem.parentNode.removeChild(listItem); } - }, - - 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'); - } + } 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); } - } - - // 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); + } else { + paragraph = doc.createElement("p"); + while (firstChild = listItem.firstChild) { + paragraph.appendChild(firstChild); } + fragment.appendChild(paragraph); } + listItem.parentNode.removeChild(listItem); } - }, + } + + list.parentNode.replaceChild(fragment, list); + } - fillMissingCells: function() { - var r_max = 0, - c_max = 0, - prevcell = null; + dom.resolveList = resolveList; +})(wysihtml.dom); - this.setTableMap(); - if (this.map) { +/** + * 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 wysihtml.dom.Sandbox(function(sandbox) { + * sandbox.getWindow().document.body.innerHTML = ''; + * }); + */ +(function(wysihtml) { + 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" + ]; - // 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; } - } + wysihtml.dom.Sandbox = Base.extend( + /** @scope wysihtml.dom.Sandbox.prototype */ { - 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); - } - } - } - } - } + constructor: function(readyCallback, config) { + this.callback = readyCallback || wysihtml.EMPTY_FUNCTION; + this.config = wysihtml.lang.object({}).merge(config).get(); + if (!this.config.className) { + this.config.className = "wysihtml-sandbox"; } + this.editableArea = this._createIframe(); }, - rectify: function() { - if (!this.removeEmptyTable()) { - this.removeSurplusLines(); - this.fillMissingCells(); - return true; - } else { - return false; + insertInto: function(element) { + if (typeof(element) === "string") { + element = doc.getElementById(element); } - }, - 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); - } - } + element.appendChild(this.editableArea); }, - // 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.'); - } - } - } + getIframe: function() { + return this.editableArea; }, - // 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'); - } - } - } + getWindow: function() { + this._readyError(); }, - // 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'); - } - } + getDocument: function() { + this._readyError(); }, - 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; + destroy: function() { + var iframe = this.getIframe(); + iframe.parentNode.removeChild(iframe); }, - 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; + _readyError: function() { + throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet"); }, - // 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); - } - }, + /** + * 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; + wysihtml.dom.setAttributes({ + "security": "restricted", + "allowtransparency": "true", + "frameborder": 0, + "width": 0, + "height": 0, + "marginwidth": 0, + "marginheight": 0 + }).on(iframe); - 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); + // Setting the src like this prevents ssl warnings in IE6 + if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { + iframe.src = "javascript:''"; } - }, - 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]); - } + 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; }, - // 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(); + /** + * 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 (!wysihtml.dom.contains(doc.documentElement, iframe)) { + return; } - }, - addRow: function(where) { - var doc = this.table.ownerDocument; + 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 + }); - 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; - } + // Create the basic dom tree including proper DOCTYPE and charset + iframeDocument.open("text/html", "replace"); + iframeDocument.write(sandboxHtml); + iframeDocument.close(); - if (this.idx !== false) { - var modRow = this.map[this.idx.row], - newRow = doc.createElement('tr'); + this.getWindow = function() { return iframe.contentWindow; }; + this.getDocument = function() { return iframe.contentWindow.document; }; - for (var ridx = 0, rmax = modRow.length; ridx < rmax; ridx++) { - if (!modRow[ridx].modified) { - this.setCellAsModified(modRow[ridx]); - this.addRowCell(modRow[ridx], newRow, where); - } - } + // 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("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber); + }; - 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; + if (!wysihtml.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 wysihtml.lang.string( + '' + + '#{stylesheets}' + + '' + ).interpolate(templateVars); }, - addColumn: function(where) { - var row, modCell; + /** + * 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) {} - 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; + try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} + if (setter) { + try { object.__defineSetter__(property, function() {}); } catch(e) {} } - 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); - } + if (!wysihtml.browser.crashesWhenDefineProperty(property)) { + try { + var config = { + get: function() { return value; } + }; + if (setter) { + config.set = function() {}; } + Object.defineProperty(object, property, config); + } catch(e) {} + } + } + }); +})(wysihtml); + +(function() { + var mapping = { + "className": "class" + }; + wysihtml.dom.setAttributes = function(attributes) { + return { + on: function(element) { + for (var i in attributes) { + element.setAttribute(mapping[i] || i, attributes[i]); } } - }, - - 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); - } + }; + }; +})(); + +wysihtml.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 { - nrow = doc.createElement('tr'); - nrow.appendChild(this.createCells(cType, 1)); - this.table.appendChild(nrow); + style[i] = styles[i]; } } } }; +}; - 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(); - }, +/** + * 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 wysihtml.Editor class + * @param {Element} view Instance of wysihtml.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 || "wysihtml-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); + } + }; - findColumnByCell: function(cell) { - var c = new TableModifyerByCell(cell); - return c.getColumnElementsByCell(); - }, + editor + .on("set_placeholder", set) + .on("unset_placeholder", unset) + .on("focus:composer", unset) + .on("paste:composer", unset) + .on("blur:composer", set); - canMerge: function(cell1, cell2) { - var c = new TableModifyerByCell(cell1); - return c.canMerge(cell2); - } + set(); }; +})(wysihtml.dom); -})(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() { +(function(dom) { var documentElement = document.documentElement; - if (documentElement.compareDocumentPosition) { - return function(container, element) { - return container.compareDocumentPosition(element); + if ("textContent" in documentElement) { + dom.setTextContent = function(element, text) { + element.textContent = text; }; - } 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; + dom.getTextContent = function(element) { + return element.textContent; + }; + } else if ("innerText" in documentElement) { + dom.setTextContent = function(element, text) { + element.innerText = text; + }; - 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; + dom.getTextContent = function(element) { + return element.innerText; + }; + } else { + dom.setTextContent = function(element, text) { + element.nodeValue = text; + }; - 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; + dom.getTextContent = function(element) { + return element.nodeValue; }; } -})(); -;/* Unwraps element and returns list of childNodes that the node contained. +})(wysihtml.dom); + +/* Unwraps element and returns list of childNodes that the node contained. * * Example: - * var childnodes = wysihtml5.dom.unwrap(document.querySelector('.unwrap-me')); + * var childnodes = wysihtml.dom.unwrap(document.querySelector('.unwrap-me')); */ -wysihtml5.dom.unwrap = function(node) { +wysihtml.dom.unwrap = function(node) { var children = []; if (node.parentNode) { while (node.lastChild) { children.unshift(node.lastChild); - wysihtml5.dom.insert(node.lastChild).after(node); + wysihtml.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() { +wysihtml.quirks.cleanPastedHTML = (function() { var styleToRegex = function (styleStr) { - var trimmedStr = wysihtml5.lang.string(styleStr).trim(), + var trimmedStr = wysihtml.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), + var newRules = wysihtml.lang.object(rules).clone(true), tag, style; for (tag in newRules.tags) { @@ -11341,13 +10233,13 @@ wysihtml5.quirks.cleanPastedHTML = (function() { return function(html, options) { var exceptStyles = { - 'color': wysihtml5.dom.getStyle("color").from(options.referenceNode), - 'fontSize': wysihtml5.dom.getStyle("font-size").from(options.referenceNode) + 'color': wysihtml.dom.getStyle("color").from(options.referenceNode), + 'fontSize': wysihtml.dom.getStyle("font-size").from(options.referenceNode) }, rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles), newHtml; - newHtml = wysihtml5.dom.parse(html, { + newHtml = wysihtml.dom.parse(html, { "rules": rules, "cleanUp": true, // elements, empty or without attributes, should be removed/replaced with their content "context": options.referenceNode.ownerDocument, @@ -11360,14 +10252,15 @@ wysihtml5.quirks.cleanPastedHTML = (function() { }; })(); -;/** + +/** * 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); + * wysihtml.quirks.ensureProperClearing(myContentEditableElement); */ -wysihtml5.quirks.ensureProperClearing = (function() { +wysihtml.quirks.ensureProperClearing = (function() { var clearIfNecessary = function() { var element = this; setTimeout(function() { @@ -11380,10 +10273,11 @@ wysihtml5.quirks.ensureProperClearing = (function() { }; return function(composer) { - wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); + wysihtml.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); }; })(); -;// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 + +// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 // // In Firefox this: // var d = document.createElement("div"); @@ -11392,9 +10286,9 @@ wysihtml5.quirks.ensureProperClearing = (function() { // will result in: // // which is wrong -(function(wysihtml5) { +(function(wysihtml) { var TILDE_ESCAPED = "%7E"; - wysihtml5.quirks.getCorrectInnerHTML = function(element) { + wysihtml.quirks.getCorrectInnerHTML = function(element) { var innerHTML = element.innerHTML; if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { return innerHTML; @@ -11407,26 +10301,27 @@ wysihtml5.quirks.ensureProperClearing = (function() { i; for (i=0, length=elementsWithTilde.length; i0) { - 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) { +(function(wysihtml) { // List of supported color format parsing methods // If radix is not defined 10 is expected as default @@ -11605,7 +10384,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { } // Public API functions for styleParser - wysihtml5.quirks.styleParser = { + wysihtml.quirks.styleParser = { // Takes color string value as an argument and returns suitable parsing method for it getColorParseMethod : getColorParseMethod, @@ -11617,13 +10396,13 @@ wysihtml5.quirks.ensureProperClearing = (function() { * 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 = wysihtml.quirks.styleParser.parseColor("#ABC"); // [170, 187, 204, 1] + * var colorArray = wysihtml.quirks.styleParser.parseColor("#AABBCC"); // [170, 187, 204, 1] + * var colorArray = wysihtml.quirks.styleParser.parseColor("rgb(1,2,3)"); // [1, 2, 3, 1] + * var colorArray = wysihtml.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] + * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1] + * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color"); // [0, 0, 0, 1] */ parseColor : function (stylesStr, paramName) { var paramsRegex, params, colorType, colorMatch, radix, @@ -11635,7 +10414,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { if (!(params = stylesStr.match(paramsRegex))) { return false; } params = params.pop().split(":")[1]; - colorStr = wysihtml5.lang.string(params).trim(); + colorStr = wysihtml.lang.string(params).trim(); } if (!(colorType = getColorParseMethod(colorStr))) { return false; } @@ -11646,7 +10425,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { if (colorType === colorParseMethods.hex3) { colorMatch.shift(); colorMatch.push(1); - return wysihtml5.lang.array(colorMatch).map(function(d, idx) { + return wysihtml.lang.array(colorMatch).map(function(d, idx) { return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d); }); } @@ -11657,7 +10436,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { colorMatch.push(1); } - return wysihtml5.lang.array(colorMatch).map(function(d, idx) { + return wysihtml.lang.array(colorMatch).map(function(d, idx) { return (idx < 3) ? parseInt(d, radix): parseFloat(d); }); }, @@ -11666,14 +10445,14 @@ wysihtml5.quirks.ensureProperClearing = (function() { * 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 = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash"); // "#AABBCC" + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex"); // "AABBCC" + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv"); // "170, 187, 204, 1" + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba"); // "rgba(170,187,204,1)" + * var colorStr = wysihtml.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)" + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 0.5]); // "rgba(170,187,204,0.5)" + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1]); // "rgb(170,187,204)" */ unparseColor: function(val, colorFormat) { var hexRadix = 16; @@ -11701,21 +10480,22 @@ wysihtml5.quirks.ensureProperClearing = (function() { parseFontSize: function(stylesStr) { var params = stylesStr.match(makeParamRegExp("font-size")); if (params) { - return wysihtml5.lang.string(params[params.length - 1].split(":")[1]).trim(); + return wysihtml.lang.string(params[params.length - 1].split(":")[1]).trim(); } return false; } }; -})(wysihtml5); -;/** +})(wysihtml); + +/** * Selection API * * @example - * var selection = new wysihtml5.Selection(editor); + * var selection = new wysihtml.Selection(editor); */ -(function(wysihtml5) { - var dom = wysihtml5.dom; +(function(wysihtml) { + var dom = wysihtml.dom; function _getCumulativeOffsetTop(element) { var top = 0; @@ -11774,8 +10554,8 @@ wysihtml5.quirks.ensureProperClearing = (function() { } }; - blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml5.INVISIBLE_SPACE)); - blankNode.className = '_wysihtml5-temp-caret-fix'; + blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml.INVISIBLE_SPACE)); + blankNode.className = '_wysihtml-temp-caret-fix'; blankNode.style.display = 'block'; blankNode.style.minWidth = '1px'; blankNode.style.height = '0px'; @@ -11814,8 +10594,8 @@ wysihtml5.quirks.ensureProperClearing = (function() { } } - wysihtml5.Selection = Base.extend( - /** @scope wysihtml5.Selection.prototype */ { + wysihtml.Selection = Base.extend( + /** @scope wysihtml.Selection.prototype */ { constructor: function(editor, contain, unselectableClass) { // Make sure that our external range library is initialized rangy.init(); @@ -11839,7 +10619,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { }, /** - * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark + * Restore a selection retrieved via wysihtml.Selection.prototype.getBookmark * * @param {Object} bookmark An object that represents the current selection */ @@ -11869,7 +10649,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { // 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), + caretPlaceholderText = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE), placeholderRemover = (function(event) { // Self-destructs the caret and keeps the text inserted into it by user var lastChild; @@ -11886,10 +10666,10 @@ wysihtml5.quirks.ensureProperClearing = (function() { // 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, ""); + caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) { lastChild = caretPlaceholder.lastChild; - wysihtml5.dom.unwrap(caretPlaceholder); + wysihtml.dom.unwrap(caretPlaceholder); this.setAfter(lastChild); } else { caretPlaceholder.parentNode.removeChild(caretPlaceholder); @@ -11908,7 +10688,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { } }; - caretPlaceholder.className = '_wysihtml5-temp-caret-fix'; + caretPlaceholder.className = '_wysihtml-temp-caret-fix'; caretPlaceholder.style.position = 'absolute'; caretPlaceholder.style.display = 'block'; caretPlaceholder.style.minWidth = '1px'; @@ -11952,7 +10732,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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); + var caretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE); node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); this.selectNode(caretPlaceholder); setTimeout(function() { @@ -12001,16 +10781,16 @@ wysihtml5.quirks.ensureProperClearing = (function() { */ selectNode: function(node, avoidInvisibleSpace) { var range = rangy.createRange(this.doc), - isElement = node.nodeType === wysihtml5.ELEMENT_NODE, + isElement = node.nodeType === wysihtml.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), content = isElement ? node.innerHTML : node.data, - isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), + isEmpty = (content === "" || content === wysihtml.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) {} + try { node.innerHTML = wysihtml.INVISIBLE_SPACE; } catch(e) {} } if (canHaveHTML) { range.selectNodeContents(node); @@ -12078,7 +10858,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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); + return wysihtml.lang.array(nodeTypes).contains(node.nodeName); }); nodes = nodes.concat(curNodes); } @@ -12112,7 +10892,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { }, // Deletes selection contents making sure uneditables/unselectables are not partially deleted - // Triggers wysihtml5:uneditable:delete custom event on all deleted uneditables if customevents suppoorted + // Triggers wysihtml:uneditable:delete custom event on all deleted uneditables if customevents suppoorted deleteContents: function() { var range = this.getRange(); this.deleteRangeContents(range); @@ -12124,20 +10904,20 @@ wysihtml5.quirks.ensureProperClearing = (function() { var startParent, endParent, uneditables, ev; if (this.unselectableClass) { - if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) { + if ((startParent = wysihtml.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))) { + if ((endParent = wysihtml.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); + return wysihtml.dom.hasClass(node, this.unselectableClass); }).bind(this)); for (var i = uneditables.length; i--;) { try { - ev = new CustomEvent("wysihtml5:uneditable:delete"); + ev = new CustomEvent("wysihtml:uneditable:delete"); uneditables[i].dispatchEvent(ev); } catch (err) {} } @@ -12177,10 +10957,10 @@ wysihtml5.quirks.ensureProperClearing = (function() { } 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); + displayStyle = wysihtml.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) && + !wysihtml.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) && + !wysihtml.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) && (/^[\s]*$/).test(ret.innerHTML) ) { ret = this.getPreviousNode(ret, ignoreEmpty); @@ -12244,7 +11024,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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); + curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain); if (curEl) { parents.push(curEl); } @@ -12281,16 +11061,16 @@ wysihtml5.quirks.ensureProperClearing = (function() { startNode = getRangeNode(range.startContainer, range.startOffset); if (startNode) { - if (startNode.nodeType === wysihtml5.TEXT_NODE) { + if (startNode.nodeType === wysihtml.TEXT_NODE) { if (!startNode.parentNode) { return false; } - if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml5.dom.domNode(startNode.previousSibling).is.block())) { + if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml.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()) { + } else if (includeLineBreaks && wysihtml.dom.domNode(startNode).is.lineBreak()) { return true; } else { r.selectNodeContents(this.getRange().commonAncestorContainer); @@ -12305,7 +11085,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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))); + return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml.dom.getParentElement(node.parentNode, { query: ofNode }, 1))); } else if (node) { return (offset === 0 && !this.getPreviousNode(node, true)); } @@ -12325,7 +11105,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { } // Escape temproray helper nodes if selection in them - inTmpCaret = wysihtml5.dom.getParentElement(startNode, { query: '._wysihtml5-temp-caret-fix' }, 1); + inTmpCaret = wysihtml.dom.getParentElement(startNode, { query: '._wysihtml-temp-caret-fix' }, 1); if (inTmpCaret) { startNode = inTmpCaret.parentNode; startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret); @@ -12361,10 +11141,10 @@ wysihtml5.quirks.ensureProperClearing = (function() { prevLeaf = null; if(prevNode) { - if (prevNode.nodeType === 1 && wysihtml5.dom.hasClass(prevNode, this.unselectableClass)) { + if (prevNode.nodeType === 1 && wysihtml.dom.hasClass(prevNode, this.unselectableClass)) { prevLeaf = prevNode; } else { - prevLeaf = wysihtml5.dom.domNode(prevNode).lastLeafNode(); + prevLeaf = wysihtml.dom.domNode(prevNode).lastLeafNode(); } } @@ -12409,8 +11189,8 @@ wysihtml5.quirks.ensureProperClearing = (function() { var body = this.doc.body, oldScrollTop = restoreScrollPosition && body.scrollTop, oldScrollLeft = restoreScrollPosition && body.scrollLeft, - className = "_wysihtml5-temp-placeholder", - placeholderHtml = '' + wysihtml5.INVISIBLE_SPACE + '', + className = "_wysihtml-temp-placeholder", + placeholderHtml = '' + wysihtml.INVISIBLE_SPACE + '', range = this.getRange(true), caretPlaceholder, newCaretPlaceholder, @@ -12459,7 +11239,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { newRange.setStartBefore(nextSibling); newRange.setEndAfter(prevSibling); } else { - newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + newCaretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE); dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]); newRange.setStartBefore(newCaretPlaceholder); newRange.setEndAfter(newCaretPlaceholder); @@ -12573,7 +11353,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { // Empty elements are cleaned up from extracted content for (var i = childNodes.length; i --;) { - if (!wysihtml5.dom.domNode(childNodes[i]).is.visible()) { + if (!wysihtml.dom.domNode(childNodes[i]).is.visible()) { contentAfterRangeStart.removeChild(childNodes[i]); } } @@ -12597,8 +11377,8 @@ wysihtml5.quirks.ensureProperClearing = (function() { range.setEndAfter(element); } - if (!wysihtml5.dom.domNode(element).is.visible()) { - if (wysihtml5.dom.getTextContent(element) === '') { + if (!wysihtml.dom.domNode(element).is.visible()) { + if (wysihtml.dom.getTextContent(element) === '') { element.parentNode.removeChild(element); } else { element.parentNode.replaceChild(this.doc.createTextNode(" "), element); @@ -12654,10 +11434,10 @@ wysihtml5.quirks.ensureProperClearing = (function() { var doc = this.doc, tolerance = 5, // px hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, - tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { + tempElement = doc._wysihtmlScrollIntoViewElement = doc._wysihtmlScrollIntoViewElement || (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; + element.innerHTML = wysihtml.INVISIBLE_SPACE; return element; })(), offsetTop; @@ -12677,7 +11457,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { */ selectLine: function() { var r = rangy.createRange(); - if (wysihtml5.browser.supportsSelectionModify()) { + if (wysihtml.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) @@ -12735,7 +11515,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { // collapses selection to current line beginning or end toLineBoundary: function (location, collapse) { collapse = (typeof collapse === 'undefined') ? false : collapse; - if (wysihtml5.browser.supportsSelectionModify()) { + if (wysihtml.browser.supportsSelectionModify()) { var selection = this.win.getSelection(); selection.modify("extend", location, "lineboundary"); @@ -12783,7 +11563,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { 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)); + return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml.lang.array(['BR', 'HR']).contains(el.nodeName)); }, prevNode = function(node) { var pnode = node; @@ -12819,7 +11599,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { } } if (!r.collapsed) { - r.insertNode(this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE)); + r.insertNode(this.doc.createTextNode(wysihtml.INVISIBLE_SPACE)); } // Is probably just empty line as can not be expanded @@ -12948,7 +11728,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { var allUneditables = dom.query(this.contain, '.' + this.unselectableClass), deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass); - return wysihtml5.lang.array(allUneditables).without(deepUneditables); + return wysihtml.lang.array(allUneditables).without(deepUneditables); }, // Returns an array of ranges that belong only to this editable @@ -13030,7 +11810,7 @@ wysihtml5.quirks.ensureProperClearing = (function() { return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None")); } - wysihtml5.dom.removeInvisibleSpaces(this.composer.element); + wysihtml.dom.removeInvisibleSpaces(this.composer.element); doSelect(); if (this.composer.element.firstChild && notSelected()) { @@ -13056,741 +11836,98 @@ wysihtml5.quirks.ensureProperClearing = (function() { }, 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); - } + return rangy.createRange(this.doc); }, - applyToRange: function(range) { - var textNodes; - for (var ri = range.length; ri--;) { - textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); + isCollapsed: function() { + return this.getSelection().isCollapsed; + }, - if (!textNodes.length) { - try { - var node = this.createContainer(range[ri].endContainer.ownerDocument); - range[ri].surroundContents(node); - this.selectNode(range[ri], node); - return; - } catch(e) {} - } + getHtml: function() { + return this.getSelection().toHtml(); + }, - range[ri].splitBoundaries(); - textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); - if (textNodes.length) { - var textNode; + getPlainText: function () { + return this.getSelection().toString(); + }, - for (var i = 0, len = textNodes.length; i < len; ++i) { - textNode = textNodes[i]; - if (!this.getMatchingAncestor(textNode).element) { - this.applyToTextNode(textNode); - } - } + isEndToEndInNode: function(nodeNames) { + var range = this.getRange(), + parentElement = range.commonAncestorContainer, + startNode = range.startContainer, + endNode = range.endContainer; - 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]); - } + if (parentElement.nodeType === wysihtml.TEXT_NODE) { + parentElement = parentElement.parentNode; } - } - }, + if (startNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) { + return false; + } - undoToRange: function(range) { - var textNodes, textNode, ancestorWithClass, ancestorWithStyle, ancestor; - for (var ri = range.length; ri--;) { + if (endNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) { + return false; + } - 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); - } + while (startNode && startNode !== parentElement) { + if (startNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, startNode)) { + return false; } + if (wysihtml.dom.domNode(startNode).prev({ignoreBlankTexts: true})) { + return false; + } + startNode = startNode.parentNode; } - 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]); + while (endNode && endNode !== parentElement) { + if (endNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, endNode)) { + return false; + } + if (wysihtml.dom.domNode(endNode).next({ignoreBlankTexts: true})) { + return false; } + endNode = endNode.parentNode; } - } + return (wysihtml.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false; }, - 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); + isInThisEditable: function() { + var sel = this.getSelection(), + fnode = sel.focusNode, + anode = sel.anchorNode; - 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); + // In IE node contains will not work for textnodes, thus taking parentNode + if (fnode && fnode.nodeType !== 1) { + fnode = fnode.parentNode; } - }, - - 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"; - } - } + if (anode && anode.nodeType !== 1) { + anode = anode.parentNode; } - return (ancestors.length) ? { - "elements": ancestors, - "coverage": appliedType - } : false; + return anode && fnode && + (wysihtml.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) && + (wysihtml.dom.contains(this.composer.element, anode) || this.composer.element === anode); }, - 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); - } + deselect: function() { + var sel = this.getSelection(); + sel && sel.removeAllRanges(); } - }; + }); - wysihtml5.selection.HTMLApplier = HTMLApplier; +})(wysihtml); -})(wysihtml5, rangy); -;/** +/** * Rich Text Query/Formatting Commands * * @example - * var commands = new wysihtml5.Commands(editor); + * var commands = new wysihtml.Commands(editor); */ -wysihtml5.Commands = Base.extend( - /** @scope wysihtml5.Commands.prototype */ { +wysihtml.Commands = Base.extend( + /** @scope wysihtml.Commands.prototype */ { constructor: function(editor) { this.editor = editor; this.composer = editor.composer; @@ -13805,7 +11942,7 @@ wysihtml5.Commands = Base.extend( * commands.supports("createLink"); */ support: function(command) { - return wysihtml5.browser.supportsCommand(this.doc, command); + return wysihtml.browser.supportsCommand(this.doc, command); }, /** @@ -13817,14 +11954,14 @@ wysihtml5.Commands = Base.extend( * 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(), + var obj = wysihtml.commands[command], + args = wysihtml.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)) { + if (this.composer.hasPlaceholderSet() && !wysihtml.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) { this.composer.element.innerHTML = ""; this.composer.selection.selectNode(this.composer.element); } @@ -13846,8 +11983,8 @@ wysihtml5.Commands = Base.extend( }, remove: function(command, commandValue) { - var obj = wysihtml5.commands[command], - args = wysihtml5.lang.array(arguments).get(), + var obj = wysihtml.commands[command], + args = wysihtml.lang.array(arguments).get(), method = obj && obj.remove; if (method) { args.unshift(this.composer); @@ -13866,8 +12003,8 @@ wysihtml5.Commands = Base.extend( * var isCurrentSelectionBold = commands.state("bold"); */ state: function(command, commandValue) { - var obj = wysihtml5.commands[command], - args = wysihtml5.lang.array(arguments).get(), + var obj = wysihtml.commands[command], + args = wysihtml.lang.array(arguments).get(), method = obj && obj.state; if (method) { args.unshift(this.composer); @@ -13884,8 +12021,8 @@ wysihtml5.Commands = Base.extend( /* 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(), + var obj = wysihtml.commands[command], + args = wysihtml.lang.array(arguments).get(), method = obj && obj.stateValue; if (method) { args.unshift(this.composer); @@ -13895,25 +12032,8 @@ wysihtml5.Commands = Base.extend( } } }); -;(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) { +(function(wysihtml) { var nodeOptions = { nodeName: "A", @@ -13922,10 +12042,10 @@ wysihtml5.Commands = Base.extend( function getOptions(value) { var options = typeof value === 'object' ? value : {'href': value}; - return wysihtml5.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get(); + return wysihtml.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get(); } - wysihtml5.commands.createLink = { + wysihtml.commands.createLink = { exec: function(composer, command, value) { var opts = getOptions(value); @@ -13934,219 +12054,24 @@ wysihtml5.Commands = Base.extend( composer.selection.insertNode(textNode); composer.selection.selectNode(textNode); } - wysihtml5.commands.formatInline.exec(composer, command, opts); + wysihtml.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; + return wysihtml.commands.formatInline.state(composer, command, nodeOptions); } - }; -})(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; - } +})(wysihtml); - }; -})(wysihtml5); -;/* Formatblock +/* 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) { +(function(wysihtml) { - var dom = wysihtml5.dom, + var dom = wysihtml.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", @@ -14192,18 +12117,18 @@ wysihtml5.Commands = Base.extend( // Removes empty block level elements function cleanup(composer, newBlockElements) { - wysihtml5.dom.removeInvisibleSpaces(composer.element); + wysihtml.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 + elements = wysihtml.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]); + nbIdx = wysihtml.lang.array(newBlockElements).indexOf(elements[i]); if (nbIdx > -1) { newBlockElements.splice(nbIdx, 1); } @@ -14272,7 +12197,7 @@ wysihtml5.Commands = Base.extend( 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)); + element.appendChild(composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE)); } if (options.nodeName && element.nodeName !== options.nodeName) { @@ -14288,7 +12213,7 @@ wysihtml5.Commands = Base.extend( } if (options.styleProperty && typeof options.styleValue !== "undefined") { - element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue; } return element; @@ -14303,7 +12228,7 @@ wysihtml5.Commands = Base.extend( unwrapped = false; if (options.styleProperty) { - element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = ''; + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = ''; } if (options.className) { element.classList.remove(options.className); @@ -14345,8 +12270,8 @@ wysihtml5.Commands = Base.extend( 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}); + nextEl = wysihtml.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}), + prevEl = wysihtml.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() !== '') { @@ -14358,7 +12283,7 @@ wysihtml5.Commands = Base.extend( blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl); } } - wysihtml5.dom.unwrap(blocks[i]); + wysihtml.dom.unwrap(blocks[i]); } } @@ -14450,7 +12375,7 @@ wysihtml5.Commands = Base.extend( // Range starts before and ends inside the node tmpRange = ranges[i].cloneRange(); - closestLI = wysihtml5.dom.domNode(lis[j]).prev({nodeTypes: [1]}); + closestLI = wysihtml.dom.domNode(lis[j]).prev({nodeTypes: [1]}); if (closestLI) { tmpRange.setEnd(closestLI, closestLI.childNodes.length); @@ -14472,7 +12397,7 @@ wysihtml5.Commands = Base.extend( 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]}); + closestLI = wysihtml.dom.domNode(lis[j]).next({nodeTypes: [1]}); if (closestLI) { ranges[i].setStart(closestLI, 0); } else if (lis[j].closest('ul, ol')) { @@ -14493,7 +12418,7 @@ wysihtml5.Commands = Base.extend( // 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; + var correctedOptions = (options) ? wysihtml.lang.object(options).clone(true) : null; if (correctedOptions) { correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer); } @@ -14515,8 +12440,8 @@ wysihtml5.Commands = Base.extend( 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}); + next = wysihtml.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true}); + prev = wysihtml.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); @@ -14566,7 +12491,7 @@ wysihtml5.Commands = Base.extend( // Split block formating and add new block to wrap caret unwrapBlocksFromContent(content.firstChild); - children = wysihtml5.dom.unwrap(content.firstChild); + children = wysihtml.dom.unwrap(content.firstChild); // Add line break before if needed if (children.length > 0) { @@ -14597,14 +12522,14 @@ wysihtml5.Commands = Base.extend( first = false; } - blocks = wysihtml5.lang.array(fragment.childNodes).get(); + blocks = wysihtml.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}); + var prevPrev = prevNode && wysihtml.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); if (isBr(nextNode)) { nextNode.parentNode.removeChild(nextNode); } @@ -14672,7 +12597,7 @@ wysihtml5.Commands = Base.extend( 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}), + prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}), nextNode = ( ( r.endContainer.nodeType === 1 && @@ -14680,23 +12605,23 @@ wysihtml5.Commands = Base.extend( ( endNode.nodeType === 1 || !isWhitespaceAfter(endNode, r.endOffset) && - !wysihtml5.dom.domNode(endNode).is.rangyBookmark() + !wysihtml.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}), + ) ? endNode : wysihtml.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, + similarOuterBlock = similarOptions ? wysihtml.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()) { + if (wysihtml.dom.domNode(nextNode).is.rangyBookmark()) { endNode = nextNode; nextNode = endNode.nextSibling; } @@ -14746,7 +12671,7 @@ wysihtml5.Commands = Base.extend( } } - blocks = wysihtml5.lang.array(fragment.childNodes).get(); + blocks = wysihtml.lang.array(fragment.childNodes).get(); } injectFragmentToRange(fragment, r, composer, firstOuterBlock); removeSurroundingLineBreaks(prevNode, nextNode, composer); @@ -14755,7 +12680,7 @@ wysihtml5.Commands = Base.extend( // (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() + typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark() ) ) { blocks[blocks.length - 1].appendChild(composer.doc.createElement('br')); @@ -14765,7 +12690,7 @@ wysihtml5.Commands = Base.extend( // Find closest block level element function getParentBlockNodeName(element, composer) { - var parentNode = wysihtml5.dom.getParentElement(element, { + var parentNode = wysihtml.dom.getParentElement(element, { query: BLOCK_ELEMENTS }, null, composer.element); @@ -14780,7 +12705,7 @@ wysihtml5.Commands = Base.extend( // // If nothing found selects the current line function expandCaretToBlock(composer, insertingNodeName) { - var parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, { + var parent = wysihtml.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, { query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'), }, null, composer.element), range; @@ -14846,13 +12771,13 @@ wysihtml5.Commands = Base.extend( if (caretInfo && caretInfo.caretNode) { if ( // caret is allready breaknode - wysihtml5.dom.domNode(caretInfo.caretNode).is.lineBreak() || + wysihtml.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())) || + (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml.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()) + (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.prevNode).is.block()) && + (!caretInfo.nextNode || wysihtml.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.nextNode).is.block()) ) ) { return true; @@ -14862,7 +12787,7 @@ wysihtml5.Commands = Base.extend( return false; } - wysihtml5.commands.formatBlock = { + wysihtml.commands.formatBlock = { exec: function(composer, command, options) { options = parseOptions(options); var newBlockElements = [], @@ -14938,7 +12863,7 @@ wysihtml5.Commands = Base.extend( 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 }); + return wysihtml.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS }); }).bind(this)), parentNodes = composer.selection.getSelectedOwnNodes(), parent; @@ -14955,67 +12880,14 @@ wysihtml5.Commands = Base.extend( } }; -})(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"); +})(wysihtml); - 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) { +(function(wysihtml) { 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", @@ -15037,8 +12909,8 @@ wysihtml5.Commands = Base.extend( // 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(); + var attr = wysihtml.dom.getAttributes(element); + return wysihtml.lang.object(attr).isEmpty(); } // compares two nodes if they are semantically the same @@ -15057,14 +12929,14 @@ wysihtml5.Commands = Base.extend( 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) { + if (wysihtml.lang.array(classes1).without(classes2).length > 0) { return false; } - attr1 = wysihtml5.dom.getAttributes(element1); - attr2 = wysihtml5.dom.getAttributes(element2); + attr1 = wysihtml.dom.getAttributes(element1); + attr2 = wysihtml.dom.getAttributes(element2); - if (attr1.length !== attr2.length || !wysihtml5.lang.object(wysihtml5.lang.object(attr1).difference(attr2)).isEmpty()) { + if (attr1.length !== attr2.length || !wysihtml.lang.object(wysihtml.lang.object(attr1).difference(attr2)).isEmpty()) { return false; } @@ -15085,7 +12957,7 @@ wysihtml5.Commands = Base.extend( } if (options.styleProperty && typeof options.styleValue !== "undefined") { - element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue; } if (options.attribute) { @@ -15119,9 +12991,9 @@ wysihtml5.Commands = Base.extend( // If attrbutes and values are the same > remove // if attributes or values function updateElementAttributes(element, newAttributes, toggle) { - var attr = wysihtml5.dom.getAttributes(element), + var attr = wysihtml.dom.getAttributes(element), fullContain = containsSameAttributes(newAttributes, attr), - attrDifference = wysihtml5.lang.object(attr).difference(newAttributes), + attrDifference = wysihtml.lang.object(attr).difference(newAttributes), a, b; if (fullContain && toggle !== false) { @@ -15132,7 +13004,7 @@ wysihtml5.Commands = Base.extend( } } else { - /*if (!wysihtml5.lang.object(attrDifference).isEmpty()) { + /*if (!wysihtml.lang.object(attrDifference).isEmpty()) { for (b in attrDifference) { if (attrDifference.hasOwnProperty(b)) { element.removeAttribute(b); @@ -15167,10 +13039,10 @@ wysihtml5.Commands = Base.extend( // 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)] = ''; + if (options.toggle !== false && element.style[wysihtml.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) { + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = ''; } else { - element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue; } } if (hasNoStyle(element)) { @@ -15190,7 +13062,7 @@ wysihtml5.Commands = Base.extend( // 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; + nodeQueryMatch = nodeNameQuery ? wysihtml.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 @@ -15199,7 +13071,7 @@ wysihtml5.Commands = Base.extend( ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) && hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element) ) { - wysihtml5.dom.unwrap(element); + wysihtml.dom.unwrap(element); } } @@ -15213,7 +13085,7 @@ wysihtml5.Commands = Base.extend( if (!selection.isCollapsed()) { textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) { // Exclude empty nodes except caret node - return (!wysihtml5.dom.domNode(node).is.emptyTextNode()); + return (!wysihtml.dom.domNode(node).is.emptyTextNode()); }, splitBounds)); } @@ -15249,11 +13121,11 @@ wysihtml5.Commands = Base.extend( var o; if (options.nodeName) { var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase(); - return wysihtml5.dom.domNode(node).test({ query: query }); + return wysihtml.dom.domNode(node).test({ query: query }); } else { - o = wysihtml5.lang.object(options).clone(); + o = wysihtml.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); + return wysihtml.dom.domNode(node).test(o); } } @@ -15269,7 +13141,7 @@ wysihtml5.Commands = Base.extend( try { rangy.getSelection(composer.win).addRange(range); } catch (e) {} - if (!composer.doc.activeElement || !wysihtml5.dom.contains(composer.element, composer.doc.activeElement)) { + if (!composer.doc.activeElement || !wysihtml.dom.contains(composer.element, composer.doc.activeElement)) { composer.element.focus(); d.scrollTop = oldScrollTop; d.scrollLeft = oldScrollLeft; @@ -15386,7 +13258,7 @@ wysihtml5.Commands = Base.extend( range.setStartAndEnd(anchor, offsetStart, offsetEnd); range.splitBoundaries(); txtNodes = range.getNodes([3], function(node) { - return (!wysihtml5.dom.domNode(node).is.emptyTextNode()); + return (!wysihtml.dom.domNode(node).is.emptyTextNode()); }); return { @@ -15458,7 +13330,7 @@ wysihtml5.Commands = Base.extend( if (wrapNode) { newWrapNode = wrapNode.cloneNode(false); - wysihtml5.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode); + wysihtml.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode); updateFormatOfElement(newWrapNode, options); } } @@ -15469,7 +13341,7 @@ wysihtml5.Commands = Base.extend( wrapNode = findSimilarTextNodeWrapper(textNode, options, container); if (wrapNode) { - wysihtml5.dom.domNode(textNode).escapeParent(wrapNode); + wysihtml.dom.domNode(textNode).escapeParent(wrapNode); } } @@ -15503,7 +13375,7 @@ wysihtml5.Commands = Base.extend( } else { // Escape caret out of format - textNode = composer.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + textNode = composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE); newNode = state.nodes[0].cloneNode(false); newNode.appendChild(textNode); composer.selection.splitElementAtCaret(state.nodes[0], newNode); @@ -15568,7 +13440,7 @@ wysihtml5.Commands = Base.extend( textOffset = selection.anchorOffset; for (i = state.nodes.length; i--;) { - wysihtml5.dom.unwrap(state.nodes[i]); + wysihtml.dom.unwrap(state.nodes[i]); } cleanupAndSetCaret(composer, textNode, textOffset, options); @@ -15616,7 +13488,7 @@ wysihtml5.Commands = Base.extend( return options; } - wysihtml5.commands.formatInline = { + wysihtml.commands.formatInline = { // Basics: // In case of plain text or inline state not set wrap all non-empty textnodes with @@ -15648,195 +13520,93 @@ wysihtml5.Commands = Base.extend( 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; + // Text allready has the format applied + removeFormat(composer, textNodes, state, options); } + + composer.element.normalize(); + }, - // Otherwise lets create the image - image = doc.createElement(NODE_NAME); + state: function(composer, command, options) { + options = fixOptions(options); + var nodes = getState(composer, options, true).nodes; + return (nodes.length === 0) ? false : nodes; + } + }; - for (var i in value) { - image.setAttribute(i === "className" ? "class" : i, value[i]); - } +})(wysihtml); - 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); +(function(wysihtml){ + wysihtml.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) { - var doc = composer.doc, - selectedNode, - text, - imagesInSelection; - - if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) { + state: function(composer, command) { return false; - } + }, - selectedNode = composer.selection.getSelectedNode(); - if (!selectedNode) { - return false; - } + tryToPushLiLevel: function(liNodes, selection) { + var listTag, list, prevLi, liNode, prevLiList, + found = false; - if (selectedNode.nodeName === NODE_NAME) { - // This works perfectly in IE - return selectedNode; - } + selection.executeAndRestoreRangy(function() { - if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) { - return false; - } + for (var i = liNodes.length; i--;) { + liNode = liNodes[i]; + listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL'; + list = liNode.ownerDocument.createElement(listTag); + prevLi = wysihtml.dom.domNode(liNode).prev({nodeTypes: [wysihtml.ELEMENT_NODE]}); + prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null; - text = composer.selection.getText(); - text = wysihtml5.lang.string(text).trim(); - if (text) { - return false; - } + if (prevLi) { + if (prevLiList) { + prevLiList.appendChild(liNode); + } else { + list.appendChild(liNode); + prevLi.appendChild(list); + } + found = true; + } + } - imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) { - return node.nodeName === "IMG"; }); - - if (imagesInSelection.length !== 1) { - return false; - } - - return imagesInSelection[0]; + return found; } }; -})(wysihtml5); -;(function(wysihtml5) { - var LINE_BREAK = "
" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); +}(wysihtml)); - wysihtml5.commands.insertLineBreak = { - exec: function(composer, command) { - composer.selection.insertHTML(LINE_BREAK); +(function(wysihtml){ + wysihtml.commands.insertHTML = { + exec: function(composer, command, html) { + composer.selection.insertHTML(html); }, state: function() { return false; } }; -})(wysihtml5); -;(function(wysihtml5){ - wysihtml5.commands.insertOrderedList = { - exec: function(composer, command) { - wysihtml5.commands.insertList.exec(composer, command, "OL"); - }, +}(wysihtml)); - state: function(composer, command) { - return wysihtml5.commands.insertList.state(composer, command, "OL"); - } - }; -}(wysihtml5)); -;(function(wysihtml5){ - wysihtml5.commands.insertUnorderedList = { +(function(wysihtml) { + var LINE_BREAK = "
" + (wysihtml.browser.needsSpaceAfterLineBreak() ? " " : ""); + + wysihtml.commands.insertLineBreak = { exec: function(composer, command) { - wysihtml5.commands.insertList.exec(composer, command, "UL"); + composer.selection.insertHTML(LINE_BREAK); }, - state: function(composer, command) { - return wysihtml5.commands.insertList.state(composer, command, "UL"); + state: function() { + return false; } }; -}(wysihtml5)); -;wysihtml5.commands.insertList = (function(wysihtml5) { +})(wysihtml); + +wysihtml.commands.insertList = (function(wysihtml) { var isNode = function(node, name) { if (node && node.nodeName) { @@ -15859,7 +13629,7 @@ wysihtml5.Commands = Base.extend( }; if (node) { - var parentLi = wysihtml5.dom.getParentElement(node, { query: "li" }, false, composer.element), + var parentLi = wysihtml.dom.getParentElement(node, { query: "li" }, false, composer.element), otherNodeName = (nodeName === "UL") ? "OL" : "UL"; if (isNode(node, nodeName)) { @@ -15901,511 +13671,138 @@ wysihtml5.Commands = Base.extend( 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 - //- // 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|(
- foo
- bar
))+$/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); - } + wysihtml.dom.renameElement(otherLists[l], nodeName.toLowerCase()); + } + } else { + innerLists = getListsInSelection(['OL', 'UL'], composer); + for (var i = innerLists.length; i--;) { + wysihtml.dom.resolveList(innerLists[i], composer.config.useLineBreaks); + } + if (innerLists.length === 0) { + wysihtml.dom.resolveList(el, composer.config.useLineBreaks); + } + } + }); }; -})(wysihtml5); -;(function(wysihtml5) { + var handleOtherTypeList = function(el, nodeName, composer) { + var otherNodeName = (nodeName === "UL") ? "OL" : "UL"; + // Turn an ordered list into an unordered list + //+ // becomes: + //
- foo
- bar
+ // Also rename other lists in selection + composer.selection.executeAndRestoreRangy(function() { + var renameLists = [el].concat(getListsInSelection(otherNodeName, composer)); - var nodeOptions = { - styleProperty: "textAlign", - styleValue: "center", - toggle: true + // All selection inner lists get renamed too + for (var l = renameLists.length; l--;) { + wysihtml.dom.renameElement(renameLists[l], nodeName.toLowerCase()); + } + }); }; - 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); - } - }; + var getListsInSelection = function(nodeName, composer) { + var ranges = composer.selection.getOwnRanges(), + renameLists = []; -})(wysihtml5); -;(function(wysihtml5) { + for (var r = ranges.length; r--;) { + renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) { + return isNode(node, nodeName); + })); + } - var nodeOptions = { - styleProperty: "textAlign", - styleValue: "justify", - toggle: true + return renameLists; }; - 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(); - }, + var createListFallback = function(nodeName, composer) { + var sel = rangy.saveSelection(composer.win); - state: function(composer) { - return false; - } - }; -}(wysihtml5)); -;(function(wysihtml5){ + // Fallback for Create list + var tempClassName = "_wysihtml-temp-" + new Date().getTime(), + isEmpty, list; - var nodeOptions = { - nodeName: "U", - toggle: true - }; + composer.commands.exec("formatBlock", { + "nodeName": "div", + "className": tempClassName + }); - wysihtml5.commands.underline = { - exec: function(composer, command) { - wysihtml5.commands.formatInline.exec(composer, command, nodeOptions); - }, + var tempElement = composer.element.querySelector("." + tempClassName); - state: function(composer, command) { - return wysihtml5.commands.formatInline.state(composer, command, nodeOptions); + // This space causes new lists to never break on enter + var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; + tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); + if (tempElement) { + isEmpty = (/^(\s|(
- foo
- bar
))+$/i).test(tempElement.innerHTML); + list = wysihtml.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer); + if (sel) { + rangy.restoreSelection(sel); + } + if (isEmpty) { + composer.selection.selectNode(list.querySelector("li"), true); + } } }; -}(wysihtml5)); -;(function(wysihtml5){ - wysihtml5.commands.undo = { - exec: function(composer) { - return composer.undoManager.undo(); - }, + 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; - 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 = "