liquid_feedback_frontend
diff static/wysihtml/wysihtml.js @ 1309:32cc544d5a5b
Cumulative patch for upcoming frontend version 4
author | bsw/jbe |
---|---|
date | Sun Jul 15 14:07:29 2018 +0200 (2018-07-15) |
parents | |
children |
line diff
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/static/wysihtml/wysihtml.js Sun Jul 15 14:07:29 2018 +0200 1.3 @@ -0,0 +1,16102 @@ 1.4 +/** 1.5 + * @license wysihtml v0.6.0-beta1 1.6 + * https://github.com/Voog/wysihtml 1.7 + * 1.8 + * Author: Christopher Blum (https://github.com/tiff) 1.9 + * Secondary author of extended features: Oliver Pulges (https://github.com/pulges) 1.10 + * 1.11 + * Copyright (C) 2012 XING AG 1.12 + * Licensed under the MIT license (MIT) 1.13 + * 1.14 + */ 1.15 +var wysihtml = { 1.16 + version: '0.6.0-beta1', 1.17 + 1.18 + // namespaces 1.19 + commands: {}, 1.20 + dom: {}, 1.21 + quirks: {}, 1.22 + toolbar: {}, 1.23 + lang: {}, 1.24 + selection: {}, 1.25 + views: {}, 1.26 + 1.27 + editorExtenders: [], 1.28 + extendEditor: function(extender) { 1.29 + this.editorExtenders.push(extender); 1.30 + }, 1.31 + 1.32 + INVISIBLE_SPACE: '\uFEFF', 1.33 + INVISIBLE_SPACE_REG_EXP: /\uFEFF/g, 1.34 + 1.35 + VOID_ELEMENTS: 'area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr', 1.36 + PERMITTED_PHRASING_CONTENT_ONLY: 'h1, h2, h3, h4, h5, h6, p, pre', 1.37 + 1.38 + EMPTY_FUNCTION: function() {}, 1.39 + 1.40 + ELEMENT_NODE: 1, 1.41 + TEXT_NODE: 3, 1.42 + 1.43 + BACKSPACE_KEY: 8, 1.44 + ENTER_KEY: 13, 1.45 + ESCAPE_KEY: 27, 1.46 + SPACE_KEY: 32, 1.47 + TAB_KEY: 9, 1.48 + DELETE_KEY: 46 1.49 +}; 1.50 + 1.51 +wysihtml.polyfills = function(win, doc) { 1.52 + 1.53 + var methods = { 1.54 + 1.55 + // Safary has a bug of not restoring selection after node.normalize correctly. 1.56 + // Detects the misbegaviour and patches it 1.57 + normalizeHasCaretError: function() { 1.58 + if ("createRange" in doc && "getSelection" in win) { 1.59 + var originalTarget, 1.60 + scrollTop = window.pageYOffset, 1.61 + scrollLeft = window.pageXOffset, 1.62 + e = doc.createElement('div'), 1.63 + t1 = doc.createTextNode('a'), 1.64 + t2 = doc.createTextNode('a'), 1.65 + t3 = doc.createTextNode('a'), 1.66 + r = doc.createRange(), 1.67 + s, ret; 1.68 + 1.69 + if (document.activeElement) { 1.70 + if (document.activeElement.nodeType === 1 && ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].indexOf(document.activeElement.nodeName) > -1) { 1.71 + originalTarget = { 1.72 + type: 'form', 1.73 + node: document.activeElement, 1.74 + start: document.activeElement.selectionStart, 1.75 + end: document.activeElement.selectionEnd, 1.76 + }; 1.77 + } else { 1.78 + s = win.getSelection(); 1.79 + if (s && s.anchorNode) { 1.80 + originalTarget = { 1.81 + type: 'range', 1.82 + anchorNode: s.anchorNode, 1.83 + anchorOffset: s.anchorOffset, 1.84 + focusNode: s.focusNode, 1.85 + focusOffset: s.focusOffset 1.86 + }; 1.87 + } 1.88 + } 1.89 + } 1.90 + 1.91 + e.setAttribute('contenteditable', 'true'); 1.92 + e.appendChild(t1); 1.93 + e.appendChild(t2); 1.94 + e.appendChild(t3); 1.95 + doc.body.appendChild(e); 1.96 + r.setStart(t2, 1); 1.97 + r.setEnd(t2, 1); 1.98 + 1.99 + s = win.getSelection(); 1.100 + s.removeAllRanges(); 1.101 + s.addRange(r); 1.102 + e.normalize(); 1.103 + s = win.getSelection(); 1.104 + 1.105 + ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2); 1.106 + e.parentNode.removeChild(e); 1.107 + s.removeAllRanges(); 1.108 + 1.109 + if (originalTarget) { 1.110 + if (originalTarget.type === 'form') { 1.111 + // The selection parameters are not present for all form elements 1.112 + if (typeof originalTarget.start !== 'undefined' && typeof originalTarget.end !== 'undefined') { 1.113 + originalTarget.node.setSelectionRange(originalTarget.start, originalTarget.end); 1.114 + } 1.115 + originalTarget.node.focus(); 1.116 + } else if (originalTarget.type === 'range') { 1.117 + r = doc.createRange(); 1.118 + r.setStart(originalTarget.anchorNode, originalTarget.anchorOffset); 1.119 + r.setEnd(originalTarget.focusNode, originalTarget.focusOffset); 1.120 + s.addRange(r); 1.121 + } 1.122 + } 1.123 + 1.124 + if (scrollTop !== window.pageYOffset || scrollLeft !== window.pageXOffset) { 1.125 + win.scrollTo(scrollLeft, scrollTop); 1.126 + } 1.127 + 1.128 + return ret; 1.129 + } 1.130 + }, 1.131 + 1.132 + apply: function() { 1.133 + // closest, matches, and remove polyfill 1.134 + // https://github.com/jonathantneal/closest 1.135 + (function (ELEMENT) { 1.136 + ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) { 1.137 + var 1.138 + element = this, 1.139 + elements = (element.document || element.ownerDocument).querySelectorAll(selector), 1.140 + index = 0; 1.141 + 1.142 + while (elements[index] && elements[index] !== element) { 1.143 + ++index; 1.144 + } 1.145 + 1.146 + return elements[index] ? true : false; 1.147 + }; 1.148 + 1.149 + ELEMENT.closest = ELEMENT.closest || function closest(selector) { 1.150 + var element = this; 1.151 + 1.152 + while (element) { 1.153 + if (element.matches(selector)) { 1.154 + break; 1.155 + } 1.156 + 1.157 + element = element.parentElement; 1.158 + } 1.159 + 1.160 + return element; 1.161 + }; 1.162 + 1.163 + ELEMENT.remove = ELEMENT.remove || function remove() { 1.164 + if (this.parentNode) { 1.165 + this.parentNode.removeChild(this); 1.166 + } 1.167 + }; 1.168 + 1.169 + }(win.Element.prototype)); 1.170 + 1.171 + if (!('classList' in doc.documentElement) && win.Object.defineProperty && typeof win.HTMLElement !== 'undefined') { 1.172 + win.Object.defineProperty(win.HTMLElement.prototype, 'classList', { 1.173 + get: function() { 1.174 + var self = this; 1.175 + function update(fn) { 1.176 + return function(value) { 1.177 + var classes = self.className.split(/\s+/), 1.178 + index = classes.indexOf(value); 1.179 + 1.180 + fn(classes, index, value); 1.181 + self.className = classes.join(' '); 1.182 + }; 1.183 + } 1.184 + 1.185 + var ret = { 1.186 + add: update(function(classes, index, value) { 1.187 + ~index || classes.push(value); 1.188 + }), 1.189 + 1.190 + remove: update(function(classes, index) { 1.191 + ~index && classes.splice(index, 1); 1.192 + }), 1.193 + 1.194 + toggle: update(function(classes, index, value) { 1.195 + ~index ? classes.splice(index, 1) : classes.push(value); 1.196 + }), 1.197 + 1.198 + contains: function(value) { 1.199 + return !!~self.className.split(/\s+/).indexOf(value); 1.200 + }, 1.201 + 1.202 + item: function(i) { 1.203 + return self.className.split(/\s+/)[i] || null; 1.204 + } 1.205 + }; 1.206 + 1.207 + win.Object.defineProperty(ret, 'length', { 1.208 + get: function() { 1.209 + return self.className.split(/\s+/).length; 1.210 + } 1.211 + }); 1.212 + 1.213 + return ret; 1.214 + } 1.215 + }); 1.216 + } 1.217 + 1.218 + var getTextNodes = function(node){ 1.219 + var all = []; 1.220 + for (node=node.firstChild;node;node=node.nextSibling){ 1.221 + if (node.nodeType == 3) { 1.222 + all.push(node); 1.223 + } else { 1.224 + all = all.concat(getTextNodes(node)); 1.225 + } 1.226 + } 1.227 + return all; 1.228 + }; 1.229 + 1.230 + var isInDom = function(node) { 1.231 + var doc = node.ownerDocument, 1.232 + n = node; 1.233 + 1.234 + do { 1.235 + if (n === doc) { 1.236 + return true; 1.237 + } 1.238 + n = n.parentNode; 1.239 + } while(n); 1.240 + 1.241 + return false; 1.242 + }; 1.243 + 1.244 + var normalizeFix = function() { 1.245 + var f = win.Node.prototype.normalize; 1.246 + var nf = function() { 1.247 + var texts = getTextNodes(this), 1.248 + s = this.ownerDocument.defaultView.getSelection(), 1.249 + anode = s.anchorNode, 1.250 + aoffset = s.anchorOffset, 1.251 + aelement = anode && anode.nodeType === 1 && anode.childNodes.length > 0 ? anode.childNodes[aoffset] : undefined, 1.252 + fnode = s.focusNode, 1.253 + foffset = s.focusOffset, 1.254 + felement = fnode && fnode.nodeType === 1 && foffset > 0 ? fnode.childNodes[foffset -1] : undefined, 1.255 + r = this.ownerDocument.createRange(), 1.256 + prevTxt = texts.shift(), 1.257 + curText = prevTxt ? texts.shift() : null; 1.258 + 1.259 + if (felement && felement.nodeType === 3) { 1.260 + fnode = felement; 1.261 + foffset = felement.nodeValue.length; 1.262 + felement = undefined; 1.263 + } 1.264 + 1.265 + if (aelement && aelement.nodeType === 3) { 1.266 + anode = aelement; 1.267 + aoffset = 0; 1.268 + aelement = undefined; 1.269 + } 1.270 + 1.271 + if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_CONTAINS))) { 1.272 + fnode = [anode, anode = fnode][0]; 1.273 + foffset = [aoffset, aoffset = foffset][0]; 1.274 + } 1.275 + 1.276 + while(prevTxt && curText) { 1.277 + if (curText.previousSibling && curText.previousSibling === prevTxt) { 1.278 + if (anode === curText) { 1.279 + anode = prevTxt; 1.280 + aoffset = prevTxt.nodeValue.length + aoffset; 1.281 + } 1.282 + if (fnode === curText) { 1.283 + fnode = prevTxt; 1.284 + foffset = prevTxt.nodeValue.length + foffset; 1.285 + } 1.286 + prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue; 1.287 + curText.parentNode.removeChild(curText); 1.288 + curText = texts.shift(); 1.289 + } else { 1.290 + prevTxt = curText; 1.291 + curText = texts.shift(); 1.292 + } 1.293 + } 1.294 + 1.295 + if (felement) { 1.296 + foffset = Array.prototype.indexOf.call(felement.parentNode.childNodes, felement) + 1; 1.297 + } 1.298 + 1.299 + if (aelement) { 1.300 + aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement); 1.301 + } 1.302 + 1.303 + if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) { 1.304 + r.setStart(anode, aoffset); 1.305 + r.setEnd(fnode, foffset); 1.306 + s.removeAllRanges(); 1.307 + s.addRange(r); 1.308 + } 1.309 + }; 1.310 + win.Node.prototype.normalize = nf; 1.311 + }; 1.312 + 1.313 + var F = function() { 1.314 + win.removeEventListener("load", F); 1.315 + if ("Node" in win && "normalize" in win.Node.prototype && methods.normalizeHasCaretError()) { 1.316 + normalizeFix(); 1.317 + } 1.318 + }; 1.319 + 1.320 + if (doc.readyState !== "complete") { 1.321 + win.addEventListener("load", F); 1.322 + } else { 1.323 + F(); 1.324 + } 1.325 + 1.326 + // CustomEvent for ie9 and up 1.327 + function nativeCustomEventSupported() { 1.328 + try { 1.329 + var p = new win.CustomEvent('cat', {detail: {foo: 'bar'}}); 1.330 + return 'cat' === p.type && 'bar' === p.detail.foo; 1.331 + } catch (e) {} 1.332 + return false; 1.333 + } 1.334 + 1.335 + // Polyfills CustomEvent object for IE9 and up 1.336 + (function() { 1.337 + if (!nativeCustomEventSupported() && "CustomEvent" in win) { 1.338 + function CustomEvent(event, params) { 1.339 + params = params || {bubbles: false, cancelable: false, detail: undefined}; 1.340 + var evt = doc.createEvent('CustomEvent'); 1.341 + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); 1.342 + return evt; 1.343 + } 1.344 + CustomEvent.prototype = win.Event.prototype; 1.345 + win.CustomEvent = CustomEvent; 1.346 + } 1.347 + })(); 1.348 + } 1.349 + } 1.350 + 1.351 + return methods; 1.352 +}; 1.353 + 1.354 +wysihtml.polyfills(window, document).apply(); 1.355 + 1.356 +/* 1.357 + Base.js, version 1.1a 1.358 + Copyright 2006-2010, Dean Edwards 1.359 + License: http://www.opensource.org/licenses/mit-license.php 1.360 +*/ 1.361 + 1.362 +var Base = function() { 1.363 + // dummy 1.364 +}; 1.365 + 1.366 +Base.extend = function(_instance, _static) { // subclass 1.367 + var extend = Base.prototype.extend; 1.368 + 1.369 + // build the prototype 1.370 + Base._prototyping = true; 1.371 + var proto = new this; 1.372 + extend.call(proto, _instance); 1.373 + proto.base = function() { 1.374 + // call this method from any other method to invoke that method's ancestor 1.375 + }; 1.376 + delete Base._prototyping; 1.377 + 1.378 + // create the wrapper for the constructor function 1.379 + //var constructor = proto.constructor.valueOf(); //-dean 1.380 + var constructor = proto.constructor; 1.381 + var klass = proto.constructor = function() { 1.382 + if (!Base._prototyping) { 1.383 + if (this._constructing || this.constructor == klass) { // instantiation 1.384 + this._constructing = true; 1.385 + constructor.apply(this, arguments); 1.386 + delete this._constructing; 1.387 + } else if (arguments[0] != null) { // casting 1.388 + return (arguments[0].extend || extend).call(arguments[0], proto); 1.389 + } 1.390 + } 1.391 + }; 1.392 + 1.393 + // build the class interface 1.394 + klass.ancestor = this; 1.395 + klass.extend = this.extend; 1.396 + klass.forEach = this.forEach; 1.397 + klass.implement = this.implement; 1.398 + klass.prototype = proto; 1.399 + klass.toString = this.toString; 1.400 + klass.valueOf = function(type) { 1.401 + //return (type == "object") ? klass : constructor; //-dean 1.402 + return (type == "object") ? klass : constructor.valueOf(); 1.403 + }; 1.404 + extend.call(klass, _static); 1.405 + // class initialisation 1.406 + if (typeof klass.init == "function") klass.init(); 1.407 + return klass; 1.408 +}; 1.409 + 1.410 +Base.prototype = { 1.411 + extend: function(source, value) { 1.412 + if (arguments.length > 1) { // extending with a name/value pair 1.413 + var ancestor = this[source]; 1.414 + if (ancestor && (typeof value == "function") && // overriding a method? 1.415 + // the valueOf() comparison is to avoid circular references 1.416 + (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && 1.417 + /\bbase\b/.test(value)) { 1.418 + // get the underlying method 1.419 + var method = value.valueOf(); 1.420 + // override 1.421 + value = function() { 1.422 + var previous = this.base || Base.prototype.base; 1.423 + this.base = ancestor; 1.424 + var returnValue = method.apply(this, arguments); 1.425 + this.base = previous; 1.426 + return returnValue; 1.427 + }; 1.428 + // point to the underlying method 1.429 + value.valueOf = function(type) { 1.430 + return (type == "object") ? value : method; 1.431 + }; 1.432 + value.toString = Base.toString; 1.433 + } 1.434 + this[source] = value; 1.435 + } else if (source) { // extending with an object literal 1.436 + var extend = Base.prototype.extend; 1.437 + // if this object has a customised extend method then use it 1.438 + if (!Base._prototyping && typeof this != "function") { 1.439 + extend = this.extend || extend; 1.440 + } 1.441 + var proto = {toSource: null}; 1.442 + // do the "toString" and other methods manually 1.443 + var hidden = ["constructor", "toString", "valueOf"]; 1.444 + // if we are prototyping then include the constructor 1.445 + var i = Base._prototyping ? 0 : 1; 1.446 + while (key = hidden[i++]) { 1.447 + if (source[key] != proto[key]) { 1.448 + extend.call(this, key, source[key]); 1.449 + 1.450 + } 1.451 + } 1.452 + // copy each of the source object's properties to this object 1.453 + for (var key in source) { 1.454 + if (!proto[key]) extend.call(this, key, source[key]); 1.455 + } 1.456 + } 1.457 + return this; 1.458 + } 1.459 +}; 1.460 + 1.461 +// initialise 1.462 +Base = Base.extend({ 1.463 + constructor: function() { 1.464 + this.extend(arguments[0]); 1.465 + } 1.466 +}, { 1.467 + ancestor: Object, 1.468 + version: "1.1", 1.469 + 1.470 + forEach: function(object, block, context) { 1.471 + for (var key in object) { 1.472 + if (this.prototype[key] === undefined) { 1.473 + block.call(context, object[key], key, object); 1.474 + } 1.475 + } 1.476 + }, 1.477 + 1.478 + implement: function() { 1.479 + for (var i = 0; i < arguments.length; i++) { 1.480 + if (typeof arguments[i] == "function") { 1.481 + // if it's a function, call it 1.482 + arguments[i](this.prototype); 1.483 + } else { 1.484 + // add the interface using the extend method 1.485 + this.prototype.extend(arguments[i]); 1.486 + } 1.487 + } 1.488 + return this; 1.489 + }, 1.490 + 1.491 + toString: function() { 1.492 + return String(this.valueOf()); 1.493 + } 1.494 +}); 1.495 +/** 1.496 + * Rangy, a cross-browser JavaScript range and selection library 1.497 + * https://github.com/timdown/rangy 1.498 + * 1.499 + * Copyright 2015, Tim Down 1.500 + * Licensed under the MIT license. 1.501 + * Version: 1.3.1-dev 1.502 + * Build date: 20 May 2015 1.503 + * 1.504 + * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges) 1.505 + */ 1.506 +var rangy; 1.507 + 1.508 +(function() { 1.509 + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; 1.510 + 1.511 + // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START 1.512 + // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. 1.513 + var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 1.514 + "commonAncestorContainer"]; 1.515 + 1.516 + // Minimal set of methods required for DOM Level 2 Range compliance 1.517 + var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", 1.518 + "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", 1.519 + "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; 1.520 + 1.521 + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; 1.522 + 1.523 + // Subset of TextRange's full set of methods that we're interested in 1.524 + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", 1.525 + "setEndPoint", "getBoundingClientRect"]; 1.526 + 1.527 + /*----------------------------------------------------------------------------------------------------------------*/ 1.528 + 1.529 + // Trio of functions taken from Peter Michaux's article: 1.530 + // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting 1.531 + function isHostMethod(o, p) { 1.532 + var t = typeof o[p]; 1.533 + return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; 1.534 + } 1.535 + 1.536 + function isHostObject(o, p) { 1.537 + return !!(typeof o[p] == OBJECT && o[p]); 1.538 + } 1.539 + 1.540 + function isHostProperty(o, p) { 1.541 + return typeof o[p] != UNDEFINED; 1.542 + } 1.543 + 1.544 + // Creates a convenience function to save verbose repeated calls to tests functions 1.545 + function createMultiplePropertyTest(testFunc) { 1.546 + return function(o, props) { 1.547 + var i = props.length; 1.548 + while (i--) { 1.549 + if (!testFunc(o, props[i])) { 1.550 + return false; 1.551 + } 1.552 + } 1.553 + return true; 1.554 + }; 1.555 + } 1.556 + 1.557 + // Next trio of functions are a convenience to save verbose repeated calls to previous two functions 1.558 + var areHostMethods = createMultiplePropertyTest(isHostMethod); 1.559 + var areHostObjects = createMultiplePropertyTest(isHostObject); 1.560 + var areHostProperties = createMultiplePropertyTest(isHostProperty); 1.561 + 1.562 + function isTextRange(range) { 1.563 + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); 1.564 + } 1.565 + 1.566 + function getBody(doc) { 1.567 + return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; 1.568 + } 1.569 + 1.570 + var forEach = [].forEach ? 1.571 + function(arr, func) { 1.572 + arr.forEach(func); 1.573 + } : 1.574 + function(arr, func) { 1.575 + for (var i = 0, len = arr.length; i < len; ++i) { 1.576 + func(arr[i], i); 1.577 + } 1.578 + }; 1.579 + 1.580 + var modules = {}; 1.581 + 1.582 + var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED); 1.583 + 1.584 + var util = { 1.585 + isHostMethod: isHostMethod, 1.586 + isHostObject: isHostObject, 1.587 + isHostProperty: isHostProperty, 1.588 + areHostMethods: areHostMethods, 1.589 + areHostObjects: areHostObjects, 1.590 + areHostProperties: areHostProperties, 1.591 + isTextRange: isTextRange, 1.592 + getBody: getBody, 1.593 + forEach: forEach 1.594 + }; 1.595 + 1.596 + var api = { 1.597 + version: "1.3.1-dev", 1.598 + initialized: false, 1.599 + isBrowser: isBrowser, 1.600 + supported: true, 1.601 + util: util, 1.602 + features: {}, 1.603 + modules: modules, 1.604 + config: { 1.605 + alertOnFail: false, 1.606 + alertOnWarn: false, 1.607 + preferTextRange: false, 1.608 + autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize 1.609 + } 1.610 + }; 1.611 + 1.612 + function consoleLog(msg) { 1.613 + if (typeof console != UNDEFINED && isHostMethod(console, "log")) { 1.614 + console.log(msg); 1.615 + } 1.616 + } 1.617 + 1.618 + function alertOrLog(msg, shouldAlert) { 1.619 + if (isBrowser && shouldAlert) { 1.620 + alert(msg); 1.621 + } else { 1.622 + consoleLog(msg); 1.623 + } 1.624 + } 1.625 + 1.626 + function fail(reason) { 1.627 + api.initialized = true; 1.628 + api.supported = false; 1.629 + alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); 1.630 + } 1.631 + 1.632 + api.fail = fail; 1.633 + 1.634 + function warn(msg) { 1.635 + alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); 1.636 + } 1.637 + 1.638 + api.warn = warn; 1.639 + 1.640 + // Add utility extend() method 1.641 + var extend; 1.642 + if ({}.hasOwnProperty) { 1.643 + util.extend = extend = function(obj, props, deep) { 1.644 + var o, p; 1.645 + for (var i in props) { 1.646 + if (props.hasOwnProperty(i)) { 1.647 + o = obj[i]; 1.648 + p = props[i]; 1.649 + if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { 1.650 + extend(o, p, true); 1.651 + } 1.652 + obj[i] = p; 1.653 + } 1.654 + } 1.655 + // Special case for toString, which does not show up in for...in loops in IE <= 8 1.656 + if (props.hasOwnProperty("toString")) { 1.657 + obj.toString = props.toString; 1.658 + } 1.659 + return obj; 1.660 + }; 1.661 + 1.662 + util.createOptions = function(optionsParam, defaults) { 1.663 + var options = {}; 1.664 + extend(options, defaults); 1.665 + if (optionsParam) { 1.666 + extend(options, optionsParam); 1.667 + } 1.668 + return options; 1.669 + }; 1.670 + } else { 1.671 + fail("hasOwnProperty not supported"); 1.672 + } 1.673 + 1.674 + // Test whether we're in a browser and bail out if not 1.675 + if (!isBrowser) { 1.676 + fail("Rangy can only run in a browser"); 1.677 + } 1.678 + 1.679 + // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not 1.680 + (function() { 1.681 + var toArray; 1.682 + 1.683 + if (isBrowser) { 1.684 + var el = document.createElement("div"); 1.685 + el.appendChild(document.createElement("span")); 1.686 + var slice = [].slice; 1.687 + try { 1.688 + if (slice.call(el.childNodes, 0)[0].nodeType == 1) { 1.689 + toArray = function(arrayLike) { 1.690 + return slice.call(arrayLike, 0); 1.691 + }; 1.692 + } 1.693 + } catch (e) {} 1.694 + } 1.695 + 1.696 + if (!toArray) { 1.697 + toArray = function(arrayLike) { 1.698 + var arr = []; 1.699 + for (var i = 0, len = arrayLike.length; i < len; ++i) { 1.700 + arr[i] = arrayLike[i]; 1.701 + } 1.702 + return arr; 1.703 + }; 1.704 + } 1.705 + 1.706 + util.toArray = toArray; 1.707 + })(); 1.708 + 1.709 + // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or 1.710 + // normalization of event properties 1.711 + var addListener; 1.712 + if (isBrowser) { 1.713 + if (isHostMethod(document, "addEventListener")) { 1.714 + addListener = function(obj, eventType, listener) { 1.715 + obj.addEventListener(eventType, listener, false); 1.716 + }; 1.717 + } else if (isHostMethod(document, "attachEvent")) { 1.718 + addListener = function(obj, eventType, listener) { 1.719 + obj.attachEvent("on" + eventType, listener); 1.720 + }; 1.721 + } else { 1.722 + fail("Document does not have required addEventListener or attachEvent method"); 1.723 + } 1.724 + 1.725 + util.addListener = addListener; 1.726 + } 1.727 + 1.728 + var initListeners = []; 1.729 + 1.730 + function getErrorDesc(ex) { 1.731 + return ex.message || ex.description || String(ex); 1.732 + } 1.733 + 1.734 + // Initialization 1.735 + function init() { 1.736 + if (!isBrowser || api.initialized) { 1.737 + return; 1.738 + } 1.739 + var testRange; 1.740 + var implementsDomRange = false, implementsTextRange = false; 1.741 + 1.742 + // First, perform basic feature tests 1.743 + 1.744 + if (isHostMethod(document, "createRange")) { 1.745 + testRange = document.createRange(); 1.746 + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { 1.747 + implementsDomRange = true; 1.748 + } 1.749 + } 1.750 + 1.751 + var body = getBody(document); 1.752 + if (!body || body.nodeName.toLowerCase() != "body") { 1.753 + fail("No body element found"); 1.754 + return; 1.755 + } 1.756 + 1.757 + if (body && isHostMethod(body, "createTextRange")) { 1.758 + testRange = body.createTextRange(); 1.759 + if (isTextRange(testRange)) { 1.760 + implementsTextRange = true; 1.761 + } 1.762 + } 1.763 + 1.764 + if (!implementsDomRange && !implementsTextRange) { 1.765 + fail("Neither Range nor TextRange are available"); 1.766 + return; 1.767 + } 1.768 + 1.769 + api.initialized = true; 1.770 + api.features = { 1.771 + implementsDomRange: implementsDomRange, 1.772 + implementsTextRange: implementsTextRange 1.773 + }; 1.774 + 1.775 + // Initialize modules 1.776 + var module, errorMessage; 1.777 + for (var moduleName in modules) { 1.778 + if ( (module = modules[moduleName]) instanceof Module ) { 1.779 + module.init(module, api); 1.780 + } 1.781 + } 1.782 + 1.783 + // Call init listeners 1.784 + for (var i = 0, len = initListeners.length; i < len; ++i) { 1.785 + try { 1.786 + initListeners[i](api); 1.787 + } catch (ex) { 1.788 + errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); 1.789 + consoleLog(errorMessage); 1.790 + } 1.791 + } 1.792 + } 1.793 + 1.794 + function deprecationNotice(deprecated, replacement, module) { 1.795 + if (module) { 1.796 + deprecated += " in module " + module.name; 1.797 + } 1.798 + api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " + 1.799 + replacement + " instead."); 1.800 + } 1.801 + 1.802 + function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) { 1.803 + owner[deprecated] = function() { 1.804 + deprecationNotice(deprecated, replacement, module); 1.805 + return owner[replacement].apply(owner, util.toArray(arguments)); 1.806 + }; 1.807 + } 1.808 + 1.809 + util.deprecationNotice = deprecationNotice; 1.810 + util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod; 1.811 + 1.812 + // Allow external scripts to initialize this library in case it's loaded after the document has loaded 1.813 + api.init = init; 1.814 + 1.815 + // Execute listener immediately if already initialized 1.816 + api.addInitListener = function(listener) { 1.817 + if (api.initialized) { 1.818 + listener(api); 1.819 + } else { 1.820 + initListeners.push(listener); 1.821 + } 1.822 + }; 1.823 + 1.824 + var shimListeners = []; 1.825 + 1.826 + api.addShimListener = function(listener) { 1.827 + shimListeners.push(listener); 1.828 + }; 1.829 + 1.830 + function shim(win) { 1.831 + win = win || window; 1.832 + init(); 1.833 + 1.834 + // Notify listeners 1.835 + for (var i = 0, len = shimListeners.length; i < len; ++i) { 1.836 + shimListeners[i](win); 1.837 + } 1.838 + } 1.839 + 1.840 + if (isBrowser) { 1.841 + api.shim = api.createMissingNativeApi = shim; 1.842 + createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); 1.843 + } 1.844 + 1.845 + function Module(name, dependencies, initializer) { 1.846 + this.name = name; 1.847 + this.dependencies = dependencies; 1.848 + this.initialized = false; 1.849 + this.supported = false; 1.850 + this.initializer = initializer; 1.851 + } 1.852 + 1.853 + Module.prototype = { 1.854 + init: function() { 1.855 + var requiredModuleNames = this.dependencies || []; 1.856 + for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) { 1.857 + moduleName = requiredModuleNames[i]; 1.858 + 1.859 + requiredModule = modules[moduleName]; 1.860 + if (!requiredModule || !(requiredModule instanceof Module)) { 1.861 + throw new Error("required module '" + moduleName + "' not found"); 1.862 + } 1.863 + 1.864 + requiredModule.init(); 1.865 + 1.866 + if (!requiredModule.supported) { 1.867 + throw new Error("required module '" + moduleName + "' not supported"); 1.868 + } 1.869 + } 1.870 + 1.871 + // Now run initializer 1.872 + this.initializer(this); 1.873 + }, 1.874 + 1.875 + fail: function(reason) { 1.876 + this.initialized = true; 1.877 + this.supported = false; 1.878 + throw new Error(reason); 1.879 + }, 1.880 + 1.881 + warn: function(msg) { 1.882 + api.warn("Module " + this.name + ": " + msg); 1.883 + }, 1.884 + 1.885 + deprecationNotice: function(deprecated, replacement) { 1.886 + api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " + 1.887 + replacement + " instead"); 1.888 + }, 1.889 + 1.890 + createError: function(msg) { 1.891 + return new Error("Error in Rangy " + this.name + " module: " + msg); 1.892 + } 1.893 + }; 1.894 + 1.895 + function createModule(name, dependencies, initFunc) { 1.896 + var newModule = new Module(name, dependencies, function(module) { 1.897 + if (!module.initialized) { 1.898 + module.initialized = true; 1.899 + try { 1.900 + initFunc(api, module); 1.901 + module.supported = true; 1.902 + } catch (ex) { 1.903 + var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); 1.904 + consoleLog(errorMessage); 1.905 + if (ex.stack) { 1.906 + consoleLog(ex.stack); 1.907 + } 1.908 + } 1.909 + } 1.910 + }); 1.911 + modules[name] = newModule; 1.912 + return newModule; 1.913 + } 1.914 + 1.915 + api.createModule = function(name) { 1.916 + // Allow 2 or 3 arguments (second argument is an optional array of dependencies) 1.917 + var initFunc, dependencies; 1.918 + if (arguments.length == 2) { 1.919 + initFunc = arguments[1]; 1.920 + dependencies = []; 1.921 + } else { 1.922 + initFunc = arguments[2]; 1.923 + dependencies = arguments[1]; 1.924 + } 1.925 + 1.926 + var module = createModule(name, dependencies, initFunc); 1.927 + 1.928 + // Initialize the module immediately if the core is already initialized 1.929 + if (api.initialized && api.supported) { 1.930 + module.init(); 1.931 + } 1.932 + }; 1.933 + 1.934 + api.createCoreModule = function(name, dependencies, initFunc) { 1.935 + createModule(name, dependencies, initFunc); 1.936 + }; 1.937 + 1.938 + /*----------------------------------------------------------------------------------------------------------------*/ 1.939 + 1.940 + // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately 1.941 + 1.942 + function RangePrototype() {} 1.943 + api.RangePrototype = RangePrototype; 1.944 + api.rangePrototype = new RangePrototype(); 1.945 + 1.946 + function SelectionPrototype() {} 1.947 + api.selectionPrototype = new SelectionPrototype(); 1.948 + 1.949 + /*----------------------------------------------------------------------------------------------------------------*/ 1.950 + 1.951 + // DOM utility methods used by Rangy 1.952 + api.createCoreModule("DomUtil", [], function(api, module) { 1.953 + var UNDEF = "undefined"; 1.954 + var util = api.util; 1.955 + var getBody = util.getBody; 1.956 + 1.957 + // Perform feature tests 1.958 + if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { 1.959 + module.fail("document missing a Node creation method"); 1.960 + } 1.961 + 1.962 + if (!util.isHostMethod(document, "getElementsByTagName")) { 1.963 + module.fail("document missing getElementsByTagName method"); 1.964 + } 1.965 + 1.966 + var el = document.createElement("div"); 1.967 + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || 1.968 + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { 1.969 + module.fail("Incomplete Element implementation"); 1.970 + } 1.971 + 1.972 + // innerHTML is required for Range's createContextualFragment method 1.973 + if (!util.isHostProperty(el, "innerHTML")) { 1.974 + module.fail("Element is missing innerHTML property"); 1.975 + } 1.976 + 1.977 + var textNode = document.createTextNode("test"); 1.978 + if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || 1.979 + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || 1.980 + !util.areHostProperties(textNode, ["data"]))) { 1.981 + module.fail("Incomplete Text Node implementation"); 1.982 + } 1.983 + 1.984 + /*----------------------------------------------------------------------------------------------------------------*/ 1.985 + 1.986 + // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been 1.987 + // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that 1.988 + // contains just the document as a single element and the value searched for is the document. 1.989 + var arrayContains = /*Array.prototype.indexOf ? 1.990 + function(arr, val) { 1.991 + return arr.indexOf(val) > -1; 1.992 + }:*/ 1.993 + 1.994 + function(arr, val) { 1.995 + var i = arr.length; 1.996 + while (i--) { 1.997 + if (arr[i] === val) { 1.998 + return true; 1.999 + } 1.1000 + } 1.1001 + return false; 1.1002 + }; 1.1003 + 1.1004 + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI 1.1005 + function isHtmlNamespace(node) { 1.1006 + var ns; 1.1007 + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); 1.1008 + } 1.1009 + 1.1010 + function parentElement(node) { 1.1011 + var parent = node.parentNode; 1.1012 + return (parent.nodeType == 1) ? parent : null; 1.1013 + } 1.1014 + 1.1015 + function getNodeIndex(node) { 1.1016 + var i = 0; 1.1017 + while( (node = node.previousSibling) ) { 1.1018 + ++i; 1.1019 + } 1.1020 + return i; 1.1021 + } 1.1022 + 1.1023 + function getNodeLength(node) { 1.1024 + switch (node.nodeType) { 1.1025 + case 7: 1.1026 + case 10: 1.1027 + return 0; 1.1028 + case 3: 1.1029 + case 8: 1.1030 + return node.length; 1.1031 + default: 1.1032 + return node.childNodes.length; 1.1033 + } 1.1034 + } 1.1035 + 1.1036 + function getCommonAncestor(node1, node2) { 1.1037 + var ancestors = [], n; 1.1038 + for (n = node1; n; n = n.parentNode) { 1.1039 + ancestors.push(n); 1.1040 + } 1.1041 + 1.1042 + for (n = node2; n; n = n.parentNode) { 1.1043 + if (arrayContains(ancestors, n)) { 1.1044 + return n; 1.1045 + } 1.1046 + } 1.1047 + 1.1048 + return null; 1.1049 + } 1.1050 + 1.1051 + function isAncestorOf(ancestor, descendant, selfIsAncestor) { 1.1052 + var n = selfIsAncestor ? descendant : descendant.parentNode; 1.1053 + while (n) { 1.1054 + if (n === ancestor) { 1.1055 + return true; 1.1056 + } else { 1.1057 + n = n.parentNode; 1.1058 + } 1.1059 + } 1.1060 + return false; 1.1061 + } 1.1062 + 1.1063 + function isOrIsAncestorOf(ancestor, descendant) { 1.1064 + return isAncestorOf(ancestor, descendant, true); 1.1065 + } 1.1066 + 1.1067 + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { 1.1068 + var p, n = selfIsAncestor ? node : node.parentNode; 1.1069 + while (n) { 1.1070 + p = n.parentNode; 1.1071 + if (p === ancestor) { 1.1072 + return n; 1.1073 + } 1.1074 + n = p; 1.1075 + } 1.1076 + return null; 1.1077 + } 1.1078 + 1.1079 + function isCharacterDataNode(node) { 1.1080 + var t = node.nodeType; 1.1081 + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment 1.1082 + } 1.1083 + 1.1084 + function isTextOrCommentNode(node) { 1.1085 + if (!node) { 1.1086 + return false; 1.1087 + } 1.1088 + var t = node.nodeType; 1.1089 + return t == 3 || t == 8 ; // Text or Comment 1.1090 + } 1.1091 + 1.1092 + function insertAfter(node, precedingNode) { 1.1093 + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; 1.1094 + if (nextNode) { 1.1095 + parent.insertBefore(node, nextNode); 1.1096 + } else { 1.1097 + parent.appendChild(node); 1.1098 + } 1.1099 + return node; 1.1100 + } 1.1101 + 1.1102 + // Note that we cannot use splitText() because it is bugridden in IE 9. 1.1103 + function splitDataNode(node, index, positionsToPreserve) { 1.1104 + var newNode = node.cloneNode(false); 1.1105 + newNode.deleteData(0, index); 1.1106 + node.deleteData(index, node.length - index); 1.1107 + insertAfter(newNode, node); 1.1108 + 1.1109 + // Preserve positions 1.1110 + if (positionsToPreserve) { 1.1111 + for (var i = 0, position; position = positionsToPreserve[i++]; ) { 1.1112 + // Handle case where position was inside the portion of node after the split point 1.1113 + if (position.node == node && position.offset > index) { 1.1114 + position.node = newNode; 1.1115 + position.offset -= index; 1.1116 + } 1.1117 + // Handle the case where the position is a node offset within node's parent 1.1118 + else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { 1.1119 + ++position.offset; 1.1120 + } 1.1121 + } 1.1122 + } 1.1123 + return newNode; 1.1124 + } 1.1125 + 1.1126 + function getDocument(node) { 1.1127 + if (node.nodeType == 9) { 1.1128 + return node; 1.1129 + } else if (typeof node.ownerDocument != UNDEF) { 1.1130 + return node.ownerDocument; 1.1131 + } else if (typeof node.document != UNDEF) { 1.1132 + return node.document; 1.1133 + } else if (node.parentNode) { 1.1134 + return getDocument(node.parentNode); 1.1135 + } else { 1.1136 + throw module.createError("getDocument: no document found for node"); 1.1137 + } 1.1138 + } 1.1139 + 1.1140 + function getWindow(node) { 1.1141 + var doc = getDocument(node); 1.1142 + if (typeof doc.defaultView != UNDEF) { 1.1143 + return doc.defaultView; 1.1144 + } else if (typeof doc.parentWindow != UNDEF) { 1.1145 + return doc.parentWindow; 1.1146 + } else { 1.1147 + throw module.createError("Cannot get a window object for node"); 1.1148 + } 1.1149 + } 1.1150 + 1.1151 + function getIframeDocument(iframeEl) { 1.1152 + if (typeof iframeEl.contentDocument != UNDEF) { 1.1153 + return iframeEl.contentDocument; 1.1154 + } else if (typeof iframeEl.contentWindow != UNDEF) { 1.1155 + return iframeEl.contentWindow.document; 1.1156 + } else { 1.1157 + throw module.createError("getIframeDocument: No Document object found for iframe element"); 1.1158 + } 1.1159 + } 1.1160 + 1.1161 + function getIframeWindow(iframeEl) { 1.1162 + if (typeof iframeEl.contentWindow != UNDEF) { 1.1163 + return iframeEl.contentWindow; 1.1164 + } else if (typeof iframeEl.contentDocument != UNDEF) { 1.1165 + return iframeEl.contentDocument.defaultView; 1.1166 + } else { 1.1167 + throw module.createError("getIframeWindow: No Window object found for iframe element"); 1.1168 + } 1.1169 + } 1.1170 + 1.1171 + // This looks bad. Is it worth it? 1.1172 + function isWindow(obj) { 1.1173 + return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); 1.1174 + } 1.1175 + 1.1176 + function getContentDocument(obj, module, methodName) { 1.1177 + var doc; 1.1178 + 1.1179 + if (!obj) { 1.1180 + doc = document; 1.1181 + } 1.1182 + 1.1183 + // Test if a DOM node has been passed and obtain a document object for it if so 1.1184 + else if (util.isHostProperty(obj, "nodeType")) { 1.1185 + doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ? 1.1186 + getIframeDocument(obj) : getDocument(obj); 1.1187 + } 1.1188 + 1.1189 + // Test if the doc parameter appears to be a Window object 1.1190 + else if (isWindow(obj)) { 1.1191 + doc = obj.document; 1.1192 + } 1.1193 + 1.1194 + if (!doc) { 1.1195 + throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); 1.1196 + } 1.1197 + 1.1198 + return doc; 1.1199 + } 1.1200 + 1.1201 + function getRootContainer(node) { 1.1202 + var parent; 1.1203 + while ( (parent = node.parentNode) ) { 1.1204 + node = parent; 1.1205 + } 1.1206 + return node; 1.1207 + } 1.1208 + 1.1209 + function comparePoints(nodeA, offsetA, nodeB, offsetB) { 1.1210 + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing 1.1211 + var nodeC, root, childA, childB, n; 1.1212 + if (nodeA == nodeB) { 1.1213 + // Case 1: nodes are the same 1.1214 + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; 1.1215 + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { 1.1216 + // Case 2: node C (container B or an ancestor) is a child node of A 1.1217 + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; 1.1218 + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { 1.1219 + // Case 3: node C (container A or an ancestor) is a child node of B 1.1220 + return getNodeIndex(nodeC) < offsetB ? -1 : 1; 1.1221 + } else { 1.1222 + root = getCommonAncestor(nodeA, nodeB); 1.1223 + if (!root) { 1.1224 + throw new Error("comparePoints error: nodes have no common ancestor"); 1.1225 + } 1.1226 + 1.1227 + // Case 4: containers are siblings or descendants of siblings 1.1228 + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); 1.1229 + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); 1.1230 + 1.1231 + if (childA === childB) { 1.1232 + // This shouldn't be possible 1.1233 + throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); 1.1234 + } else { 1.1235 + n = root.firstChild; 1.1236 + while (n) { 1.1237 + if (n === childA) { 1.1238 + return -1; 1.1239 + } else if (n === childB) { 1.1240 + return 1; 1.1241 + } 1.1242 + n = n.nextSibling; 1.1243 + } 1.1244 + } 1.1245 + } 1.1246 + } 1.1247 + 1.1248 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1249 + 1.1250 + // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried 1.1251 + var crashyTextNodes = false; 1.1252 + 1.1253 + function isBrokenNode(node) { 1.1254 + var n; 1.1255 + try { 1.1256 + n = node.parentNode; 1.1257 + return false; 1.1258 + } catch (e) { 1.1259 + return true; 1.1260 + } 1.1261 + } 1.1262 + 1.1263 + (function() { 1.1264 + var el = document.createElement("b"); 1.1265 + el.innerHTML = "1"; 1.1266 + var textNode = el.firstChild; 1.1267 + el.innerHTML = "<br />"; 1.1268 + crashyTextNodes = isBrokenNode(textNode); 1.1269 + 1.1270 + api.features.crashyTextNodes = crashyTextNodes; 1.1271 + })(); 1.1272 + 1.1273 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1274 + 1.1275 + function inspectNode(node) { 1.1276 + if (!node) { 1.1277 + return "[No node]"; 1.1278 + } 1.1279 + if (crashyTextNodes && isBrokenNode(node)) { 1.1280 + return "[Broken node]"; 1.1281 + } 1.1282 + if (isCharacterDataNode(node)) { 1.1283 + return '"' + node.data + '"'; 1.1284 + } 1.1285 + if (node.nodeType == 1) { 1.1286 + var idAttr = node.id ? ' id="' + node.id + '"' : ""; 1.1287 + return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; 1.1288 + } 1.1289 + return node.nodeName; 1.1290 + } 1.1291 + 1.1292 + function fragmentFromNodeChildren(node) { 1.1293 + var fragment = getDocument(node).createDocumentFragment(), child; 1.1294 + while ( (child = node.firstChild) ) { 1.1295 + fragment.appendChild(child); 1.1296 + } 1.1297 + return fragment; 1.1298 + } 1.1299 + 1.1300 + var getComputedStyleProperty; 1.1301 + if (typeof window.getComputedStyle != UNDEF) { 1.1302 + getComputedStyleProperty = function(el, propName) { 1.1303 + return getWindow(el).getComputedStyle(el, null)[propName]; 1.1304 + }; 1.1305 + } else if (typeof document.documentElement.currentStyle != UNDEF) { 1.1306 + getComputedStyleProperty = function(el, propName) { 1.1307 + return el.currentStyle ? el.currentStyle[propName] : ""; 1.1308 + }; 1.1309 + } else { 1.1310 + module.fail("No means of obtaining computed style properties found"); 1.1311 + } 1.1312 + 1.1313 + function createTestElement(doc, html, contentEditable) { 1.1314 + var body = getBody(doc); 1.1315 + var el = doc.createElement("div"); 1.1316 + el.contentEditable = "" + !!contentEditable; 1.1317 + if (html) { 1.1318 + el.innerHTML = html; 1.1319 + } 1.1320 + 1.1321 + // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292) 1.1322 + var bodyFirstChild = body.firstChild; 1.1323 + if (bodyFirstChild) { 1.1324 + body.insertBefore(el, bodyFirstChild); 1.1325 + } else { 1.1326 + body.appendChild(el); 1.1327 + } 1.1328 + 1.1329 + return el; 1.1330 + } 1.1331 + 1.1332 + function removeNode(node) { 1.1333 + return node.parentNode.removeChild(node); 1.1334 + } 1.1335 + 1.1336 + function NodeIterator(root) { 1.1337 + this.root = root; 1.1338 + this._next = root; 1.1339 + } 1.1340 + 1.1341 + NodeIterator.prototype = { 1.1342 + _current: null, 1.1343 + 1.1344 + hasNext: function() { 1.1345 + return !!this._next; 1.1346 + }, 1.1347 + 1.1348 + next: function() { 1.1349 + var n = this._current = this._next; 1.1350 + var child, next; 1.1351 + if (this._current) { 1.1352 + child = n.firstChild; 1.1353 + if (child) { 1.1354 + this._next = child; 1.1355 + } else { 1.1356 + next = null; 1.1357 + while ((n !== this.root) && !(next = n.nextSibling)) { 1.1358 + n = n.parentNode; 1.1359 + } 1.1360 + this._next = next; 1.1361 + } 1.1362 + } 1.1363 + return this._current; 1.1364 + }, 1.1365 + 1.1366 + detach: function() { 1.1367 + this._current = this._next = this.root = null; 1.1368 + } 1.1369 + }; 1.1370 + 1.1371 + function createIterator(root) { 1.1372 + return new NodeIterator(root); 1.1373 + } 1.1374 + 1.1375 + function DomPosition(node, offset) { 1.1376 + this.node = node; 1.1377 + this.offset = offset; 1.1378 + } 1.1379 + 1.1380 + DomPosition.prototype = { 1.1381 + equals: function(pos) { 1.1382 + return !!pos && this.node === pos.node && this.offset == pos.offset; 1.1383 + }, 1.1384 + 1.1385 + inspect: function() { 1.1386 + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; 1.1387 + }, 1.1388 + 1.1389 + toString: function() { 1.1390 + return this.inspect(); 1.1391 + } 1.1392 + }; 1.1393 + 1.1394 + function DOMException(codeName) { 1.1395 + this.code = this[codeName]; 1.1396 + this.codeName = codeName; 1.1397 + this.message = "DOMException: " + this.codeName; 1.1398 + } 1.1399 + 1.1400 + DOMException.prototype = { 1.1401 + INDEX_SIZE_ERR: 1, 1.1402 + HIERARCHY_REQUEST_ERR: 3, 1.1403 + WRONG_DOCUMENT_ERR: 4, 1.1404 + NO_MODIFICATION_ALLOWED_ERR: 7, 1.1405 + NOT_FOUND_ERR: 8, 1.1406 + NOT_SUPPORTED_ERR: 9, 1.1407 + INVALID_STATE_ERR: 11, 1.1408 + INVALID_NODE_TYPE_ERR: 24 1.1409 + }; 1.1410 + 1.1411 + DOMException.prototype.toString = function() { 1.1412 + return this.message; 1.1413 + }; 1.1414 + 1.1415 + api.dom = { 1.1416 + arrayContains: arrayContains, 1.1417 + isHtmlNamespace: isHtmlNamespace, 1.1418 + parentElement: parentElement, 1.1419 + getNodeIndex: getNodeIndex, 1.1420 + getNodeLength: getNodeLength, 1.1421 + getCommonAncestor: getCommonAncestor, 1.1422 + isAncestorOf: isAncestorOf, 1.1423 + isOrIsAncestorOf: isOrIsAncestorOf, 1.1424 + getClosestAncestorIn: getClosestAncestorIn, 1.1425 + isCharacterDataNode: isCharacterDataNode, 1.1426 + isTextOrCommentNode: isTextOrCommentNode, 1.1427 + insertAfter: insertAfter, 1.1428 + splitDataNode: splitDataNode, 1.1429 + getDocument: getDocument, 1.1430 + getWindow: getWindow, 1.1431 + getIframeWindow: getIframeWindow, 1.1432 + getIframeDocument: getIframeDocument, 1.1433 + getBody: getBody, 1.1434 + isWindow: isWindow, 1.1435 + getContentDocument: getContentDocument, 1.1436 + getRootContainer: getRootContainer, 1.1437 + comparePoints: comparePoints, 1.1438 + isBrokenNode: isBrokenNode, 1.1439 + inspectNode: inspectNode, 1.1440 + getComputedStyleProperty: getComputedStyleProperty, 1.1441 + createTestElement: createTestElement, 1.1442 + removeNode: removeNode, 1.1443 + fragmentFromNodeChildren: fragmentFromNodeChildren, 1.1444 + createIterator: createIterator, 1.1445 + DomPosition: DomPosition 1.1446 + }; 1.1447 + 1.1448 + api.DOMException = DOMException; 1.1449 + }); 1.1450 + 1.1451 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1452 + 1.1453 + // Pure JavaScript implementation of DOM Range 1.1454 + api.createCoreModule("DomRange", ["DomUtil"], function(api, module) { 1.1455 + var dom = api.dom; 1.1456 + var util = api.util; 1.1457 + var DomPosition = dom.DomPosition; 1.1458 + var DOMException = api.DOMException; 1.1459 + 1.1460 + var isCharacterDataNode = dom.isCharacterDataNode; 1.1461 + var getNodeIndex = dom.getNodeIndex; 1.1462 + var isOrIsAncestorOf = dom.isOrIsAncestorOf; 1.1463 + var getDocument = dom.getDocument; 1.1464 + var comparePoints = dom.comparePoints; 1.1465 + var splitDataNode = dom.splitDataNode; 1.1466 + var getClosestAncestorIn = dom.getClosestAncestorIn; 1.1467 + var getNodeLength = dom.getNodeLength; 1.1468 + var arrayContains = dom.arrayContains; 1.1469 + var getRootContainer = dom.getRootContainer; 1.1470 + var crashyTextNodes = api.features.crashyTextNodes; 1.1471 + 1.1472 + var removeNode = dom.removeNode; 1.1473 + 1.1474 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1475 + 1.1476 + // Utility functions 1.1477 + 1.1478 + function isNonTextPartiallySelected(node, range) { 1.1479 + return (node.nodeType != 3) && 1.1480 + (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); 1.1481 + } 1.1482 + 1.1483 + function getRangeDocument(range) { 1.1484 + return range.document || getDocument(range.startContainer); 1.1485 + } 1.1486 + 1.1487 + function getRangeRoot(range) { 1.1488 + return getRootContainer(range.startContainer); 1.1489 + } 1.1490 + 1.1491 + function getBoundaryBeforeNode(node) { 1.1492 + return new DomPosition(node.parentNode, getNodeIndex(node)); 1.1493 + } 1.1494 + 1.1495 + function getBoundaryAfterNode(node) { 1.1496 + return new DomPosition(node.parentNode, getNodeIndex(node) + 1); 1.1497 + } 1.1498 + 1.1499 + function insertNodeAtPosition(node, n, o) { 1.1500 + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; 1.1501 + if (isCharacterDataNode(n)) { 1.1502 + if (o == n.length) { 1.1503 + dom.insertAfter(node, n); 1.1504 + } else { 1.1505 + n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); 1.1506 + } 1.1507 + } else if (o >= n.childNodes.length) { 1.1508 + n.appendChild(node); 1.1509 + } else { 1.1510 + n.insertBefore(node, n.childNodes[o]); 1.1511 + } 1.1512 + return firstNodeInserted; 1.1513 + } 1.1514 + 1.1515 + function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { 1.1516 + assertRangeValid(rangeA); 1.1517 + assertRangeValid(rangeB); 1.1518 + 1.1519 + if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { 1.1520 + throw new DOMException("WRONG_DOCUMENT_ERR"); 1.1521 + } 1.1522 + 1.1523 + var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), 1.1524 + endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); 1.1525 + 1.1526 + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1.1527 + } 1.1528 + 1.1529 + function cloneSubtree(iterator) { 1.1530 + var partiallySelected; 1.1531 + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 1.1532 + partiallySelected = iterator.isPartiallySelectedSubtree(); 1.1533 + node = node.cloneNode(!partiallySelected); 1.1534 + if (partiallySelected) { 1.1535 + subIterator = iterator.getSubtreeIterator(); 1.1536 + node.appendChild(cloneSubtree(subIterator)); 1.1537 + subIterator.detach(); 1.1538 + } 1.1539 + 1.1540 + if (node.nodeType == 10) { // DocumentType 1.1541 + throw new DOMException("HIERARCHY_REQUEST_ERR"); 1.1542 + } 1.1543 + frag.appendChild(node); 1.1544 + } 1.1545 + return frag; 1.1546 + } 1.1547 + 1.1548 + function iterateSubtree(rangeIterator, func, iteratorState) { 1.1549 + var it, n; 1.1550 + iteratorState = iteratorState || { stop: false }; 1.1551 + for (var node, subRangeIterator; node = rangeIterator.next(); ) { 1.1552 + if (rangeIterator.isPartiallySelectedSubtree()) { 1.1553 + if (func(node) === false) { 1.1554 + iteratorState.stop = true; 1.1555 + return; 1.1556 + } else { 1.1557 + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of 1.1558 + // the node selected by the Range. 1.1559 + subRangeIterator = rangeIterator.getSubtreeIterator(); 1.1560 + iterateSubtree(subRangeIterator, func, iteratorState); 1.1561 + subRangeIterator.detach(); 1.1562 + if (iteratorState.stop) { 1.1563 + return; 1.1564 + } 1.1565 + } 1.1566 + } else { 1.1567 + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its 1.1568 + // descendants 1.1569 + it = dom.createIterator(node); 1.1570 + while ( (n = it.next()) ) { 1.1571 + if (func(n) === false) { 1.1572 + iteratorState.stop = true; 1.1573 + return; 1.1574 + } 1.1575 + } 1.1576 + } 1.1577 + } 1.1578 + } 1.1579 + 1.1580 + function deleteSubtree(iterator) { 1.1581 + var subIterator; 1.1582 + while (iterator.next()) { 1.1583 + if (iterator.isPartiallySelectedSubtree()) { 1.1584 + subIterator = iterator.getSubtreeIterator(); 1.1585 + deleteSubtree(subIterator); 1.1586 + subIterator.detach(); 1.1587 + } else { 1.1588 + iterator.remove(); 1.1589 + } 1.1590 + } 1.1591 + } 1.1592 + 1.1593 + function extractSubtree(iterator) { 1.1594 + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { 1.1595 + 1.1596 + if (iterator.isPartiallySelectedSubtree()) { 1.1597 + node = node.cloneNode(false); 1.1598 + subIterator = iterator.getSubtreeIterator(); 1.1599 + node.appendChild(extractSubtree(subIterator)); 1.1600 + subIterator.detach(); 1.1601 + } else { 1.1602 + iterator.remove(); 1.1603 + } 1.1604 + if (node.nodeType == 10) { // DocumentType 1.1605 + throw new DOMException("HIERARCHY_REQUEST_ERR"); 1.1606 + } 1.1607 + frag.appendChild(node); 1.1608 + } 1.1609 + return frag; 1.1610 + } 1.1611 + 1.1612 + function getNodesInRange(range, nodeTypes, filter) { 1.1613 + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; 1.1614 + var filterExists = !!filter; 1.1615 + if (filterNodeTypes) { 1.1616 + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); 1.1617 + } 1.1618 + 1.1619 + var nodes = []; 1.1620 + iterateSubtree(new RangeIterator(range, false), function(node) { 1.1621 + if (filterNodeTypes && !regex.test(node.nodeType)) { 1.1622 + return; 1.1623 + } 1.1624 + if (filterExists && !filter(node)) { 1.1625 + return; 1.1626 + } 1.1627 + // Don't include a boundary container if it is a character data node and the range does not contain any 1.1628 + // of its character data. See issue 190. 1.1629 + var sc = range.startContainer; 1.1630 + if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { 1.1631 + return; 1.1632 + } 1.1633 + 1.1634 + var ec = range.endContainer; 1.1635 + if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { 1.1636 + return; 1.1637 + } 1.1638 + 1.1639 + nodes.push(node); 1.1640 + }); 1.1641 + return nodes; 1.1642 + } 1.1643 + 1.1644 + function inspect(range) { 1.1645 + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); 1.1646 + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + 1.1647 + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; 1.1648 + } 1.1649 + 1.1650 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1651 + 1.1652 + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) 1.1653 + 1.1654 + function RangeIterator(range, clonePartiallySelectedTextNodes) { 1.1655 + this.range = range; 1.1656 + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; 1.1657 + 1.1658 + 1.1659 + if (!range.collapsed) { 1.1660 + this.sc = range.startContainer; 1.1661 + this.so = range.startOffset; 1.1662 + this.ec = range.endContainer; 1.1663 + this.eo = range.endOffset; 1.1664 + var root = range.commonAncestorContainer; 1.1665 + 1.1666 + if (this.sc === this.ec && isCharacterDataNode(this.sc)) { 1.1667 + this.isSingleCharacterDataNode = true; 1.1668 + this._first = this._last = this._next = this.sc; 1.1669 + } else { 1.1670 + this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ? 1.1671 + this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); 1.1672 + this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ? 1.1673 + this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); 1.1674 + } 1.1675 + } 1.1676 + } 1.1677 + 1.1678 + RangeIterator.prototype = { 1.1679 + _current: null, 1.1680 + _next: null, 1.1681 + _first: null, 1.1682 + _last: null, 1.1683 + isSingleCharacterDataNode: false, 1.1684 + 1.1685 + reset: function() { 1.1686 + this._current = null; 1.1687 + this._next = this._first; 1.1688 + }, 1.1689 + 1.1690 + hasNext: function() { 1.1691 + return !!this._next; 1.1692 + }, 1.1693 + 1.1694 + next: function() { 1.1695 + // Move to next node 1.1696 + var current = this._current = this._next; 1.1697 + if (current) { 1.1698 + this._next = (current !== this._last) ? current.nextSibling : null; 1.1699 + 1.1700 + // Check for partially selected text nodes 1.1701 + if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { 1.1702 + if (current === this.ec) { 1.1703 + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); 1.1704 + } 1.1705 + if (this._current === this.sc) { 1.1706 + (current = current.cloneNode(true)).deleteData(0, this.so); 1.1707 + } 1.1708 + } 1.1709 + } 1.1710 + 1.1711 + return current; 1.1712 + }, 1.1713 + 1.1714 + remove: function() { 1.1715 + var current = this._current, start, end; 1.1716 + 1.1717 + if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { 1.1718 + start = (current === this.sc) ? this.so : 0; 1.1719 + end = (current === this.ec) ? this.eo : current.length; 1.1720 + if (start != end) { 1.1721 + current.deleteData(start, end - start); 1.1722 + } 1.1723 + } else { 1.1724 + if (current.parentNode) { 1.1725 + removeNode(current); 1.1726 + } else { 1.1727 + } 1.1728 + } 1.1729 + }, 1.1730 + 1.1731 + // Checks if the current node is partially selected 1.1732 + isPartiallySelectedSubtree: function() { 1.1733 + var current = this._current; 1.1734 + return isNonTextPartiallySelected(current, this.range); 1.1735 + }, 1.1736 + 1.1737 + getSubtreeIterator: function() { 1.1738 + var subRange; 1.1739 + if (this.isSingleCharacterDataNode) { 1.1740 + subRange = this.range.cloneRange(); 1.1741 + subRange.collapse(false); 1.1742 + } else { 1.1743 + subRange = new Range(getRangeDocument(this.range)); 1.1744 + var current = this._current; 1.1745 + var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current); 1.1746 + 1.1747 + if (isOrIsAncestorOf(current, this.sc)) { 1.1748 + startContainer = this.sc; 1.1749 + startOffset = this.so; 1.1750 + } 1.1751 + if (isOrIsAncestorOf(current, this.ec)) { 1.1752 + endContainer = this.ec; 1.1753 + endOffset = this.eo; 1.1754 + } 1.1755 + 1.1756 + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); 1.1757 + } 1.1758 + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); 1.1759 + }, 1.1760 + 1.1761 + detach: function() { 1.1762 + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; 1.1763 + } 1.1764 + }; 1.1765 + 1.1766 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1767 + 1.1768 + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; 1.1769 + var rootContainerNodeTypes = [2, 9, 11]; 1.1770 + var readonlyNodeTypes = [5, 6, 10, 12]; 1.1771 + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; 1.1772 + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; 1.1773 + 1.1774 + function createAncestorFinder(nodeTypes) { 1.1775 + return function(node, selfIsAncestor) { 1.1776 + var t, n = selfIsAncestor ? node : node.parentNode; 1.1777 + while (n) { 1.1778 + t = n.nodeType; 1.1779 + if (arrayContains(nodeTypes, t)) { 1.1780 + return n; 1.1781 + } 1.1782 + n = n.parentNode; 1.1783 + } 1.1784 + return null; 1.1785 + }; 1.1786 + } 1.1787 + 1.1788 + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); 1.1789 + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); 1.1790 + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); 1.1791 + 1.1792 + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { 1.1793 + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { 1.1794 + throw new DOMException("INVALID_NODE_TYPE_ERR"); 1.1795 + } 1.1796 + } 1.1797 + 1.1798 + function assertValidNodeType(node, invalidTypes) { 1.1799 + if (!arrayContains(invalidTypes, node.nodeType)) { 1.1800 + throw new DOMException("INVALID_NODE_TYPE_ERR"); 1.1801 + } 1.1802 + } 1.1803 + 1.1804 + function assertValidOffset(node, offset) { 1.1805 + if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { 1.1806 + throw new DOMException("INDEX_SIZE_ERR"); 1.1807 + } 1.1808 + } 1.1809 + 1.1810 + function assertSameDocumentOrFragment(node1, node2) { 1.1811 + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { 1.1812 + throw new DOMException("WRONG_DOCUMENT_ERR"); 1.1813 + } 1.1814 + } 1.1815 + 1.1816 + function assertNodeNotReadOnly(node) { 1.1817 + if (getReadonlyAncestor(node, true)) { 1.1818 + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); 1.1819 + } 1.1820 + } 1.1821 + 1.1822 + function assertNode(node, codeName) { 1.1823 + if (!node) { 1.1824 + throw new DOMException(codeName); 1.1825 + } 1.1826 + } 1.1827 + 1.1828 + function isValidOffset(node, offset) { 1.1829 + return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); 1.1830 + } 1.1831 + 1.1832 + function isRangeValid(range) { 1.1833 + return (!!range.startContainer && !!range.endContainer && 1.1834 + !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) && 1.1835 + getRootContainer(range.startContainer) == getRootContainer(range.endContainer) && 1.1836 + isValidOffset(range.startContainer, range.startOffset) && 1.1837 + isValidOffset(range.endContainer, range.endOffset)); 1.1838 + } 1.1839 + 1.1840 + function assertRangeValid(range) { 1.1841 + if (!isRangeValid(range)) { 1.1842 + throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); 1.1843 + } 1.1844 + } 1.1845 + 1.1846 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1847 + 1.1848 + // Test the browser's innerHTML support to decide how to implement createContextualFragment 1.1849 + var styleEl = document.createElement("style"); 1.1850 + var htmlParsingConforms = false; 1.1851 + try { 1.1852 + styleEl.innerHTML = "<b>x</b>"; 1.1853 + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node 1.1854 + } catch (e) { 1.1855 + // IE 6 and 7 throw 1.1856 + } 1.1857 + 1.1858 + api.features.htmlParsingConforms = htmlParsingConforms; 1.1859 + 1.1860 + var createContextualFragment = htmlParsingConforms ? 1.1861 + 1.1862 + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See 1.1863 + // discussion and base code for this implementation at issue 67. 1.1864 + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface 1.1865 + // Thanks to Aleks Williams. 1.1866 + function(fragmentStr) { 1.1867 + // "Let node the context object's start's node." 1.1868 + var node = this.startContainer; 1.1869 + var doc = getDocument(node); 1.1870 + 1.1871 + // "If the context object's start's node is null, raise an INVALID_STATE_ERR 1.1872 + // exception and abort these steps." 1.1873 + if (!node) { 1.1874 + throw new DOMException("INVALID_STATE_ERR"); 1.1875 + } 1.1876 + 1.1877 + // "Let element be as follows, depending on node's interface:" 1.1878 + // Document, Document Fragment: null 1.1879 + var el = null; 1.1880 + 1.1881 + // "Element: node" 1.1882 + if (node.nodeType == 1) { 1.1883 + el = node; 1.1884 + 1.1885 + // "Text, Comment: node's parentElement" 1.1886 + } else if (isCharacterDataNode(node)) { 1.1887 + el = dom.parentElement(node); 1.1888 + } 1.1889 + 1.1890 + // "If either element is null or element's ownerDocument is an HTML document 1.1891 + // and element's local name is "html" and element's namespace is the HTML 1.1892 + // namespace" 1.1893 + if (el === null || ( 1.1894 + el.nodeName == "HTML" && 1.1895 + dom.isHtmlNamespace(getDocument(el).documentElement) && 1.1896 + dom.isHtmlNamespace(el) 1.1897 + )) { 1.1898 + 1.1899 + // "let element be a new Element with "body" as its local name and the HTML 1.1900 + // namespace as its namespace."" 1.1901 + el = doc.createElement("body"); 1.1902 + } else { 1.1903 + el = el.cloneNode(false); 1.1904 + } 1.1905 + 1.1906 + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." 1.1907 + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." 1.1908 + // "In either case, the algorithm must be invoked with fragment as the input 1.1909 + // and element as the context element." 1.1910 + el.innerHTML = fragmentStr; 1.1911 + 1.1912 + // "If this raises an exception, then abort these steps. Otherwise, let new 1.1913 + // children be the nodes returned." 1.1914 + 1.1915 + // "Let fragment be a new DocumentFragment." 1.1916 + // "Append all new children to fragment." 1.1917 + // "Return fragment." 1.1918 + return dom.fragmentFromNodeChildren(el); 1.1919 + } : 1.1920 + 1.1921 + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that 1.1922 + // previous versions of Rangy used (with the exception of using a body element rather than a div) 1.1923 + function(fragmentStr) { 1.1924 + var doc = getRangeDocument(this); 1.1925 + var el = doc.createElement("body"); 1.1926 + el.innerHTML = fragmentStr; 1.1927 + 1.1928 + return dom.fragmentFromNodeChildren(el); 1.1929 + }; 1.1930 + 1.1931 + function splitRangeBoundaries(range, positionsToPreserve) { 1.1932 + assertRangeValid(range); 1.1933 + 1.1934 + var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset; 1.1935 + var startEndSame = (sc === ec); 1.1936 + 1.1937 + if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { 1.1938 + splitDataNode(ec, eo, positionsToPreserve); 1.1939 + } 1.1940 + 1.1941 + if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { 1.1942 + sc = splitDataNode(sc, so, positionsToPreserve); 1.1943 + if (startEndSame) { 1.1944 + eo -= so; 1.1945 + ec = sc; 1.1946 + } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { 1.1947 + eo++; 1.1948 + } 1.1949 + so = 0; 1.1950 + } 1.1951 + range.setStartAndEnd(sc, so, ec, eo); 1.1952 + } 1.1953 + 1.1954 + function rangeToHtml(range) { 1.1955 + assertRangeValid(range); 1.1956 + var container = range.commonAncestorContainer.parentNode.cloneNode(false); 1.1957 + container.appendChild( range.cloneContents() ); 1.1958 + return container.innerHTML; 1.1959 + } 1.1960 + 1.1961 + /*----------------------------------------------------------------------------------------------------------------*/ 1.1962 + 1.1963 + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", 1.1964 + "commonAncestorContainer"]; 1.1965 + 1.1966 + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; 1.1967 + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; 1.1968 + 1.1969 + util.extend(api.rangePrototype, { 1.1970 + compareBoundaryPoints: function(how, range) { 1.1971 + assertRangeValid(this); 1.1972 + assertSameDocumentOrFragment(this.startContainer, range.startContainer); 1.1973 + 1.1974 + var nodeA, offsetA, nodeB, offsetB; 1.1975 + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; 1.1976 + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; 1.1977 + nodeA = this[prefixA + "Container"]; 1.1978 + offsetA = this[prefixA + "Offset"]; 1.1979 + nodeB = range[prefixB + "Container"]; 1.1980 + offsetB = range[prefixB + "Offset"]; 1.1981 + return comparePoints(nodeA, offsetA, nodeB, offsetB); 1.1982 + }, 1.1983 + 1.1984 + insertNode: function(node) { 1.1985 + assertRangeValid(this); 1.1986 + assertValidNodeType(node, insertableNodeTypes); 1.1987 + assertNodeNotReadOnly(this.startContainer); 1.1988 + 1.1989 + if (isOrIsAncestorOf(node, this.startContainer)) { 1.1990 + throw new DOMException("HIERARCHY_REQUEST_ERR"); 1.1991 + } 1.1992 + 1.1993 + // No check for whether the container of the start of the Range is of a type that does not allow 1.1994 + // children of the type of node: the browser's DOM implementation should do this for us when we attempt 1.1995 + // to add the node 1.1996 + 1.1997 + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); 1.1998 + this.setStartBefore(firstNodeInserted); 1.1999 + }, 1.2000 + 1.2001 + cloneContents: function() { 1.2002 + assertRangeValid(this); 1.2003 + 1.2004 + var clone, frag; 1.2005 + if (this.collapsed) { 1.2006 + return getRangeDocument(this).createDocumentFragment(); 1.2007 + } else { 1.2008 + if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { 1.2009 + clone = this.startContainer.cloneNode(true); 1.2010 + clone.data = clone.data.slice(this.startOffset, this.endOffset); 1.2011 + frag = getRangeDocument(this).createDocumentFragment(); 1.2012 + frag.appendChild(clone); 1.2013 + return frag; 1.2014 + } else { 1.2015 + var iterator = new RangeIterator(this, true); 1.2016 + clone = cloneSubtree(iterator); 1.2017 + iterator.detach(); 1.2018 + } 1.2019 + return clone; 1.2020 + } 1.2021 + }, 1.2022 + 1.2023 + canSurroundContents: function() { 1.2024 + assertRangeValid(this); 1.2025 + assertNodeNotReadOnly(this.startContainer); 1.2026 + assertNodeNotReadOnly(this.endContainer); 1.2027 + 1.2028 + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1.2029 + // no non-text nodes. 1.2030 + var iterator = new RangeIterator(this, true); 1.2031 + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || 1.2032 + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1.2033 + iterator.detach(); 1.2034 + return !boundariesInvalid; 1.2035 + }, 1.2036 + 1.2037 + surroundContents: function(node) { 1.2038 + assertValidNodeType(node, surroundNodeTypes); 1.2039 + 1.2040 + if (!this.canSurroundContents()) { 1.2041 + throw new DOMException("INVALID_STATE_ERR"); 1.2042 + } 1.2043 + 1.2044 + // Extract the contents 1.2045 + var content = this.extractContents(); 1.2046 + 1.2047 + // Clear the children of the node 1.2048 + if (node.hasChildNodes()) { 1.2049 + while (node.lastChild) { 1.2050 + node.removeChild(node.lastChild); 1.2051 + } 1.2052 + } 1.2053 + 1.2054 + // Insert the new node and add the extracted contents 1.2055 + insertNodeAtPosition(node, this.startContainer, this.startOffset); 1.2056 + node.appendChild(content); 1.2057 + 1.2058 + this.selectNode(node); 1.2059 + }, 1.2060 + 1.2061 + cloneRange: function() { 1.2062 + assertRangeValid(this); 1.2063 + var range = new Range(getRangeDocument(this)); 1.2064 + var i = rangeProperties.length, prop; 1.2065 + while (i--) { 1.2066 + prop = rangeProperties[i]; 1.2067 + range[prop] = this[prop]; 1.2068 + } 1.2069 + return range; 1.2070 + }, 1.2071 + 1.2072 + toString: function() { 1.2073 + assertRangeValid(this); 1.2074 + var sc = this.startContainer; 1.2075 + if (sc === this.endContainer && isCharacterDataNode(sc)) { 1.2076 + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; 1.2077 + } else { 1.2078 + var textParts = [], iterator = new RangeIterator(this, true); 1.2079 + iterateSubtree(iterator, function(node) { 1.2080 + // Accept only text or CDATA nodes, not comments 1.2081 + if (node.nodeType == 3 || node.nodeType == 4) { 1.2082 + textParts.push(node.data); 1.2083 + } 1.2084 + }); 1.2085 + iterator.detach(); 1.2086 + return textParts.join(""); 1.2087 + } 1.2088 + }, 1.2089 + 1.2090 + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since 1.2091 + // been removed from Mozilla. 1.2092 + 1.2093 + compareNode: function(node) { 1.2094 + assertRangeValid(this); 1.2095 + 1.2096 + var parent = node.parentNode; 1.2097 + var nodeIndex = getNodeIndex(node); 1.2098 + 1.2099 + if (!parent) { 1.2100 + throw new DOMException("NOT_FOUND_ERR"); 1.2101 + } 1.2102 + 1.2103 + var startComparison = this.comparePoint(parent, nodeIndex), 1.2104 + endComparison = this.comparePoint(parent, nodeIndex + 1); 1.2105 + 1.2106 + if (startComparison < 0) { // Node starts before 1.2107 + return (endComparison > 0) ? n_b_a : n_b; 1.2108 + } else { 1.2109 + return (endComparison > 0) ? n_a : n_i; 1.2110 + } 1.2111 + }, 1.2112 + 1.2113 + comparePoint: function(node, offset) { 1.2114 + assertRangeValid(this); 1.2115 + assertNode(node, "HIERARCHY_REQUEST_ERR"); 1.2116 + assertSameDocumentOrFragment(node, this.startContainer); 1.2117 + 1.2118 + if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { 1.2119 + return -1; 1.2120 + } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { 1.2121 + return 1; 1.2122 + } 1.2123 + return 0; 1.2124 + }, 1.2125 + 1.2126 + createContextualFragment: createContextualFragment, 1.2127 + 1.2128 + toHtml: function() { 1.2129 + return rangeToHtml(this); 1.2130 + }, 1.2131 + 1.2132 + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects 1.2133 + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) 1.2134 + intersectsNode: function(node, touchingIsIntersecting) { 1.2135 + assertRangeValid(this); 1.2136 + if (getRootContainer(node) != getRangeRoot(this)) { 1.2137 + return false; 1.2138 + } 1.2139 + 1.2140 + var parent = node.parentNode, offset = getNodeIndex(node); 1.2141 + if (!parent) { 1.2142 + return true; 1.2143 + } 1.2144 + 1.2145 + var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), 1.2146 + endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); 1.2147 + 1.2148 + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; 1.2149 + }, 1.2150 + 1.2151 + isPointInRange: function(node, offset) { 1.2152 + assertRangeValid(this); 1.2153 + assertNode(node, "HIERARCHY_REQUEST_ERR"); 1.2154 + assertSameDocumentOrFragment(node, this.startContainer); 1.2155 + 1.2156 + return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && 1.2157 + (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); 1.2158 + }, 1.2159 + 1.2160 + // The methods below are non-standard and invented by me. 1.2161 + 1.2162 + // Sharing a boundary start-to-end or end-to-start does not count as intersection. 1.2163 + intersectsRange: function(range) { 1.2164 + return rangesIntersect(this, range, false); 1.2165 + }, 1.2166 + 1.2167 + // Sharing a boundary start-to-end or end-to-start does count as intersection. 1.2168 + intersectsOrTouchesRange: function(range) { 1.2169 + return rangesIntersect(this, range, true); 1.2170 + }, 1.2171 + 1.2172 + intersection: function(range) { 1.2173 + if (this.intersectsRange(range)) { 1.2174 + var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), 1.2175 + endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); 1.2176 + 1.2177 + var intersectionRange = this.cloneRange(); 1.2178 + if (startComparison == -1) { 1.2179 + intersectionRange.setStart(range.startContainer, range.startOffset); 1.2180 + } 1.2181 + if (endComparison == 1) { 1.2182 + intersectionRange.setEnd(range.endContainer, range.endOffset); 1.2183 + } 1.2184 + return intersectionRange; 1.2185 + } 1.2186 + return null; 1.2187 + }, 1.2188 + 1.2189 + union: function(range) { 1.2190 + if (this.intersectsOrTouchesRange(range)) { 1.2191 + var unionRange = this.cloneRange(); 1.2192 + if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { 1.2193 + unionRange.setStart(range.startContainer, range.startOffset); 1.2194 + } 1.2195 + if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { 1.2196 + unionRange.setEnd(range.endContainer, range.endOffset); 1.2197 + } 1.2198 + return unionRange; 1.2199 + } else { 1.2200 + throw new DOMException("Ranges do not intersect"); 1.2201 + } 1.2202 + }, 1.2203 + 1.2204 + containsNode: function(node, allowPartial) { 1.2205 + if (allowPartial) { 1.2206 + return this.intersectsNode(node, false); 1.2207 + } else { 1.2208 + return this.compareNode(node) == n_i; 1.2209 + } 1.2210 + }, 1.2211 + 1.2212 + containsNodeContents: function(node) { 1.2213 + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; 1.2214 + }, 1.2215 + 1.2216 + containsRange: function(range) { 1.2217 + var intersection = this.intersection(range); 1.2218 + return intersection !== null && range.equals(intersection); 1.2219 + }, 1.2220 + 1.2221 + containsNodeText: function(node) { 1.2222 + var nodeRange = this.cloneRange(); 1.2223 + nodeRange.selectNode(node); 1.2224 + var textNodes = nodeRange.getNodes([3]); 1.2225 + if (textNodes.length > 0) { 1.2226 + nodeRange.setStart(textNodes[0], 0); 1.2227 + var lastTextNode = textNodes.pop(); 1.2228 + nodeRange.setEnd(lastTextNode, lastTextNode.length); 1.2229 + return this.containsRange(nodeRange); 1.2230 + } else { 1.2231 + return this.containsNodeContents(node); 1.2232 + } 1.2233 + }, 1.2234 + 1.2235 + getNodes: function(nodeTypes, filter) { 1.2236 + assertRangeValid(this); 1.2237 + return getNodesInRange(this, nodeTypes, filter); 1.2238 + }, 1.2239 + 1.2240 + getDocument: function() { 1.2241 + return getRangeDocument(this); 1.2242 + }, 1.2243 + 1.2244 + collapseBefore: function(node) { 1.2245 + this.setEndBefore(node); 1.2246 + this.collapse(false); 1.2247 + }, 1.2248 + 1.2249 + collapseAfter: function(node) { 1.2250 + this.setStartAfter(node); 1.2251 + this.collapse(true); 1.2252 + }, 1.2253 + 1.2254 + getBookmark: function(containerNode) { 1.2255 + var doc = getRangeDocument(this); 1.2256 + var preSelectionRange = api.createRange(doc); 1.2257 + containerNode = containerNode || dom.getBody(doc); 1.2258 + preSelectionRange.selectNodeContents(containerNode); 1.2259 + var range = this.intersection(preSelectionRange); 1.2260 + var start = 0, end = 0; 1.2261 + if (range) { 1.2262 + preSelectionRange.setEnd(range.startContainer, range.startOffset); 1.2263 + start = preSelectionRange.toString().length; 1.2264 + end = start + range.toString().length; 1.2265 + } 1.2266 + 1.2267 + return { 1.2268 + start: start, 1.2269 + end: end, 1.2270 + containerNode: containerNode 1.2271 + }; 1.2272 + }, 1.2273 + 1.2274 + moveToBookmark: function(bookmark) { 1.2275 + var containerNode = bookmark.containerNode; 1.2276 + var charIndex = 0; 1.2277 + this.setStart(containerNode, 0); 1.2278 + this.collapse(true); 1.2279 + var nodeStack = [containerNode], node, foundStart = false, stop = false; 1.2280 + var nextCharIndex, i, childNodes; 1.2281 + 1.2282 + while (!stop && (node = nodeStack.pop())) { 1.2283 + if (node.nodeType == 3) { 1.2284 + nextCharIndex = charIndex + node.length; 1.2285 + if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { 1.2286 + this.setStart(node, bookmark.start - charIndex); 1.2287 + foundStart = true; 1.2288 + } 1.2289 + if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { 1.2290 + this.setEnd(node, bookmark.end - charIndex); 1.2291 + stop = true; 1.2292 + } 1.2293 + charIndex = nextCharIndex; 1.2294 + } else { 1.2295 + childNodes = node.childNodes; 1.2296 + i = childNodes.length; 1.2297 + while (i--) { 1.2298 + nodeStack.push(childNodes[i]); 1.2299 + } 1.2300 + } 1.2301 + } 1.2302 + }, 1.2303 + 1.2304 + getName: function() { 1.2305 + return "DomRange"; 1.2306 + }, 1.2307 + 1.2308 + equals: function(range) { 1.2309 + return Range.rangesEqual(this, range); 1.2310 + }, 1.2311 + 1.2312 + isValid: function() { 1.2313 + return isRangeValid(this); 1.2314 + }, 1.2315 + 1.2316 + inspect: function() { 1.2317 + return inspect(this); 1.2318 + }, 1.2319 + 1.2320 + detach: function() { 1.2321 + // In DOM4, detach() is now a no-op. 1.2322 + } 1.2323 + }); 1.2324 + 1.2325 + function copyComparisonConstantsToObject(obj) { 1.2326 + obj.START_TO_START = s2s; 1.2327 + obj.START_TO_END = s2e; 1.2328 + obj.END_TO_END = e2e; 1.2329 + obj.END_TO_START = e2s; 1.2330 + 1.2331 + obj.NODE_BEFORE = n_b; 1.2332 + obj.NODE_AFTER = n_a; 1.2333 + obj.NODE_BEFORE_AND_AFTER = n_b_a; 1.2334 + obj.NODE_INSIDE = n_i; 1.2335 + } 1.2336 + 1.2337 + function copyComparisonConstants(constructor) { 1.2338 + copyComparisonConstantsToObject(constructor); 1.2339 + copyComparisonConstantsToObject(constructor.prototype); 1.2340 + } 1.2341 + 1.2342 + function createRangeContentRemover(remover, boundaryUpdater) { 1.2343 + return function() { 1.2344 + assertRangeValid(this); 1.2345 + 1.2346 + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; 1.2347 + 1.2348 + var iterator = new RangeIterator(this, true); 1.2349 + 1.2350 + // Work out where to position the range after content removal 1.2351 + var node, boundary; 1.2352 + if (sc !== root) { 1.2353 + node = getClosestAncestorIn(sc, root, true); 1.2354 + boundary = getBoundaryAfterNode(node); 1.2355 + sc = boundary.node; 1.2356 + so = boundary.offset; 1.2357 + } 1.2358 + 1.2359 + // Check none of the range is read-only 1.2360 + iterateSubtree(iterator, assertNodeNotReadOnly); 1.2361 + 1.2362 + iterator.reset(); 1.2363 + 1.2364 + // Remove the content 1.2365 + var returnValue = remover(iterator); 1.2366 + iterator.detach(); 1.2367 + 1.2368 + // Move to the new position 1.2369 + boundaryUpdater(this, sc, so, sc, so); 1.2370 + 1.2371 + return returnValue; 1.2372 + }; 1.2373 + } 1.2374 + 1.2375 + function createPrototypeRange(constructor, boundaryUpdater) { 1.2376 + function createBeforeAfterNodeSetter(isBefore, isStart) { 1.2377 + return function(node) { 1.2378 + assertValidNodeType(node, beforeAfterNodeTypes); 1.2379 + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); 1.2380 + 1.2381 + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); 1.2382 + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); 1.2383 + }; 1.2384 + } 1.2385 + 1.2386 + function setRangeStart(range, node, offset) { 1.2387 + var ec = range.endContainer, eo = range.endOffset; 1.2388 + if (node !== range.startContainer || offset !== range.startOffset) { 1.2389 + // Check the root containers of the range and the new boundary, and also check whether the new boundary 1.2390 + // is after the current end. In either case, collapse the range to the new position 1.2391 + if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { 1.2392 + ec = node; 1.2393 + eo = offset; 1.2394 + } 1.2395 + boundaryUpdater(range, node, offset, ec, eo); 1.2396 + } 1.2397 + } 1.2398 + 1.2399 + function setRangeEnd(range, node, offset) { 1.2400 + var sc = range.startContainer, so = range.startOffset; 1.2401 + if (node !== range.endContainer || offset !== range.endOffset) { 1.2402 + // Check the root containers of the range and the new boundary, and also check whether the new boundary 1.2403 + // is after the current end. In either case, collapse the range to the new position 1.2404 + if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { 1.2405 + sc = node; 1.2406 + so = offset; 1.2407 + } 1.2408 + boundaryUpdater(range, sc, so, node, offset); 1.2409 + } 1.2410 + } 1.2411 + 1.2412 + // Set up inheritance 1.2413 + var F = function() {}; 1.2414 + F.prototype = api.rangePrototype; 1.2415 + constructor.prototype = new F(); 1.2416 + 1.2417 + util.extend(constructor.prototype, { 1.2418 + setStart: function(node, offset) { 1.2419 + assertNoDocTypeNotationEntityAncestor(node, true); 1.2420 + assertValidOffset(node, offset); 1.2421 + 1.2422 + setRangeStart(this, node, offset); 1.2423 + }, 1.2424 + 1.2425 + setEnd: function(node, offset) { 1.2426 + assertNoDocTypeNotationEntityAncestor(node, true); 1.2427 + assertValidOffset(node, offset); 1.2428 + 1.2429 + setRangeEnd(this, node, offset); 1.2430 + }, 1.2431 + 1.2432 + /** 1.2433 + * Convenience method to set a range's start and end boundaries. Overloaded as follows: 1.2434 + * - Two parameters (node, offset) creates a collapsed range at that position 1.2435 + * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at 1.2436 + * startOffset and ending at endOffset 1.2437 + * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in 1.2438 + * startNode and ending at endOffset in endNode 1.2439 + */ 1.2440 + setStartAndEnd: function() { 1.2441 + var args = arguments; 1.2442 + var sc = args[0], so = args[1], ec = sc, eo = so; 1.2443 + 1.2444 + switch (args.length) { 1.2445 + case 3: 1.2446 + eo = args[2]; 1.2447 + break; 1.2448 + case 4: 1.2449 + ec = args[2]; 1.2450 + eo = args[3]; 1.2451 + break; 1.2452 + } 1.2453 + 1.2454 + boundaryUpdater(this, sc, so, ec, eo); 1.2455 + }, 1.2456 + 1.2457 + setBoundary: function(node, offset, isStart) { 1.2458 + this["set" + (isStart ? "Start" : "End")](node, offset); 1.2459 + }, 1.2460 + 1.2461 + setStartBefore: createBeforeAfterNodeSetter(true, true), 1.2462 + setStartAfter: createBeforeAfterNodeSetter(false, true), 1.2463 + setEndBefore: createBeforeAfterNodeSetter(true, false), 1.2464 + setEndAfter: createBeforeAfterNodeSetter(false, false), 1.2465 + 1.2466 + collapse: function(isStart) { 1.2467 + assertRangeValid(this); 1.2468 + if (isStart) { 1.2469 + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); 1.2470 + } else { 1.2471 + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); 1.2472 + } 1.2473 + }, 1.2474 + 1.2475 + selectNodeContents: function(node) { 1.2476 + assertNoDocTypeNotationEntityAncestor(node, true); 1.2477 + 1.2478 + boundaryUpdater(this, node, 0, node, getNodeLength(node)); 1.2479 + }, 1.2480 + 1.2481 + selectNode: function(node) { 1.2482 + assertNoDocTypeNotationEntityAncestor(node, false); 1.2483 + assertValidNodeType(node, beforeAfterNodeTypes); 1.2484 + 1.2485 + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); 1.2486 + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); 1.2487 + }, 1.2488 + 1.2489 + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), 1.2490 + 1.2491 + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), 1.2492 + 1.2493 + canSurroundContents: function() { 1.2494 + assertRangeValid(this); 1.2495 + assertNodeNotReadOnly(this.startContainer); 1.2496 + assertNodeNotReadOnly(this.endContainer); 1.2497 + 1.2498 + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects 1.2499 + // no non-text nodes. 1.2500 + var iterator = new RangeIterator(this, true); 1.2501 + var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) || 1.2502 + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); 1.2503 + iterator.detach(); 1.2504 + return !boundariesInvalid; 1.2505 + }, 1.2506 + 1.2507 + splitBoundaries: function() { 1.2508 + splitRangeBoundaries(this); 1.2509 + }, 1.2510 + 1.2511 + splitBoundariesPreservingPositions: function(positionsToPreserve) { 1.2512 + splitRangeBoundaries(this, positionsToPreserve); 1.2513 + }, 1.2514 + 1.2515 + normalizeBoundaries: function() { 1.2516 + assertRangeValid(this); 1.2517 + 1.2518 + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; 1.2519 + 1.2520 + var mergeForward = function(node) { 1.2521 + var sibling = node.nextSibling; 1.2522 + if (sibling && sibling.nodeType == node.nodeType) { 1.2523 + ec = node; 1.2524 + eo = node.length; 1.2525 + node.appendData(sibling.data); 1.2526 + removeNode(sibling); 1.2527 + } 1.2528 + }; 1.2529 + 1.2530 + var mergeBackward = function(node) { 1.2531 + var sibling = node.previousSibling; 1.2532 + if (sibling && sibling.nodeType == node.nodeType) { 1.2533 + sc = node; 1.2534 + var nodeLength = node.length; 1.2535 + so = sibling.length; 1.2536 + node.insertData(0, sibling.data); 1.2537 + removeNode(sibling); 1.2538 + if (sc == ec) { 1.2539 + eo += so; 1.2540 + ec = sc; 1.2541 + } else if (ec == node.parentNode) { 1.2542 + var nodeIndex = getNodeIndex(node); 1.2543 + if (eo == nodeIndex) { 1.2544 + ec = node; 1.2545 + eo = nodeLength; 1.2546 + } else if (eo > nodeIndex) { 1.2547 + eo--; 1.2548 + } 1.2549 + } 1.2550 + } 1.2551 + }; 1.2552 + 1.2553 + var normalizeStart = true; 1.2554 + var sibling; 1.2555 + 1.2556 + if (isCharacterDataNode(ec)) { 1.2557 + if (eo == ec.length) { 1.2558 + mergeForward(ec); 1.2559 + } else if (eo == 0) { 1.2560 + sibling = ec.previousSibling; 1.2561 + if (sibling && sibling.nodeType == ec.nodeType) { 1.2562 + eo = sibling.length; 1.2563 + if (sc == ec) { 1.2564 + normalizeStart = false; 1.2565 + } 1.2566 + sibling.appendData(ec.data); 1.2567 + removeNode(ec); 1.2568 + ec = sibling; 1.2569 + } 1.2570 + } 1.2571 + } else { 1.2572 + if (eo > 0) { 1.2573 + var endNode = ec.childNodes[eo - 1]; 1.2574 + if (endNode && isCharacterDataNode(endNode)) { 1.2575 + mergeForward(endNode); 1.2576 + } 1.2577 + } 1.2578 + normalizeStart = !this.collapsed; 1.2579 + } 1.2580 + 1.2581 + if (normalizeStart) { 1.2582 + if (isCharacterDataNode(sc)) { 1.2583 + if (so == 0) { 1.2584 + mergeBackward(sc); 1.2585 + } else if (so == sc.length) { 1.2586 + sibling = sc.nextSibling; 1.2587 + if (sibling && sibling.nodeType == sc.nodeType) { 1.2588 + if (ec == sibling) { 1.2589 + ec = sc; 1.2590 + eo += sc.length; 1.2591 + } 1.2592 + sc.appendData(sibling.data); 1.2593 + removeNode(sibling); 1.2594 + } 1.2595 + } 1.2596 + } else { 1.2597 + if (so < sc.childNodes.length) { 1.2598 + var startNode = sc.childNodes[so]; 1.2599 + if (startNode && isCharacterDataNode(startNode)) { 1.2600 + mergeBackward(startNode); 1.2601 + } 1.2602 + } 1.2603 + } 1.2604 + } else { 1.2605 + sc = ec; 1.2606 + so = eo; 1.2607 + } 1.2608 + 1.2609 + boundaryUpdater(this, sc, so, ec, eo); 1.2610 + }, 1.2611 + 1.2612 + collapseToPoint: function(node, offset) { 1.2613 + assertNoDocTypeNotationEntityAncestor(node, true); 1.2614 + assertValidOffset(node, offset); 1.2615 + this.setStartAndEnd(node, offset); 1.2616 + } 1.2617 + }); 1.2618 + 1.2619 + copyComparisonConstants(constructor); 1.2620 + } 1.2621 + 1.2622 + /*----------------------------------------------------------------------------------------------------------------*/ 1.2623 + 1.2624 + // Updates commonAncestorContainer and collapsed after boundary change 1.2625 + function updateCollapsedAndCommonAncestor(range) { 1.2626 + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 1.2627 + range.commonAncestorContainer = range.collapsed ? 1.2628 + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); 1.2629 + } 1.2630 + 1.2631 + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { 1.2632 + range.startContainer = startContainer; 1.2633 + range.startOffset = startOffset; 1.2634 + range.endContainer = endContainer; 1.2635 + range.endOffset = endOffset; 1.2636 + range.document = dom.getDocument(startContainer); 1.2637 + 1.2638 + updateCollapsedAndCommonAncestor(range); 1.2639 + } 1.2640 + 1.2641 + function Range(doc) { 1.2642 + this.startContainer = doc; 1.2643 + this.startOffset = 0; 1.2644 + this.endContainer = doc; 1.2645 + this.endOffset = 0; 1.2646 + this.document = doc; 1.2647 + updateCollapsedAndCommonAncestor(this); 1.2648 + } 1.2649 + 1.2650 + createPrototypeRange(Range, updateBoundaries); 1.2651 + 1.2652 + util.extend(Range, { 1.2653 + rangeProperties: rangeProperties, 1.2654 + RangeIterator: RangeIterator, 1.2655 + copyComparisonConstants: copyComparisonConstants, 1.2656 + createPrototypeRange: createPrototypeRange, 1.2657 + inspect: inspect, 1.2658 + toHtml: rangeToHtml, 1.2659 + getRangeDocument: getRangeDocument, 1.2660 + rangesEqual: function(r1, r2) { 1.2661 + return r1.startContainer === r2.startContainer && 1.2662 + r1.startOffset === r2.startOffset && 1.2663 + r1.endContainer === r2.endContainer && 1.2664 + r1.endOffset === r2.endOffset; 1.2665 + } 1.2666 + }); 1.2667 + 1.2668 + api.DomRange = Range; 1.2669 + }); 1.2670 + 1.2671 + /*----------------------------------------------------------------------------------------------------------------*/ 1.2672 + 1.2673 + // Wrappers for the browser's native DOM Range and/or TextRange implementation 1.2674 + api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) { 1.2675 + var WrappedRange, WrappedTextRange; 1.2676 + var dom = api.dom; 1.2677 + var util = api.util; 1.2678 + var DomPosition = dom.DomPosition; 1.2679 + var DomRange = api.DomRange; 1.2680 + var getBody = dom.getBody; 1.2681 + var getContentDocument = dom.getContentDocument; 1.2682 + var isCharacterDataNode = dom.isCharacterDataNode; 1.2683 + 1.2684 + 1.2685 + /*----------------------------------------------------------------------------------------------------------------*/ 1.2686 + 1.2687 + if (api.features.implementsDomRange) { 1.2688 + // This is a wrapper around the browser's native DOM Range. It has two aims: 1.2689 + // - Provide workarounds for specific browser bugs 1.2690 + // - provide convenient extensions, which are inherited from Rangy's DomRange 1.2691 + 1.2692 + (function() { 1.2693 + var rangeProto; 1.2694 + var rangeProperties = DomRange.rangeProperties; 1.2695 + 1.2696 + function updateRangeProperties(range) { 1.2697 + var i = rangeProperties.length, prop; 1.2698 + while (i--) { 1.2699 + prop = rangeProperties[i]; 1.2700 + range[prop] = range.nativeRange[prop]; 1.2701 + } 1.2702 + // Fix for broken collapsed property in IE 9. 1.2703 + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 1.2704 + } 1.2705 + 1.2706 + function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { 1.2707 + var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset); 1.2708 + var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset); 1.2709 + var nativeRangeDifferent = !range.equals(range.nativeRange); 1.2710 + 1.2711 + // Always set both boundaries for the benefit of IE9 (see issue 35) 1.2712 + if (startMoved || endMoved || nativeRangeDifferent) { 1.2713 + range.setEnd(endContainer, endOffset); 1.2714 + range.setStart(startContainer, startOffset); 1.2715 + } 1.2716 + } 1.2717 + 1.2718 + var createBeforeAfterNodeSetter; 1.2719 + 1.2720 + WrappedRange = function(range) { 1.2721 + if (!range) { 1.2722 + throw module.createError("WrappedRange: Range must be specified"); 1.2723 + } 1.2724 + this.nativeRange = range; 1.2725 + updateRangeProperties(this); 1.2726 + }; 1.2727 + 1.2728 + DomRange.createPrototypeRange(WrappedRange, updateNativeRange); 1.2729 + 1.2730 + rangeProto = WrappedRange.prototype; 1.2731 + 1.2732 + rangeProto.selectNode = function(node) { 1.2733 + this.nativeRange.selectNode(node); 1.2734 + updateRangeProperties(this); 1.2735 + }; 1.2736 + 1.2737 + rangeProto.cloneContents = function() { 1.2738 + return this.nativeRange.cloneContents(); 1.2739 + }; 1.2740 + 1.2741 + // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, 1.2742 + // insertNode() is never delegated to the native range. 1.2743 + 1.2744 + rangeProto.surroundContents = function(node) { 1.2745 + this.nativeRange.surroundContents(node); 1.2746 + updateRangeProperties(this); 1.2747 + }; 1.2748 + 1.2749 + rangeProto.collapse = function(isStart) { 1.2750 + this.nativeRange.collapse(isStart); 1.2751 + updateRangeProperties(this); 1.2752 + }; 1.2753 + 1.2754 + rangeProto.cloneRange = function() { 1.2755 + return new WrappedRange(this.nativeRange.cloneRange()); 1.2756 + }; 1.2757 + 1.2758 + rangeProto.refresh = function() { 1.2759 + updateRangeProperties(this); 1.2760 + }; 1.2761 + 1.2762 + rangeProto.toString = function() { 1.2763 + return this.nativeRange.toString(); 1.2764 + }; 1.2765 + 1.2766 + // Create test range and node for feature detection 1.2767 + 1.2768 + var testTextNode = document.createTextNode("test"); 1.2769 + getBody(document).appendChild(testTextNode); 1.2770 + var range = document.createRange(); 1.2771 + 1.2772 + /*--------------------------------------------------------------------------------------------------------*/ 1.2773 + 1.2774 + // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and 1.2775 + // correct for it 1.2776 + 1.2777 + range.setStart(testTextNode, 0); 1.2778 + range.setEnd(testTextNode, 0); 1.2779 + 1.2780 + try { 1.2781 + range.setStart(testTextNode, 1); 1.2782 + 1.2783 + rangeProto.setStart = function(node, offset) { 1.2784 + this.nativeRange.setStart(node, offset); 1.2785 + updateRangeProperties(this); 1.2786 + }; 1.2787 + 1.2788 + rangeProto.setEnd = function(node, offset) { 1.2789 + this.nativeRange.setEnd(node, offset); 1.2790 + updateRangeProperties(this); 1.2791 + }; 1.2792 + 1.2793 + createBeforeAfterNodeSetter = function(name) { 1.2794 + return function(node) { 1.2795 + this.nativeRange[name](node); 1.2796 + updateRangeProperties(this); 1.2797 + }; 1.2798 + }; 1.2799 + 1.2800 + } catch(ex) { 1.2801 + 1.2802 + rangeProto.setStart = function(node, offset) { 1.2803 + try { 1.2804 + this.nativeRange.setStart(node, offset); 1.2805 + } catch (ex) { 1.2806 + this.nativeRange.setEnd(node, offset); 1.2807 + this.nativeRange.setStart(node, offset); 1.2808 + } 1.2809 + updateRangeProperties(this); 1.2810 + }; 1.2811 + 1.2812 + rangeProto.setEnd = function(node, offset) { 1.2813 + try { 1.2814 + this.nativeRange.setEnd(node, offset); 1.2815 + } catch (ex) { 1.2816 + this.nativeRange.setStart(node, offset); 1.2817 + this.nativeRange.setEnd(node, offset); 1.2818 + } 1.2819 + updateRangeProperties(this); 1.2820 + }; 1.2821 + 1.2822 + createBeforeAfterNodeSetter = function(name, oppositeName) { 1.2823 + return function(node) { 1.2824 + try { 1.2825 + this.nativeRange[name](node); 1.2826 + } catch (ex) { 1.2827 + this.nativeRange[oppositeName](node); 1.2828 + this.nativeRange[name](node); 1.2829 + } 1.2830 + updateRangeProperties(this); 1.2831 + }; 1.2832 + }; 1.2833 + } 1.2834 + 1.2835 + rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); 1.2836 + rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); 1.2837 + rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); 1.2838 + rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); 1.2839 + 1.2840 + /*--------------------------------------------------------------------------------------------------------*/ 1.2841 + 1.2842 + // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing 1.2843 + // whether the native implementation can be trusted 1.2844 + rangeProto.selectNodeContents = function(node) { 1.2845 + this.setStartAndEnd(node, 0, dom.getNodeLength(node)); 1.2846 + }; 1.2847 + 1.2848 + /*--------------------------------------------------------------------------------------------------------*/ 1.2849 + 1.2850 + // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for 1.2851 + // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 1.2852 + 1.2853 + range.selectNodeContents(testTextNode); 1.2854 + range.setEnd(testTextNode, 3); 1.2855 + 1.2856 + var range2 = document.createRange(); 1.2857 + range2.selectNodeContents(testTextNode); 1.2858 + range2.setEnd(testTextNode, 4); 1.2859 + range2.setStart(testTextNode, 2); 1.2860 + 1.2861 + if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && 1.2862 + range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { 1.2863 + // This is the wrong way round, so correct for it 1.2864 + 1.2865 + rangeProto.compareBoundaryPoints = function(type, range) { 1.2866 + range = range.nativeRange || range; 1.2867 + if (type == range.START_TO_END) { 1.2868 + type = range.END_TO_START; 1.2869 + } else if (type == range.END_TO_START) { 1.2870 + type = range.START_TO_END; 1.2871 + } 1.2872 + return this.nativeRange.compareBoundaryPoints(type, range); 1.2873 + }; 1.2874 + } else { 1.2875 + rangeProto.compareBoundaryPoints = function(type, range) { 1.2876 + return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); 1.2877 + }; 1.2878 + } 1.2879 + 1.2880 + /*--------------------------------------------------------------------------------------------------------*/ 1.2881 + 1.2882 + // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. 1.2883 + 1.2884 + var el = document.createElement("div"); 1.2885 + el.innerHTML = "123"; 1.2886 + var textNode = el.firstChild; 1.2887 + var body = getBody(document); 1.2888 + body.appendChild(el); 1.2889 + 1.2890 + range.setStart(textNode, 1); 1.2891 + range.setEnd(textNode, 2); 1.2892 + range.deleteContents(); 1.2893 + 1.2894 + if (textNode.data == "13") { 1.2895 + // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and 1.2896 + // extractContents() 1.2897 + rangeProto.deleteContents = function() { 1.2898 + this.nativeRange.deleteContents(); 1.2899 + updateRangeProperties(this); 1.2900 + }; 1.2901 + 1.2902 + rangeProto.extractContents = function() { 1.2903 + var frag = this.nativeRange.extractContents(); 1.2904 + updateRangeProperties(this); 1.2905 + return frag; 1.2906 + }; 1.2907 + } else { 1.2908 + } 1.2909 + 1.2910 + body.removeChild(el); 1.2911 + body = null; 1.2912 + 1.2913 + /*--------------------------------------------------------------------------------------------------------*/ 1.2914 + 1.2915 + // Test for existence of createContextualFragment and delegate to it if it exists 1.2916 + if (util.isHostMethod(range, "createContextualFragment")) { 1.2917 + rangeProto.createContextualFragment = function(fragmentStr) { 1.2918 + return this.nativeRange.createContextualFragment(fragmentStr); 1.2919 + }; 1.2920 + } 1.2921 + 1.2922 + /*--------------------------------------------------------------------------------------------------------*/ 1.2923 + 1.2924 + // Clean up 1.2925 + getBody(document).removeChild(testTextNode); 1.2926 + 1.2927 + rangeProto.getName = function() { 1.2928 + return "WrappedRange"; 1.2929 + }; 1.2930 + 1.2931 + api.WrappedRange = WrappedRange; 1.2932 + 1.2933 + api.createNativeRange = function(doc) { 1.2934 + doc = getContentDocument(doc, module, "createNativeRange"); 1.2935 + return doc.createRange(); 1.2936 + }; 1.2937 + })(); 1.2938 + } 1.2939 + 1.2940 + if (api.features.implementsTextRange) { 1.2941 + /* 1.2942 + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() 1.2943 + method. For example, in the following (where pipes denote the selection boundaries): 1.2944 + 1.2945 + <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> 1.2946 + 1.2947 + var range = document.selection.createRange(); 1.2948 + alert(range.parentElement().id); // Should alert "ul" but alerts "b" 1.2949 + 1.2950 + This method returns the common ancestor node of the following: 1.2951 + - the parentElement() of the textRange 1.2952 + - the parentElement() of the textRange after calling collapse(true) 1.2953 + - the parentElement() of the textRange after calling collapse(false) 1.2954 + */ 1.2955 + var getTextRangeContainerElement = function(textRange) { 1.2956 + var parentEl = textRange.parentElement(); 1.2957 + var range = textRange.duplicate(); 1.2958 + range.collapse(true); 1.2959 + var startEl = range.parentElement(); 1.2960 + range = textRange.duplicate(); 1.2961 + range.collapse(false); 1.2962 + var endEl = range.parentElement(); 1.2963 + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); 1.2964 + 1.2965 + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); 1.2966 + }; 1.2967 + 1.2968 + var textRangeIsCollapsed = function(textRange) { 1.2969 + return textRange.compareEndPoints("StartToEnd", textRange) == 0; 1.2970 + }; 1.2971 + 1.2972 + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started 1.2973 + // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) 1.2974 + // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange 1.2975 + // bugs, handling for inputs and images, plus optimizations. 1.2976 + var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { 1.2977 + var workingRange = textRange.duplicate(); 1.2978 + workingRange.collapse(isStart); 1.2979 + var containerElement = workingRange.parentElement(); 1.2980 + 1.2981 + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so 1.2982 + // check for that 1.2983 + if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { 1.2984 + containerElement = wholeRangeContainerElement; 1.2985 + } 1.2986 + 1.2987 + 1.2988 + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and 1.2989 + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx 1.2990 + if (!containerElement.canHaveHTML) { 1.2991 + var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); 1.2992 + return { 1.2993 + boundaryPosition: pos, 1.2994 + nodeInfo: { 1.2995 + nodeIndex: pos.offset, 1.2996 + containerElement: pos.node 1.2997 + } 1.2998 + }; 1.2999 + } 1.3000 + 1.3001 + var workingNode = dom.getDocument(containerElement).createElement("span"); 1.3002 + 1.3003 + // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 1.3004 + // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 1.3005 + if (workingNode.parentNode) { 1.3006 + dom.removeNode(workingNode); 1.3007 + } 1.3008 + 1.3009 + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; 1.3010 + var previousNode, nextNode, boundaryPosition, boundaryNode; 1.3011 + var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0; 1.3012 + var childNodeCount = containerElement.childNodes.length; 1.3013 + var end = childNodeCount; 1.3014 + 1.3015 + // Check end first. Code within the loop assumes that the endth child node of the container is definitely 1.3016 + // after the range boundary. 1.3017 + var nodeIndex = end; 1.3018 + 1.3019 + while (true) { 1.3020 + if (nodeIndex == childNodeCount) { 1.3021 + containerElement.appendChild(workingNode); 1.3022 + } else { 1.3023 + containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); 1.3024 + } 1.3025 + workingRange.moveToElementText(workingNode); 1.3026 + comparison = workingRange.compareEndPoints(workingComparisonType, textRange); 1.3027 + if (comparison == 0 || start == end) { 1.3028 + break; 1.3029 + } else if (comparison == -1) { 1.3030 + if (end == start + 1) { 1.3031 + // We know the endth child node is after the range boundary, so we must be done. 1.3032 + break; 1.3033 + } else { 1.3034 + start = nodeIndex; 1.3035 + } 1.3036 + } else { 1.3037 + end = (end == start + 1) ? start : nodeIndex; 1.3038 + } 1.3039 + nodeIndex = Math.floor((start + end) / 2); 1.3040 + containerElement.removeChild(workingNode); 1.3041 + } 1.3042 + 1.3043 + 1.3044 + // We've now reached or gone past the boundary of the text range we're interested in 1.3045 + // so have identified the node we want 1.3046 + boundaryNode = workingNode.nextSibling; 1.3047 + 1.3048 + if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { 1.3049 + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of 1.3050 + // the node containing the text range's boundary, so we move the end of the working range to the 1.3051 + // boundary point and measure the length of its text to get the boundary's offset within the node. 1.3052 + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); 1.3053 + 1.3054 + var offset; 1.3055 + 1.3056 + if (/[\r\n]/.test(boundaryNode.data)) { 1.3057 + /* 1.3058 + For the particular case of a boundary within a text node containing rendered line breaks (within a 1.3059 + <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in 1.3060 + IE. The facts: 1.3061 + 1.3062 + - Each line break is represented as \r in the text node's data/nodeValue properties 1.3063 + - Each line break is represented as \r\n in the TextRange's 'text' property 1.3064 + - The 'text' property of the TextRange does not contain trailing line breaks 1.3065 + 1.3066 + To get round the problem presented by the final fact above, we can use the fact that TextRange's 1.3067 + moveStart() and moveEnd() methods return the actual number of characters moved, which is not 1.3068 + necessarily the same as the number of characters it was instructed to move. The simplest approach is 1.3069 + to use this to store the characters moved when moving both the start and end of the range to the 1.3070 + start of the document body and subtracting the start offset from the end offset (the 1.3071 + "move-negative-gazillion" method). However, this is extremely slow when the document is large and 1.3072 + the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to 1.3073 + the end of the document) has the same problem. 1.3074 + 1.3075 + Another approach that works is to use moveStart() to move the start boundary of the range up to the 1.3076 + end boundary one character at a time and incrementing a counter with the value returned by the 1.3077 + moveStart() call. However, the check for whether the start boundary has reached the end boundary is 1.3078 + expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected 1.3079 + by the location of the range within the document). 1.3080 + 1.3081 + The approach used below is a hybrid of the two methods above. It uses the fact that a string 1.3082 + containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot 1.3083 + be longer than the text of the TextRange, so the start of the range is moved that length initially 1.3084 + and then a character at a time to make up for any trailing line breaks not contained in the 'text' 1.3085 + property. This has good performance in most situations compared to the previous two methods. 1.3086 + */ 1.3087 + var tempRange = workingRange.duplicate(); 1.3088 + var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length; 1.3089 + 1.3090 + offset = tempRange.moveStart("character", rangeLength); 1.3091 + while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) { 1.3092 + offset++; 1.3093 + tempRange.moveStart("character", 1); 1.3094 + } 1.3095 + } else { 1.3096 + offset = workingRange.text.length; 1.3097 + } 1.3098 + boundaryPosition = new DomPosition(boundaryNode, offset); 1.3099 + } else { 1.3100 + 1.3101 + // If the boundary immediately follows a character data node and this is the end boundary, we should favour 1.3102 + // a position within that, and likewise for a start boundary preceding a character data node 1.3103 + previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; 1.3104 + nextNode = (isCollapsed || isStart) && workingNode.nextSibling; 1.3105 + if (nextNode && isCharacterDataNode(nextNode)) { 1.3106 + boundaryPosition = new DomPosition(nextNode, 0); 1.3107 + } else if (previousNode && isCharacterDataNode(previousNode)) { 1.3108 + boundaryPosition = new DomPosition(previousNode, previousNode.data.length); 1.3109 + } else { 1.3110 + boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); 1.3111 + } 1.3112 + } 1.3113 + 1.3114 + // Clean up 1.3115 + dom.removeNode(workingNode); 1.3116 + 1.3117 + return { 1.3118 + boundaryPosition: boundaryPosition, 1.3119 + nodeInfo: { 1.3120 + nodeIndex: nodeIndex, 1.3121 + containerElement: containerElement 1.3122 + } 1.3123 + }; 1.3124 + }; 1.3125 + 1.3126 + // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that 1.3127 + // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange 1.3128 + // (http://code.google.com/p/ierange/) 1.3129 + var createBoundaryTextRange = function(boundaryPosition, isStart) { 1.3130 + var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; 1.3131 + var doc = dom.getDocument(boundaryPosition.node); 1.3132 + var workingNode, childNodes, workingRange = getBody(doc).createTextRange(); 1.3133 + var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node); 1.3134 + 1.3135 + if (nodeIsDataNode) { 1.3136 + boundaryNode = boundaryPosition.node; 1.3137 + boundaryParent = boundaryNode.parentNode; 1.3138 + } else { 1.3139 + childNodes = boundaryPosition.node.childNodes; 1.3140 + boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; 1.3141 + boundaryParent = boundaryPosition.node; 1.3142 + } 1.3143 + 1.3144 + // Position the range immediately before the node containing the boundary 1.3145 + workingNode = doc.createElement("span"); 1.3146 + 1.3147 + // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within 1.3148 + // the element rather than immediately before or after it 1.3149 + workingNode.innerHTML = "&#feff;"; 1.3150 + 1.3151 + // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report 1.3152 + // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 1.3153 + if (boundaryNode) { 1.3154 + boundaryParent.insertBefore(workingNode, boundaryNode); 1.3155 + } else { 1.3156 + boundaryParent.appendChild(workingNode); 1.3157 + } 1.3158 + 1.3159 + workingRange.moveToElementText(workingNode); 1.3160 + workingRange.collapse(!isStart); 1.3161 + 1.3162 + // Clean up 1.3163 + boundaryParent.removeChild(workingNode); 1.3164 + 1.3165 + // Move the working range to the text offset, if required 1.3166 + if (nodeIsDataNode) { 1.3167 + workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); 1.3168 + } 1.3169 + 1.3170 + return workingRange; 1.3171 + }; 1.3172 + 1.3173 + /*------------------------------------------------------------------------------------------------------------*/ 1.3174 + 1.3175 + // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a 1.3176 + // prototype 1.3177 + 1.3178 + WrappedTextRange = function(textRange) { 1.3179 + this.textRange = textRange; 1.3180 + this.refresh(); 1.3181 + }; 1.3182 + 1.3183 + WrappedTextRange.prototype = new DomRange(document); 1.3184 + 1.3185 + WrappedTextRange.prototype.refresh = function() { 1.3186 + var start, end, startBoundary; 1.3187 + 1.3188 + // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. 1.3189 + var rangeContainerElement = getTextRangeContainerElement(this.textRange); 1.3190 + 1.3191 + if (textRangeIsCollapsed(this.textRange)) { 1.3192 + end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, 1.3193 + true).boundaryPosition; 1.3194 + } else { 1.3195 + startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); 1.3196 + start = startBoundary.boundaryPosition; 1.3197 + 1.3198 + // An optimization used here is that if the start and end boundaries have the same parent element, the 1.3199 + // search scope for the end boundary can be limited to exclude the portion of the element that precedes 1.3200 + // the start boundary 1.3201 + end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false, 1.3202 + startBoundary.nodeInfo).boundaryPosition; 1.3203 + } 1.3204 + 1.3205 + this.setStart(start.node, start.offset); 1.3206 + this.setEnd(end.node, end.offset); 1.3207 + }; 1.3208 + 1.3209 + WrappedTextRange.prototype.getName = function() { 1.3210 + return "WrappedTextRange"; 1.3211 + }; 1.3212 + 1.3213 + DomRange.copyComparisonConstants(WrappedTextRange); 1.3214 + 1.3215 + var rangeToTextRange = function(range) { 1.3216 + if (range.collapsed) { 1.3217 + return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 1.3218 + } else { 1.3219 + var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); 1.3220 + var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); 1.3221 + var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange(); 1.3222 + textRange.setEndPoint("StartToStart", startRange); 1.3223 + textRange.setEndPoint("EndToEnd", endRange); 1.3224 + return textRange; 1.3225 + } 1.3226 + }; 1.3227 + 1.3228 + WrappedTextRange.rangeToTextRange = rangeToTextRange; 1.3229 + 1.3230 + WrappedTextRange.prototype.toTextRange = function() { 1.3231 + return rangeToTextRange(this); 1.3232 + }; 1.3233 + 1.3234 + api.WrappedTextRange = WrappedTextRange; 1.3235 + 1.3236 + // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which 1.3237 + // implementation to use by default. 1.3238 + if (!api.features.implementsDomRange || api.config.preferTextRange) { 1.3239 + // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work 1.3240 + var globalObj = (function(f) { return f("return this;")(); })(Function); 1.3241 + if (typeof globalObj.Range == "undefined") { 1.3242 + globalObj.Range = WrappedTextRange; 1.3243 + } 1.3244 + 1.3245 + api.createNativeRange = function(doc) { 1.3246 + doc = getContentDocument(doc, module, "createNativeRange"); 1.3247 + return getBody(doc).createTextRange(); 1.3248 + }; 1.3249 + 1.3250 + api.WrappedRange = WrappedTextRange; 1.3251 + } 1.3252 + } 1.3253 + 1.3254 + api.createRange = function(doc) { 1.3255 + doc = getContentDocument(doc, module, "createRange"); 1.3256 + return new api.WrappedRange(api.createNativeRange(doc)); 1.3257 + }; 1.3258 + 1.3259 + api.createRangyRange = function(doc) { 1.3260 + doc = getContentDocument(doc, module, "createRangyRange"); 1.3261 + return new DomRange(doc); 1.3262 + }; 1.3263 + 1.3264 + util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange"); 1.3265 + util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange"); 1.3266 + 1.3267 + api.addShimListener(function(win) { 1.3268 + var doc = win.document; 1.3269 + if (typeof doc.createRange == "undefined") { 1.3270 + doc.createRange = function() { 1.3271 + return api.createRange(doc); 1.3272 + }; 1.3273 + } 1.3274 + doc = win = null; 1.3275 + }); 1.3276 + }); 1.3277 + 1.3278 + /*----------------------------------------------------------------------------------------------------------------*/ 1.3279 + 1.3280 + // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification 1.3281 + // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections) 1.3282 + api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) { 1.3283 + api.config.checkSelectionRanges = true; 1.3284 + 1.3285 + var BOOLEAN = "boolean"; 1.3286 + var NUMBER = "number"; 1.3287 + var dom = api.dom; 1.3288 + var util = api.util; 1.3289 + var isHostMethod = util.isHostMethod; 1.3290 + var DomRange = api.DomRange; 1.3291 + var WrappedRange = api.WrappedRange; 1.3292 + var DOMException = api.DOMException; 1.3293 + var DomPosition = dom.DomPosition; 1.3294 + var getNativeSelection; 1.3295 + var selectionIsCollapsed; 1.3296 + var features = api.features; 1.3297 + var CONTROL = "Control"; 1.3298 + var getDocument = dom.getDocument; 1.3299 + var getBody = dom.getBody; 1.3300 + var rangesEqual = DomRange.rangesEqual; 1.3301 + 1.3302 + 1.3303 + // Utility function to support direction parameters in the API that may be a string ("backward", "backwards", 1.3304 + // "forward" or "forwards") or a Boolean (true for backwards). 1.3305 + function isDirectionBackward(dir) { 1.3306 + return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir; 1.3307 + } 1.3308 + 1.3309 + function getWindow(win, methodName) { 1.3310 + if (!win) { 1.3311 + return window; 1.3312 + } else if (dom.isWindow(win)) { 1.3313 + return win; 1.3314 + } else if (win instanceof WrappedSelection) { 1.3315 + return win.win; 1.3316 + } else { 1.3317 + var doc = dom.getContentDocument(win, module, methodName); 1.3318 + return dom.getWindow(doc); 1.3319 + } 1.3320 + } 1.3321 + 1.3322 + function getWinSelection(winParam) { 1.3323 + return getWindow(winParam, "getWinSelection").getSelection(); 1.3324 + } 1.3325 + 1.3326 + function getDocSelection(winParam) { 1.3327 + return getWindow(winParam, "getDocSelection").document.selection; 1.3328 + } 1.3329 + 1.3330 + function winSelectionIsBackward(sel) { 1.3331 + var backward = false; 1.3332 + if (sel.anchorNode) { 1.3333 + backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); 1.3334 + } 1.3335 + return backward; 1.3336 + } 1.3337 + 1.3338 + // Test for the Range/TextRange and Selection features required 1.3339 + // Test for ability to retrieve selection 1.3340 + var implementsWinGetSelection = isHostMethod(window, "getSelection"), 1.3341 + implementsDocSelection = util.isHostObject(document, "selection"); 1.3342 + 1.3343 + features.implementsWinGetSelection = implementsWinGetSelection; 1.3344 + features.implementsDocSelection = implementsDocSelection; 1.3345 + 1.3346 + var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); 1.3347 + 1.3348 + if (useDocumentSelection) { 1.3349 + getNativeSelection = getDocSelection; 1.3350 + api.isSelectionValid = function(winParam) { 1.3351 + var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection; 1.3352 + 1.3353 + // Check whether the selection TextRange is actually contained within the correct document 1.3354 + return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc); 1.3355 + }; 1.3356 + } else if (implementsWinGetSelection) { 1.3357 + getNativeSelection = getWinSelection; 1.3358 + api.isSelectionValid = function() { 1.3359 + return true; 1.3360 + }; 1.3361 + } else { 1.3362 + module.fail("Neither document.selection or window.getSelection() detected."); 1.3363 + return false; 1.3364 + } 1.3365 + 1.3366 + api.getNativeSelection = getNativeSelection; 1.3367 + 1.3368 + var testSelection = getNativeSelection(); 1.3369 + 1.3370 + // In Firefox, the selection is null in an iframe with display: none. See issue #138. 1.3371 + if (!testSelection) { 1.3372 + module.fail("Native selection was null (possibly issue 138?)"); 1.3373 + return false; 1.3374 + } 1.3375 + 1.3376 + var testRange = api.createNativeRange(document); 1.3377 + var body = getBody(document); 1.3378 + 1.3379 + // Obtaining a range from a selection 1.3380 + var selectionHasAnchorAndFocus = util.areHostProperties(testSelection, 1.3381 + ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]); 1.3382 + 1.3383 + features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; 1.3384 + 1.3385 + // Test for existence of native selection extend() method 1.3386 + var selectionHasExtend = isHostMethod(testSelection, "extend"); 1.3387 + features.selectionHasExtend = selectionHasExtend; 1.3388 + 1.3389 + // Test if rangeCount exists 1.3390 + var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER); 1.3391 + features.selectionHasRangeCount = selectionHasRangeCount; 1.3392 + 1.3393 + var selectionSupportsMultipleRanges = false; 1.3394 + var collapsedNonEditableSelectionsSupported = true; 1.3395 + 1.3396 + var addRangeBackwardToNative = selectionHasExtend ? 1.3397 + function(nativeSelection, range) { 1.3398 + var doc = DomRange.getRangeDocument(range); 1.3399 + var endRange = api.createRange(doc); 1.3400 + endRange.collapseToPoint(range.endContainer, range.endOffset); 1.3401 + nativeSelection.addRange(getNativeRange(endRange)); 1.3402 + nativeSelection.extend(range.startContainer, range.startOffset); 1.3403 + } : null; 1.3404 + 1.3405 + if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && 1.3406 + typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) { 1.3407 + 1.3408 + (function() { 1.3409 + // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are 1.3410 + // performed on the current document's selection. See issue 109. 1.3411 + 1.3412 + // Note also that if a selection previously existed, it is wiped and later restored by these tests. This 1.3413 + // will result in the selection direction begin reversed if the original selection was backwards and the 1.3414 + // browser does not support setting backwards selections (Internet Explorer, I'm looking at you). 1.3415 + var sel = window.getSelection(); 1.3416 + if (sel) { 1.3417 + // Store the current selection 1.3418 + var originalSelectionRangeCount = sel.rangeCount; 1.3419 + var selectionHasMultipleRanges = (originalSelectionRangeCount > 1); 1.3420 + var originalSelectionRanges = []; 1.3421 + var originalSelectionBackward = winSelectionIsBackward(sel); 1.3422 + for (var i = 0; i < originalSelectionRangeCount; ++i) { 1.3423 + originalSelectionRanges[i] = sel.getRangeAt(i); 1.3424 + } 1.3425 + 1.3426 + // Create some test elements 1.3427 + var testEl = dom.createTestElement(document, "", false); 1.3428 + var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") ); 1.3429 + 1.3430 + // Test whether the native selection will allow a collapsed selection within a non-editable element 1.3431 + var r1 = document.createRange(); 1.3432 + 1.3433 + r1.setStart(textNode, 1); 1.3434 + r1.collapse(true); 1.3435 + sel.removeAllRanges(); 1.3436 + sel.addRange(r1); 1.3437 + collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); 1.3438 + sel.removeAllRanges(); 1.3439 + 1.3440 + // Test whether the native selection is capable of supporting multiple ranges. 1.3441 + if (!selectionHasMultipleRanges) { 1.3442 + // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a 1.3443 + // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's 1.3444 + // nothing we can do about this while retaining the feature test so we have to resort to a browser 1.3445 + // sniff. I'm not happy about it. See 1.3446 + // https://code.google.com/p/chromium/issues/detail?id=399791 1.3447 + var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /); 1.3448 + if (chromeMatch && parseInt(chromeMatch[1]) >= 36) { 1.3449 + selectionSupportsMultipleRanges = false; 1.3450 + } else { 1.3451 + var r2 = r1.cloneRange(); 1.3452 + r1.setStart(textNode, 0); 1.3453 + r2.setEnd(textNode, 3); 1.3454 + r2.setStart(textNode, 2); 1.3455 + sel.addRange(r1); 1.3456 + sel.addRange(r2); 1.3457 + selectionSupportsMultipleRanges = (sel.rangeCount == 2); 1.3458 + } 1.3459 + } 1.3460 + 1.3461 + // Clean up 1.3462 + dom.removeNode(testEl); 1.3463 + sel.removeAllRanges(); 1.3464 + 1.3465 + for (i = 0; i < originalSelectionRangeCount; ++i) { 1.3466 + if (i == 0 && originalSelectionBackward) { 1.3467 + if (addRangeBackwardToNative) { 1.3468 + addRangeBackwardToNative(sel, originalSelectionRanges[i]); 1.3469 + } else { 1.3470 + api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"); 1.3471 + sel.addRange(originalSelectionRanges[i]); 1.3472 + } 1.3473 + } else { 1.3474 + sel.addRange(originalSelectionRanges[i]); 1.3475 + } 1.3476 + } 1.3477 + } 1.3478 + })(); 1.3479 + } 1.3480 + 1.3481 + features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges; 1.3482 + features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; 1.3483 + 1.3484 + // ControlRanges 1.3485 + var implementsControlRange = false, testControlRange; 1.3486 + 1.3487 + if (body && isHostMethod(body, "createControlRange")) { 1.3488 + testControlRange = body.createControlRange(); 1.3489 + if (util.areHostProperties(testControlRange, ["item", "add"])) { 1.3490 + implementsControlRange = true; 1.3491 + } 1.3492 + } 1.3493 + features.implementsControlRange = implementsControlRange; 1.3494 + 1.3495 + // Selection collapsedness 1.3496 + if (selectionHasAnchorAndFocus) { 1.3497 + selectionIsCollapsed = function(sel) { 1.3498 + return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; 1.3499 + }; 1.3500 + } else { 1.3501 + selectionIsCollapsed = function(sel) { 1.3502 + return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; 1.3503 + }; 1.3504 + } 1.3505 + 1.3506 + function updateAnchorAndFocusFromRange(sel, range, backward) { 1.3507 + var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end"; 1.3508 + sel.anchorNode = range[anchorPrefix + "Container"]; 1.3509 + sel.anchorOffset = range[anchorPrefix + "Offset"]; 1.3510 + sel.focusNode = range[focusPrefix + "Container"]; 1.3511 + sel.focusOffset = range[focusPrefix + "Offset"]; 1.3512 + } 1.3513 + 1.3514 + function updateAnchorAndFocusFromNativeSelection(sel) { 1.3515 + var nativeSel = sel.nativeSelection; 1.3516 + sel.anchorNode = nativeSel.anchorNode; 1.3517 + sel.anchorOffset = nativeSel.anchorOffset; 1.3518 + sel.focusNode = nativeSel.focusNode; 1.3519 + sel.focusOffset = nativeSel.focusOffset; 1.3520 + } 1.3521 + 1.3522 + function updateEmptySelection(sel) { 1.3523 + sel.anchorNode = sel.focusNode = null; 1.3524 + sel.anchorOffset = sel.focusOffset = 0; 1.3525 + sel.rangeCount = 0; 1.3526 + sel.isCollapsed = true; 1.3527 + sel._ranges.length = 0; 1.3528 + } 1.3529 + 1.3530 + function getNativeRange(range) { 1.3531 + var nativeRange; 1.3532 + if (range instanceof DomRange) { 1.3533 + nativeRange = api.createNativeRange(range.getDocument()); 1.3534 + nativeRange.setEnd(range.endContainer, range.endOffset); 1.3535 + nativeRange.setStart(range.startContainer, range.startOffset); 1.3536 + } else if (range instanceof WrappedRange) { 1.3537 + nativeRange = range.nativeRange; 1.3538 + } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { 1.3539 + nativeRange = range; 1.3540 + } 1.3541 + return nativeRange; 1.3542 + } 1.3543 + 1.3544 + function rangeContainsSingleElement(rangeNodes) { 1.3545 + if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { 1.3546 + return false; 1.3547 + } 1.3548 + for (var i = 1, len = rangeNodes.length; i < len; ++i) { 1.3549 + if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { 1.3550 + return false; 1.3551 + } 1.3552 + } 1.3553 + return true; 1.3554 + } 1.3555 + 1.3556 + function getSingleElementFromRange(range) { 1.3557 + var nodes = range.getNodes(); 1.3558 + if (!rangeContainsSingleElement(nodes)) { 1.3559 + throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); 1.3560 + } 1.3561 + return nodes[0]; 1.3562 + } 1.3563 + 1.3564 + // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange 1.3565 + function isTextRange(range) { 1.3566 + return !!range && typeof range.text != "undefined"; 1.3567 + } 1.3568 + 1.3569 + function updateFromTextRange(sel, range) { 1.3570 + // Create a Range from the selected TextRange 1.3571 + var wrappedRange = new WrappedRange(range); 1.3572 + sel._ranges = [wrappedRange]; 1.3573 + 1.3574 + updateAnchorAndFocusFromRange(sel, wrappedRange, false); 1.3575 + sel.rangeCount = 1; 1.3576 + sel.isCollapsed = wrappedRange.collapsed; 1.3577 + } 1.3578 + 1.3579 + function updateControlSelection(sel) { 1.3580 + // Update the wrapped selection based on what's now in the native selection 1.3581 + sel._ranges.length = 0; 1.3582 + if (sel.docSelection.type == "None") { 1.3583 + updateEmptySelection(sel); 1.3584 + } else { 1.3585 + var controlRange = sel.docSelection.createRange(); 1.3586 + if (isTextRange(controlRange)) { 1.3587 + // This case (where the selection type is "Control" and calling createRange() on the selection returns 1.3588 + // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected 1.3589 + // ControlRange have been removed from the ControlRange and removed from the document. 1.3590 + updateFromTextRange(sel, controlRange); 1.3591 + } else { 1.3592 + sel.rangeCount = controlRange.length; 1.3593 + var range, doc = getDocument(controlRange.item(0)); 1.3594 + for (var i = 0; i < sel.rangeCount; ++i) { 1.3595 + range = api.createRange(doc); 1.3596 + range.selectNode(controlRange.item(i)); 1.3597 + sel._ranges.push(range); 1.3598 + } 1.3599 + sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; 1.3600 + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); 1.3601 + } 1.3602 + } 1.3603 + } 1.3604 + 1.3605 + function addRangeToControlSelection(sel, range) { 1.3606 + var controlRange = sel.docSelection.createRange(); 1.3607 + var rangeElement = getSingleElementFromRange(range); 1.3608 + 1.3609 + // Create a new ControlRange containing all the elements in the selected ControlRange plus the element 1.3610 + // contained by the supplied range 1.3611 + var doc = getDocument(controlRange.item(0)); 1.3612 + var newControlRange = getBody(doc).createControlRange(); 1.3613 + for (var i = 0, len = controlRange.length; i < len; ++i) { 1.3614 + newControlRange.add(controlRange.item(i)); 1.3615 + } 1.3616 + try { 1.3617 + newControlRange.add(rangeElement); 1.3618 + } catch (ex) { 1.3619 + throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); 1.3620 + } 1.3621 + newControlRange.select(); 1.3622 + 1.3623 + // Update the wrapped selection based on what's now in the native selection 1.3624 + updateControlSelection(sel); 1.3625 + } 1.3626 + 1.3627 + var getSelectionRangeAt; 1.3628 + 1.3629 + if (isHostMethod(testSelection, "getRangeAt")) { 1.3630 + // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation. 1.3631 + // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a 1.3632 + // lesson to us all, especially me. 1.3633 + getSelectionRangeAt = function(sel, index) { 1.3634 + try { 1.3635 + return sel.getRangeAt(index); 1.3636 + } catch (ex) { 1.3637 + return null; 1.3638 + } 1.3639 + }; 1.3640 + } else if (selectionHasAnchorAndFocus) { 1.3641 + getSelectionRangeAt = function(sel) { 1.3642 + var doc = getDocument(sel.anchorNode); 1.3643 + var range = api.createRange(doc); 1.3644 + range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset); 1.3645 + 1.3646 + // Handle the case when the selection was selected backwards (from the end to the start in the 1.3647 + // document) 1.3648 + if (range.collapsed !== this.isCollapsed) { 1.3649 + range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset); 1.3650 + } 1.3651 + 1.3652 + return range; 1.3653 + }; 1.3654 + } 1.3655 + 1.3656 + function WrappedSelection(selection, docSelection, win) { 1.3657 + this.nativeSelection = selection; 1.3658 + this.docSelection = docSelection; 1.3659 + this._ranges = []; 1.3660 + this.win = win; 1.3661 + this.refresh(); 1.3662 + } 1.3663 + 1.3664 + WrappedSelection.prototype = api.selectionPrototype; 1.3665 + 1.3666 + function deleteProperties(sel) { 1.3667 + sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null; 1.3668 + sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0; 1.3669 + sel.detached = true; 1.3670 + } 1.3671 + 1.3672 + var cachedRangySelections = []; 1.3673 + 1.3674 + function actOnCachedSelection(win, action) { 1.3675 + var i = cachedRangySelections.length, cached, sel; 1.3676 + while (i--) { 1.3677 + cached = cachedRangySelections[i]; 1.3678 + sel = cached.selection; 1.3679 + if (action == "deleteAll") { 1.3680 + deleteProperties(sel); 1.3681 + } else if (cached.win == win) { 1.3682 + if (action == "delete") { 1.3683 + cachedRangySelections.splice(i, 1); 1.3684 + return true; 1.3685 + } else { 1.3686 + return sel; 1.3687 + } 1.3688 + } 1.3689 + } 1.3690 + if (action == "deleteAll") { 1.3691 + cachedRangySelections.length = 0; 1.3692 + } 1.3693 + return null; 1.3694 + } 1.3695 + 1.3696 + var getSelection = function(win) { 1.3697 + // Check if the parameter is a Rangy Selection object 1.3698 + if (win && win instanceof WrappedSelection) { 1.3699 + win.refresh(); 1.3700 + return win; 1.3701 + } 1.3702 + 1.3703 + win = getWindow(win, "getNativeSelection"); 1.3704 + 1.3705 + var sel = actOnCachedSelection(win); 1.3706 + var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; 1.3707 + if (sel) { 1.3708 + sel.nativeSelection = nativeSel; 1.3709 + sel.docSelection = docSel; 1.3710 + sel.refresh(); 1.3711 + } else { 1.3712 + sel = new WrappedSelection(nativeSel, docSel, win); 1.3713 + cachedRangySelections.push( { win: win, selection: sel } ); 1.3714 + } 1.3715 + return sel; 1.3716 + }; 1.3717 + 1.3718 + api.getSelection = getSelection; 1.3719 + 1.3720 + util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection"); 1.3721 + 1.3722 + var selProto = WrappedSelection.prototype; 1.3723 + 1.3724 + function createControlSelection(sel, ranges) { 1.3725 + // Ensure that the selection becomes of type "Control" 1.3726 + var doc = getDocument(ranges[0].startContainer); 1.3727 + var controlRange = getBody(doc).createControlRange(); 1.3728 + for (var i = 0, el, len = ranges.length; i < len; ++i) { 1.3729 + el = getSingleElementFromRange(ranges[i]); 1.3730 + try { 1.3731 + controlRange.add(el); 1.3732 + } catch (ex) { 1.3733 + throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)"); 1.3734 + } 1.3735 + } 1.3736 + controlRange.select(); 1.3737 + 1.3738 + // Update the wrapped selection based on what's now in the native selection 1.3739 + updateControlSelection(sel); 1.3740 + } 1.3741 + 1.3742 + // Selecting a range 1.3743 + if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { 1.3744 + selProto.removeAllRanges = function() { 1.3745 + this.nativeSelection.removeAllRanges(); 1.3746 + updateEmptySelection(this); 1.3747 + }; 1.3748 + 1.3749 + var addRangeBackward = function(sel, range) { 1.3750 + addRangeBackwardToNative(sel.nativeSelection, range); 1.3751 + sel.refresh(); 1.3752 + }; 1.3753 + 1.3754 + if (selectionHasRangeCount) { 1.3755 + selProto.addRange = function(range, direction) { 1.3756 + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 1.3757 + addRangeToControlSelection(this, range); 1.3758 + } else { 1.3759 + if (isDirectionBackward(direction) && selectionHasExtend) { 1.3760 + addRangeBackward(this, range); 1.3761 + } else { 1.3762 + var previousRangeCount; 1.3763 + if (selectionSupportsMultipleRanges) { 1.3764 + previousRangeCount = this.rangeCount; 1.3765 + } else { 1.3766 + this.removeAllRanges(); 1.3767 + previousRangeCount = 0; 1.3768 + } 1.3769 + // Clone the native range so that changing the selected range does not affect the selection. 1.3770 + // This is contrary to the spec but is the only way to achieve consistency between browsers. See 1.3771 + // issue 80. 1.3772 + var clonedNativeRange = getNativeRange(range).cloneRange(); 1.3773 + try { 1.3774 + this.nativeSelection.addRange(clonedNativeRange); 1.3775 + } catch (ex) { 1.3776 + } 1.3777 + 1.3778 + // Check whether adding the range was successful 1.3779 + this.rangeCount = this.nativeSelection.rangeCount; 1.3780 + 1.3781 + if (this.rangeCount == previousRangeCount + 1) { 1.3782 + // The range was added successfully 1.3783 + 1.3784 + // Check whether the range that we added to the selection is reflected in the last range extracted from 1.3785 + // the selection 1.3786 + if (api.config.checkSelectionRanges) { 1.3787 + var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); 1.3788 + if (nativeRange && !rangesEqual(nativeRange, range)) { 1.3789 + // Happens in WebKit with, for example, a selection placed at the start of a text node 1.3790 + range = new WrappedRange(nativeRange); 1.3791 + } 1.3792 + } 1.3793 + this._ranges[this.rangeCount - 1] = range; 1.3794 + updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection)); 1.3795 + this.isCollapsed = selectionIsCollapsed(this); 1.3796 + } else { 1.3797 + // The range was not added successfully. The simplest thing is to refresh 1.3798 + this.refresh(); 1.3799 + } 1.3800 + } 1.3801 + } 1.3802 + }; 1.3803 + } else { 1.3804 + selProto.addRange = function(range, direction) { 1.3805 + if (isDirectionBackward(direction) && selectionHasExtend) { 1.3806 + addRangeBackward(this, range); 1.3807 + } else { 1.3808 + this.nativeSelection.addRange(getNativeRange(range)); 1.3809 + this.refresh(); 1.3810 + } 1.3811 + }; 1.3812 + } 1.3813 + 1.3814 + selProto.setRanges = function(ranges) { 1.3815 + if (implementsControlRange && implementsDocSelection && ranges.length > 1) { 1.3816 + createControlSelection(this, ranges); 1.3817 + } else { 1.3818 + this.removeAllRanges(); 1.3819 + for (var i = 0, len = ranges.length; i < len; ++i) { 1.3820 + this.addRange(ranges[i]); 1.3821 + } 1.3822 + } 1.3823 + }; 1.3824 + } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") && 1.3825 + implementsControlRange && useDocumentSelection) { 1.3826 + 1.3827 + selProto.removeAllRanges = function() { 1.3828 + // Added try/catch as fix for issue #21 1.3829 + try { 1.3830 + this.docSelection.empty(); 1.3831 + 1.3832 + // Check for empty() not working (issue #24) 1.3833 + if (this.docSelection.type != "None") { 1.3834 + // Work around failure to empty a control selection by instead selecting a TextRange and then 1.3835 + // calling empty() 1.3836 + var doc; 1.3837 + if (this.anchorNode) { 1.3838 + doc = getDocument(this.anchorNode); 1.3839 + } else if (this.docSelection.type == CONTROL) { 1.3840 + var controlRange = this.docSelection.createRange(); 1.3841 + if (controlRange.length) { 1.3842 + doc = getDocument( controlRange.item(0) ); 1.3843 + } 1.3844 + } 1.3845 + if (doc) { 1.3846 + var textRange = getBody(doc).createTextRange(); 1.3847 + textRange.select(); 1.3848 + this.docSelection.empty(); 1.3849 + } 1.3850 + } 1.3851 + } catch(ex) {} 1.3852 + updateEmptySelection(this); 1.3853 + }; 1.3854 + 1.3855 + selProto.addRange = function(range) { 1.3856 + if (this.docSelection.type == CONTROL) { 1.3857 + addRangeToControlSelection(this, range); 1.3858 + } else { 1.3859 + api.WrappedTextRange.rangeToTextRange(range).select(); 1.3860 + this._ranges[0] = range; 1.3861 + this.rangeCount = 1; 1.3862 + this.isCollapsed = this._ranges[0].collapsed; 1.3863 + updateAnchorAndFocusFromRange(this, range, false); 1.3864 + } 1.3865 + }; 1.3866 + 1.3867 + selProto.setRanges = function(ranges) { 1.3868 + this.removeAllRanges(); 1.3869 + var rangeCount = ranges.length; 1.3870 + if (rangeCount > 1) { 1.3871 + createControlSelection(this, ranges); 1.3872 + } else if (rangeCount) { 1.3873 + this.addRange(ranges[0]); 1.3874 + } 1.3875 + }; 1.3876 + } else { 1.3877 + module.fail("No means of selecting a Range or TextRange was found"); 1.3878 + return false; 1.3879 + } 1.3880 + 1.3881 + selProto.getRangeAt = function(index) { 1.3882 + if (index < 0 || index >= this.rangeCount) { 1.3883 + throw new DOMException("INDEX_SIZE_ERR"); 1.3884 + } else { 1.3885 + // Clone the range to preserve selection-range independence. See issue 80. 1.3886 + return this._ranges[index].cloneRange(); 1.3887 + } 1.3888 + }; 1.3889 + 1.3890 + var refreshSelection; 1.3891 + 1.3892 + if (useDocumentSelection) { 1.3893 + refreshSelection = function(sel) { 1.3894 + var range; 1.3895 + if (api.isSelectionValid(sel.win)) { 1.3896 + range = sel.docSelection.createRange(); 1.3897 + } else { 1.3898 + range = getBody(sel.win.document).createTextRange(); 1.3899 + range.collapse(true); 1.3900 + } 1.3901 + 1.3902 + if (sel.docSelection.type == CONTROL) { 1.3903 + updateControlSelection(sel); 1.3904 + } else if (isTextRange(range)) { 1.3905 + updateFromTextRange(sel, range); 1.3906 + } else { 1.3907 + updateEmptySelection(sel); 1.3908 + } 1.3909 + }; 1.3910 + } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) { 1.3911 + refreshSelection = function(sel) { 1.3912 + if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { 1.3913 + updateControlSelection(sel); 1.3914 + } else { 1.3915 + sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; 1.3916 + if (sel.rangeCount) { 1.3917 + for (var i = 0, len = sel.rangeCount; i < len; ++i) { 1.3918 + sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); 1.3919 + } 1.3920 + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection)); 1.3921 + sel.isCollapsed = selectionIsCollapsed(sel); 1.3922 + } else { 1.3923 + updateEmptySelection(sel); 1.3924 + } 1.3925 + } 1.3926 + }; 1.3927 + } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) { 1.3928 + refreshSelection = function(sel) { 1.3929 + var range, nativeSel = sel.nativeSelection; 1.3930 + if (nativeSel.anchorNode) { 1.3931 + range = getSelectionRangeAt(nativeSel, 0); 1.3932 + sel._ranges = [range]; 1.3933 + sel.rangeCount = 1; 1.3934 + updateAnchorAndFocusFromNativeSelection(sel); 1.3935 + sel.isCollapsed = selectionIsCollapsed(sel); 1.3936 + } else { 1.3937 + updateEmptySelection(sel); 1.3938 + } 1.3939 + }; 1.3940 + } else { 1.3941 + module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); 1.3942 + return false; 1.3943 + } 1.3944 + 1.3945 + selProto.refresh = function(checkForChanges) { 1.3946 + var oldRanges = checkForChanges ? this._ranges.slice(0) : null; 1.3947 + var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset; 1.3948 + 1.3949 + refreshSelection(this); 1.3950 + if (checkForChanges) { 1.3951 + // Check the range count first 1.3952 + var i = oldRanges.length; 1.3953 + if (i != this._ranges.length) { 1.3954 + return true; 1.3955 + } 1.3956 + 1.3957 + // Now check the direction. Checking the anchor position is the same is enough since we're checking all the 1.3958 + // ranges after this 1.3959 + if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) { 1.3960 + return true; 1.3961 + } 1.3962 + 1.3963 + // Finally, compare each range in turn 1.3964 + while (i--) { 1.3965 + if (!rangesEqual(oldRanges[i], this._ranges[i])) { 1.3966 + return true; 1.3967 + } 1.3968 + } 1.3969 + return false; 1.3970 + } 1.3971 + }; 1.3972 + 1.3973 + // Removal of a single range 1.3974 + var removeRangeManually = function(sel, range) { 1.3975 + var ranges = sel.getAllRanges(); 1.3976 + sel.removeAllRanges(); 1.3977 + for (var i = 0, len = ranges.length; i < len; ++i) { 1.3978 + if (!rangesEqual(range, ranges[i])) { 1.3979 + sel.addRange(ranges[i]); 1.3980 + } 1.3981 + } 1.3982 + if (!sel.rangeCount) { 1.3983 + updateEmptySelection(sel); 1.3984 + } 1.3985 + }; 1.3986 + 1.3987 + if (implementsControlRange && implementsDocSelection) { 1.3988 + selProto.removeRange = function(range) { 1.3989 + if (this.docSelection.type == CONTROL) { 1.3990 + var controlRange = this.docSelection.createRange(); 1.3991 + var rangeElement = getSingleElementFromRange(range); 1.3992 + 1.3993 + // Create a new ControlRange containing all the elements in the selected ControlRange minus the 1.3994 + // element contained by the supplied range 1.3995 + var doc = getDocument(controlRange.item(0)); 1.3996 + var newControlRange = getBody(doc).createControlRange(); 1.3997 + var el, removed = false; 1.3998 + for (var i = 0, len = controlRange.length; i < len; ++i) { 1.3999 + el = controlRange.item(i); 1.4000 + if (el !== rangeElement || removed) { 1.4001 + newControlRange.add(controlRange.item(i)); 1.4002 + } else { 1.4003 + removed = true; 1.4004 + } 1.4005 + } 1.4006 + newControlRange.select(); 1.4007 + 1.4008 + // Update the wrapped selection based on what's now in the native selection 1.4009 + updateControlSelection(this); 1.4010 + } else { 1.4011 + removeRangeManually(this, range); 1.4012 + } 1.4013 + }; 1.4014 + } else { 1.4015 + selProto.removeRange = function(range) { 1.4016 + removeRangeManually(this, range); 1.4017 + }; 1.4018 + } 1.4019 + 1.4020 + // Detecting if a selection is backward 1.4021 + var selectionIsBackward; 1.4022 + if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) { 1.4023 + selectionIsBackward = winSelectionIsBackward; 1.4024 + 1.4025 + selProto.isBackward = function() { 1.4026 + return selectionIsBackward(this); 1.4027 + }; 1.4028 + } else { 1.4029 + selectionIsBackward = selProto.isBackward = function() { 1.4030 + return false; 1.4031 + }; 1.4032 + } 1.4033 + 1.4034 + // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards" 1.4035 + selProto.isBackwards = selProto.isBackward; 1.4036 + 1.4037 + // Selection stringifier 1.4038 + // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation. 1.4039 + // The current spec does not yet define this method. 1.4040 + selProto.toString = function() { 1.4041 + var rangeTexts = []; 1.4042 + for (var i = 0, len = this.rangeCount; i < len; ++i) { 1.4043 + rangeTexts[i] = "" + this._ranges[i]; 1.4044 + } 1.4045 + return rangeTexts.join(""); 1.4046 + }; 1.4047 + 1.4048 + function assertNodeInSameDocument(sel, node) { 1.4049 + if (sel.win.document != getDocument(node)) { 1.4050 + throw new DOMException("WRONG_DOCUMENT_ERR"); 1.4051 + } 1.4052 + } 1.4053 + 1.4054 + // No current browser conforms fully to the spec for this method, so Rangy's own method is always used 1.4055 + selProto.collapse = function(node, offset) { 1.4056 + assertNodeInSameDocument(this, node); 1.4057 + var range = api.createRange(node); 1.4058 + range.collapseToPoint(node, offset); 1.4059 + this.setSingleRange(range); 1.4060 + this.isCollapsed = true; 1.4061 + }; 1.4062 + 1.4063 + selProto.collapseToStart = function() { 1.4064 + if (this.rangeCount) { 1.4065 + var range = this._ranges[0]; 1.4066 + this.collapse(range.startContainer, range.startOffset); 1.4067 + } else { 1.4068 + throw new DOMException("INVALID_STATE_ERR"); 1.4069 + } 1.4070 + }; 1.4071 + 1.4072 + selProto.collapseToEnd = function() { 1.4073 + if (this.rangeCount) { 1.4074 + var range = this._ranges[this.rangeCount - 1]; 1.4075 + this.collapse(range.endContainer, range.endOffset); 1.4076 + } else { 1.4077 + throw new DOMException("INVALID_STATE_ERR"); 1.4078 + } 1.4079 + }; 1.4080 + 1.4081 + // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as 1.4082 + // specified so the native implementation is never used by Rangy. 1.4083 + selProto.selectAllChildren = function(node) { 1.4084 + assertNodeInSameDocument(this, node); 1.4085 + var range = api.createRange(node); 1.4086 + range.selectNodeContents(node); 1.4087 + this.setSingleRange(range); 1.4088 + }; 1.4089 + 1.4090 + selProto.deleteFromDocument = function() { 1.4091 + // Sepcial behaviour required for IE's control selections 1.4092 + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { 1.4093 + var controlRange = this.docSelection.createRange(); 1.4094 + var element; 1.4095 + while (controlRange.length) { 1.4096 + element = controlRange.item(0); 1.4097 + controlRange.remove(element); 1.4098 + dom.removeNode(element); 1.4099 + } 1.4100 + this.refresh(); 1.4101 + } else if (this.rangeCount) { 1.4102 + var ranges = this.getAllRanges(); 1.4103 + if (ranges.length) { 1.4104 + this.removeAllRanges(); 1.4105 + for (var i = 0, len = ranges.length; i < len; ++i) { 1.4106 + ranges[i].deleteContents(); 1.4107 + } 1.4108 + // The spec says nothing about what the selection should contain after calling deleteContents on each 1.4109 + // range. Firefox moves the selection to where the final selected range was, so we emulate that 1.4110 + this.addRange(ranges[len - 1]); 1.4111 + } 1.4112 + } 1.4113 + }; 1.4114 + 1.4115 + // The following are non-standard extensions 1.4116 + selProto.eachRange = function(func, returnValue) { 1.4117 + for (var i = 0, len = this._ranges.length; i < len; ++i) { 1.4118 + if ( func( this.getRangeAt(i) ) ) { 1.4119 + return returnValue; 1.4120 + } 1.4121 + } 1.4122 + }; 1.4123 + 1.4124 + selProto.getAllRanges = function() { 1.4125 + var ranges = []; 1.4126 + this.eachRange(function(range) { 1.4127 + ranges.push(range); 1.4128 + }); 1.4129 + return ranges; 1.4130 + }; 1.4131 + 1.4132 + selProto.setSingleRange = function(range, direction) { 1.4133 + this.removeAllRanges(); 1.4134 + this.addRange(range, direction); 1.4135 + }; 1.4136 + 1.4137 + selProto.callMethodOnEachRange = function(methodName, params) { 1.4138 + var results = []; 1.4139 + this.eachRange( function(range) { 1.4140 + results.push( range[methodName].apply(range, params || []) ); 1.4141 + } ); 1.4142 + return results; 1.4143 + }; 1.4144 + 1.4145 + function createStartOrEndSetter(isStart) { 1.4146 + return function(node, offset) { 1.4147 + var range; 1.4148 + if (this.rangeCount) { 1.4149 + range = this.getRangeAt(0); 1.4150 + range["set" + (isStart ? "Start" : "End")](node, offset); 1.4151 + } else { 1.4152 + range = api.createRange(this.win.document); 1.4153 + range.setStartAndEnd(node, offset); 1.4154 + } 1.4155 + this.setSingleRange(range, this.isBackward()); 1.4156 + }; 1.4157 + } 1.4158 + 1.4159 + selProto.setStart = createStartOrEndSetter(true); 1.4160 + selProto.setEnd = createStartOrEndSetter(false); 1.4161 + 1.4162 + // Add select() method to Range prototype. Any existing selection will be removed. 1.4163 + api.rangePrototype.select = function(direction) { 1.4164 + getSelection( this.getDocument() ).setSingleRange(this, direction); 1.4165 + }; 1.4166 + 1.4167 + selProto.changeEachRange = function(func) { 1.4168 + var ranges = []; 1.4169 + var backward = this.isBackward(); 1.4170 + 1.4171 + this.eachRange(function(range) { 1.4172 + func(range); 1.4173 + ranges.push(range); 1.4174 + }); 1.4175 + 1.4176 + this.removeAllRanges(); 1.4177 + if (backward && ranges.length == 1) { 1.4178 + this.addRange(ranges[0], "backward"); 1.4179 + } else { 1.4180 + this.setRanges(ranges); 1.4181 + } 1.4182 + }; 1.4183 + 1.4184 + selProto.containsNode = function(node, allowPartial) { 1.4185 + return this.eachRange( function(range) { 1.4186 + return range.containsNode(node, allowPartial); 1.4187 + }, true ) || false; 1.4188 + }; 1.4189 + 1.4190 + selProto.getBookmark = function(containerNode) { 1.4191 + return { 1.4192 + backward: this.isBackward(), 1.4193 + rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode]) 1.4194 + }; 1.4195 + }; 1.4196 + 1.4197 + selProto.moveToBookmark = function(bookmark) { 1.4198 + var selRanges = []; 1.4199 + for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) { 1.4200 + range = api.createRange(this.win); 1.4201 + range.moveToBookmark(rangeBookmark); 1.4202 + selRanges.push(range); 1.4203 + } 1.4204 + if (bookmark.backward) { 1.4205 + this.setSingleRange(selRanges[0], "backward"); 1.4206 + } else { 1.4207 + this.setRanges(selRanges); 1.4208 + } 1.4209 + }; 1.4210 + 1.4211 + selProto.saveRanges = function() { 1.4212 + return { 1.4213 + backward: this.isBackward(), 1.4214 + ranges: this.callMethodOnEachRange("cloneRange") 1.4215 + }; 1.4216 + }; 1.4217 + 1.4218 + selProto.restoreRanges = function(selRanges) { 1.4219 + this.removeAllRanges(); 1.4220 + for (var i = 0, range; range = selRanges.ranges[i]; ++i) { 1.4221 + this.addRange(range, (selRanges.backward && i == 0)); 1.4222 + } 1.4223 + }; 1.4224 + 1.4225 + selProto.toHtml = function() { 1.4226 + var rangeHtmls = []; 1.4227 + this.eachRange(function(range) { 1.4228 + rangeHtmls.push( DomRange.toHtml(range) ); 1.4229 + }); 1.4230 + return rangeHtmls.join(""); 1.4231 + }; 1.4232 + 1.4233 + if (features.implementsTextRange) { 1.4234 + selProto.getNativeTextRange = function() { 1.4235 + var sel, textRange; 1.4236 + if ( (sel = this.docSelection) ) { 1.4237 + var range = sel.createRange(); 1.4238 + if (isTextRange(range)) { 1.4239 + return range; 1.4240 + } else { 1.4241 + throw module.createError("getNativeTextRange: selection is a control selection"); 1.4242 + } 1.4243 + } else if (this.rangeCount > 0) { 1.4244 + return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) ); 1.4245 + } else { 1.4246 + throw module.createError("getNativeTextRange: selection contains no range"); 1.4247 + } 1.4248 + }; 1.4249 + } 1.4250 + 1.4251 + function inspect(sel) { 1.4252 + var rangeInspects = []; 1.4253 + var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); 1.4254 + var focus = new DomPosition(sel.focusNode, sel.focusOffset); 1.4255 + var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; 1.4256 + 1.4257 + if (typeof sel.rangeCount != "undefined") { 1.4258 + for (var i = 0, len = sel.rangeCount; i < len; ++i) { 1.4259 + rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); 1.4260 + } 1.4261 + } 1.4262 + return "[" + name + "(Ranges: " + rangeInspects.join(", ") + 1.4263 + ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; 1.4264 + } 1.4265 + 1.4266 + selProto.getName = function() { 1.4267 + return "WrappedSelection"; 1.4268 + }; 1.4269 + 1.4270 + selProto.inspect = function() { 1.4271 + return inspect(this); 1.4272 + }; 1.4273 + 1.4274 + selProto.detach = function() { 1.4275 + actOnCachedSelection(this.win, "delete"); 1.4276 + deleteProperties(this); 1.4277 + }; 1.4278 + 1.4279 + WrappedSelection.detachAll = function() { 1.4280 + actOnCachedSelection(null, "deleteAll"); 1.4281 + }; 1.4282 + 1.4283 + WrappedSelection.inspect = inspect; 1.4284 + WrappedSelection.isDirectionBackward = isDirectionBackward; 1.4285 + 1.4286 + api.Selection = WrappedSelection; 1.4287 + 1.4288 + api.selectionPrototype = selProto; 1.4289 + 1.4290 + api.addShimListener(function(win) { 1.4291 + if (typeof win.getSelection == "undefined") { 1.4292 + win.getSelection = function() { 1.4293 + return getSelection(win); 1.4294 + }; 1.4295 + } 1.4296 + win = null; 1.4297 + }); 1.4298 + }); 1.4299 + 1.4300 + 1.4301 + /*----------------------------------------------------------------------------------------------------------------*/ 1.4302 + 1.4303 + // Wait for document to load before initializing 1.4304 + var docReady = false; 1.4305 + 1.4306 + var loadHandler = function(e) { 1.4307 + if (!docReady) { 1.4308 + docReady = true; 1.4309 + if (!api.initialized && api.config.autoInitialize) { 1.4310 + init(); 1.4311 + } 1.4312 + } 1.4313 + }; 1.4314 + 1.4315 + if (isBrowser) { 1.4316 + // Test whether the document has already been loaded and initialize immediately if so 1.4317 + if (document.readyState == "complete") { 1.4318 + loadHandler(); 1.4319 + } else { 1.4320 + if (isHostMethod(document, "addEventListener")) { 1.4321 + document.addEventListener("DOMContentLoaded", loadHandler, false); 1.4322 + } 1.4323 + 1.4324 + // Add a fallback in case the DOMContentLoaded event isn't supported 1.4325 + addListener(window, "load", loadHandler); 1.4326 + } 1.4327 + } 1.4328 + 1.4329 + rangy = api; 1.4330 +})(); 1.4331 + 1.4332 +/** 1.4333 + * Selection save and restore module for Rangy. 1.4334 + * Saves and restores user selections using marker invisible elements in the DOM. 1.4335 + * 1.4336 + * Part of Rangy, a cross-browser JavaScript range and selection library 1.4337 + * https://github.com/timdown/rangy 1.4338 + * 1.4339 + * Depends on Rangy core. 1.4340 + * 1.4341 + * Copyright 2015, Tim Down 1.4342 + * Licensed under the MIT license. 1.4343 + * Version: 1.3.1-dev 1.4344 + * Build date: 20 May 2015 1.4345 + * 1.4346 +* NOTE: UMD wrapper removed manually for bundling (Oliver Pulges) 1.4347 +*/ 1.4348 +rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { 1.4349 + var dom = api.dom; 1.4350 + var removeNode = dom.removeNode; 1.4351 + var isDirectionBackward = api.Selection.isDirectionBackward; 1.4352 + var markerTextChar = "\ufeff"; 1.4353 + 1.4354 + function gEBI(id, doc) { 1.4355 + return (doc || document).getElementById(id); 1.4356 + } 1.4357 + 1.4358 + function insertRangeBoundaryMarker(range, atStart) { 1.4359 + var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2); 1.4360 + var markerEl; 1.4361 + var doc = dom.getDocument(range.startContainer); 1.4362 + 1.4363 + // Clone the Range and collapse to the appropriate boundary point 1.4364 + var boundaryRange = range.cloneRange(); 1.4365 + boundaryRange.collapse(atStart); 1.4366 + 1.4367 + // Create the marker element containing a single invisible character using DOM methods and insert it 1.4368 + markerEl = doc.createElement("span"); 1.4369 + markerEl.id = markerId; 1.4370 + markerEl.style.lineHeight = "0"; 1.4371 + markerEl.style.display = "none"; 1.4372 + markerEl.className = "rangySelectionBoundary"; 1.4373 + markerEl.appendChild(doc.createTextNode(markerTextChar)); 1.4374 + 1.4375 + boundaryRange.insertNode(markerEl); 1.4376 + return markerEl; 1.4377 + } 1.4378 + 1.4379 + function setRangeBoundary(doc, range, markerId, atStart) { 1.4380 + var markerEl = gEBI(markerId, doc); 1.4381 + if (markerEl) { 1.4382 + range[atStart ? "setStartBefore" : "setEndBefore"](markerEl); 1.4383 + removeNode(markerEl); 1.4384 + } else { 1.4385 + module.warn("Marker element has been removed. Cannot restore selection."); 1.4386 + } 1.4387 + } 1.4388 + 1.4389 + function compareRanges(r1, r2) { 1.4390 + return r2.compareBoundaryPoints(r1.START_TO_START, r1); 1.4391 + } 1.4392 + 1.4393 + function saveRange(range, direction) { 1.4394 + var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString(); 1.4395 + var backward = isDirectionBackward(direction); 1.4396 + 1.4397 + if (range.collapsed) { 1.4398 + endEl = insertRangeBoundaryMarker(range, false); 1.4399 + return { 1.4400 + document: doc, 1.4401 + markerId: endEl.id, 1.4402 + collapsed: true 1.4403 + }; 1.4404 + } else { 1.4405 + endEl = insertRangeBoundaryMarker(range, false); 1.4406 + startEl = insertRangeBoundaryMarker(range, true); 1.4407 + 1.4408 + return { 1.4409 + document: doc, 1.4410 + startMarkerId: startEl.id, 1.4411 + endMarkerId: endEl.id, 1.4412 + collapsed: false, 1.4413 + backward: backward, 1.4414 + toString: function() { 1.4415 + return "original text: '" + text + "', new text: '" + range.toString() + "'"; 1.4416 + } 1.4417 + }; 1.4418 + } 1.4419 + } 1.4420 + 1.4421 + function restoreRange(rangeInfo, normalize) { 1.4422 + var doc = rangeInfo.document; 1.4423 + if (typeof normalize == "undefined") { 1.4424 + normalize = true; 1.4425 + } 1.4426 + var range = api.createRange(doc); 1.4427 + if (rangeInfo.collapsed) { 1.4428 + var markerEl = gEBI(rangeInfo.markerId, doc); 1.4429 + if (markerEl) { 1.4430 + markerEl.style.display = "inline"; 1.4431 + var previousNode = markerEl.previousSibling; 1.4432 + 1.4433 + // Workaround for issue 17 1.4434 + if (previousNode && previousNode.nodeType == 3) { 1.4435 + removeNode(markerEl); 1.4436 + range.collapseToPoint(previousNode, previousNode.length); 1.4437 + } else { 1.4438 + range.collapseBefore(markerEl); 1.4439 + removeNode(markerEl); 1.4440 + } 1.4441 + } else { 1.4442 + module.warn("Marker element has been removed. Cannot restore selection."); 1.4443 + } 1.4444 + } else { 1.4445 + setRangeBoundary(doc, range, rangeInfo.startMarkerId, true); 1.4446 + setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); 1.4447 + } 1.4448 + 1.4449 + if (normalize) { 1.4450 + range.normalizeBoundaries(); 1.4451 + } 1.4452 + 1.4453 + return range; 1.4454 + } 1.4455 + 1.4456 + function saveRanges(ranges, direction) { 1.4457 + var rangeInfos = [], range, doc; 1.4458 + var backward = isDirectionBackward(direction); 1.4459 + 1.4460 + // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched 1.4461 + ranges = ranges.slice(0); 1.4462 + ranges.sort(compareRanges); 1.4463 + 1.4464 + for (var i = 0, len = ranges.length; i < len; ++i) { 1.4465 + rangeInfos[i] = saveRange(ranges[i], backward); 1.4466 + } 1.4467 + 1.4468 + // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie 1.4469 + // between its markers 1.4470 + for (i = len - 1; i >= 0; --i) { 1.4471 + range = ranges[i]; 1.4472 + doc = api.DomRange.getRangeDocument(range); 1.4473 + if (range.collapsed) { 1.4474 + range.collapseAfter(gEBI(rangeInfos[i].markerId, doc)); 1.4475 + } else { 1.4476 + range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); 1.4477 + range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); 1.4478 + } 1.4479 + } 1.4480 + 1.4481 + return rangeInfos; 1.4482 + } 1.4483 + 1.4484 + function saveSelection(win) { 1.4485 + if (!api.isSelectionValid(win)) { 1.4486 + module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."); 1.4487 + return null; 1.4488 + } 1.4489 + var sel = api.getSelection(win); 1.4490 + var ranges = sel.getAllRanges(); 1.4491 + var backward = (ranges.length == 1 && sel.isBackward()); 1.4492 + 1.4493 + var rangeInfos = saveRanges(ranges, backward); 1.4494 + 1.4495 + // Ensure current selection is unaffected 1.4496 + if (backward) { 1.4497 + sel.setSingleRange(ranges[0], backward); 1.4498 + } else { 1.4499 + sel.setRanges(ranges); 1.4500 + } 1.4501 + 1.4502 + return { 1.4503 + win: win, 1.4504 + rangeInfos: rangeInfos, 1.4505 + restored: false 1.4506 + }; 1.4507 + } 1.4508 + 1.4509 + function restoreRanges(rangeInfos) { 1.4510 + var ranges = []; 1.4511 + 1.4512 + // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid 1.4513 + // normalization affecting previously restored ranges. 1.4514 + var rangeCount = rangeInfos.length; 1.4515 + 1.4516 + for (var i = rangeCount - 1; i >= 0; i--) { 1.4517 + ranges[i] = restoreRange(rangeInfos[i], true); 1.4518 + } 1.4519 + 1.4520 + return ranges; 1.4521 + } 1.4522 + 1.4523 + function restoreSelection(savedSelection, preserveDirection) { 1.4524 + if (!savedSelection.restored) { 1.4525 + var rangeInfos = savedSelection.rangeInfos; 1.4526 + var sel = api.getSelection(savedSelection.win); 1.4527 + var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length; 1.4528 + 1.4529 + if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) { 1.4530 + sel.removeAllRanges(); 1.4531 + sel.addRange(ranges[0], true); 1.4532 + } else { 1.4533 + sel.setRanges(ranges); 1.4534 + } 1.4535 + 1.4536 + savedSelection.restored = true; 1.4537 + } 1.4538 + } 1.4539 + 1.4540 + function removeMarkerElement(doc, markerId) { 1.4541 + var markerEl = gEBI(markerId, doc); 1.4542 + if (markerEl) { 1.4543 + removeNode(markerEl); 1.4544 + } 1.4545 + } 1.4546 + 1.4547 + function removeMarkers(savedSelection) { 1.4548 + var rangeInfos = savedSelection.rangeInfos; 1.4549 + for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) { 1.4550 + rangeInfo = rangeInfos[i]; 1.4551 + if (rangeInfo.collapsed) { 1.4552 + removeMarkerElement(savedSelection.doc, rangeInfo.markerId); 1.4553 + } else { 1.4554 + removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId); 1.4555 + removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId); 1.4556 + } 1.4557 + } 1.4558 + } 1.4559 + 1.4560 + api.util.extend(api, { 1.4561 + saveRange: saveRange, 1.4562 + restoreRange: restoreRange, 1.4563 + saveRanges: saveRanges, 1.4564 + restoreRanges: restoreRanges, 1.4565 + saveSelection: saveSelection, 1.4566 + restoreSelection: restoreSelection, 1.4567 + removeMarkerElement: removeMarkerElement, 1.4568 + removeMarkers: removeMarkers 1.4569 + }); 1.4570 +}); 1.4571 + 1.4572 +/** 1.4573 + * Text range module for Rangy. 1.4574 + * Text-based manipulation and searching of ranges and selections. 1.4575 + * 1.4576 + * Features 1.4577 + * 1.4578 + * - Ability to move range boundaries by character or word offsets 1.4579 + * - Customizable word tokenizer 1.4580 + * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties 1.4581 + * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case 1.4582 + * sensitivity 1.4583 + * - Selection and range save/restore as text offsets within a node 1.4584 + * - Methods to return visible text within a range or selection 1.4585 + * - innerText method for elements 1.4586 + * 1.4587 + * References 1.4588 + * 1.4589 + * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145 1.4590 + * http://aryeh.name/spec/innertext/innertext.html 1.4591 + * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html 1.4592 + * 1.4593 + * Part of Rangy, a cross-browser JavaScript range and selection library 1.4594 + * https://github.com/timdown/rangy 1.4595 + * 1.4596 + * Depends on Rangy core. 1.4597 + * 1.4598 + * Copyright 2015, Tim Down 1.4599 + * Licensed under the MIT license. 1.4600 + * Version: 1.3.1-dev 1.4601 + * Build date: 20 May 2015 1.4602 + */ 1.4603 + 1.4604 +/** 1.4605 + * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers. 1.4606 + * 1.4607 + * First, a <br>: this is relatively simple. For the following HTML: 1.4608 + * 1.4609 + * 1 <br>2 1.4610 + * 1.4611 + * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a 1.4612 + * textarea, the space is present) and allow the caret to be placed after it. 1.4613 + * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it. 1.4614 + * - Opera does not render the space but has two separate caret positions on either side of the space (left and right 1.4615 + * arrow keys show this) and includes the space in the selection. 1.4616 + * 1.4617 + * The other case is the line break or breaks implied by block elements. For the following HTML: 1.4618 + * 1.4619 + * <p>1 </p><p>2<p> 1.4620 + * 1.4621 + * - WebKit does not acknowledge the space in any way 1.4622 + * - Firefox, IE and Opera as per <br> 1.4623 + * 1.4624 + * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML: 1.4625 + * 1.4626 + * <p style="white-space: pre-line">1 1.4627 + * 2</p> 1.4628 + * 1.4629 + * - Firefox and WebKit include the space in caret positions 1.4630 + * - IE does not support pre-line up to and including version 9 1.4631 + * - Opera ignores the space 1.4632 + * - Trailing space only renders if there is a non-collapsed character in the line 1.4633 + * 1.4634 + * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be 1.4635 + * feature-tested 1.4636 + * 1.4637 + * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges) 1.4638 +*/ 1.4639 +rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) { 1.4640 + var UNDEF = "undefined"; 1.4641 + var CHARACTER = "character", WORD = "word"; 1.4642 + var dom = api.dom, util = api.util; 1.4643 + var extend = util.extend; 1.4644 + var createOptions = util.createOptions; 1.4645 + var getBody = dom.getBody; 1.4646 + 1.4647 + 1.4648 + var spacesRegex = /^[ \t\f\r\n]+$/; 1.4649 + var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/; 1.4650 + var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/; 1.4651 + var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/; 1.4652 + var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/; 1.4653 + 1.4654 + var defaultLanguage = "en"; 1.4655 + 1.4656 + var isDirectionBackward = api.Selection.isDirectionBackward; 1.4657 + 1.4658 + // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit, 1.4659 + // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed. 1.4660 + var trailingSpaceInBlockCollapses = false; 1.4661 + var trailingSpaceBeforeBrCollapses = false; 1.4662 + var trailingSpaceBeforeBlockCollapses = false; 1.4663 + var trailingSpaceBeforeLineBreakInPreLineCollapses = true; 1.4664 + 1.4665 + (function() { 1.4666 + var el = dom.createTestElement(document, "<p>1 </p><p></p>", true); 1.4667 + var p = el.firstChild; 1.4668 + var sel = api.getSelection(); 1.4669 + sel.collapse(p.lastChild, 2); 1.4670 + sel.setStart(p.firstChild, 0); 1.4671 + trailingSpaceInBlockCollapses = ("" + sel).length == 1; 1.4672 + 1.4673 + el.innerHTML = "1 <br />"; 1.4674 + sel.collapse(el, 2); 1.4675 + sel.setStart(el.firstChild, 0); 1.4676 + trailingSpaceBeforeBrCollapses = ("" + sel).length == 1; 1.4677 + 1.4678 + el.innerHTML = "1 <p>1</p>"; 1.4679 + sel.collapse(el, 2); 1.4680 + sel.setStart(el.firstChild, 0); 1.4681 + trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1; 1.4682 + 1.4683 + dom.removeNode(el); 1.4684 + sel.removeAllRanges(); 1.4685 + })(); 1.4686 + 1.4687 + /*----------------------------------------------------------------------------------------------------------------*/ 1.4688 + 1.4689 + // This function must create word and non-word tokens for the whole of the text supplied to it 1.4690 + function defaultTokenizer(chars, wordOptions) { 1.4691 + var word = chars.join(""), result, tokenRanges = []; 1.4692 + 1.4693 + function createTokenRange(start, end, isWord) { 1.4694 + tokenRanges.push( { start: start, end: end, isWord: isWord } ); 1.4695 + } 1.4696 + 1.4697 + // Match words and mark characters 1.4698 + var lastWordEnd = 0, wordStart, wordEnd; 1.4699 + while ( (result = wordOptions.wordRegex.exec(word)) ) { 1.4700 + wordStart = result.index; 1.4701 + wordEnd = wordStart + result[0].length; 1.4702 + 1.4703 + // Create token for non-word characters preceding this word 1.4704 + if (wordStart > lastWordEnd) { 1.4705 + createTokenRange(lastWordEnd, wordStart, false); 1.4706 + } 1.4707 + 1.4708 + // Get trailing space characters for word 1.4709 + if (wordOptions.includeTrailingSpace) { 1.4710 + while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) { 1.4711 + ++wordEnd; 1.4712 + } 1.4713 + } 1.4714 + createTokenRange(wordStart, wordEnd, true); 1.4715 + lastWordEnd = wordEnd; 1.4716 + } 1.4717 + 1.4718 + // Create token for trailing non-word characters, if any exist 1.4719 + if (lastWordEnd < chars.length) { 1.4720 + createTokenRange(lastWordEnd, chars.length, false); 1.4721 + } 1.4722 + 1.4723 + return tokenRanges; 1.4724 + } 1.4725 + 1.4726 + function convertCharRangeToToken(chars, tokenRange) { 1.4727 + var tokenChars = chars.slice(tokenRange.start, tokenRange.end); 1.4728 + var token = { 1.4729 + isWord: tokenRange.isWord, 1.4730 + chars: tokenChars, 1.4731 + toString: function() { 1.4732 + return tokenChars.join(""); 1.4733 + } 1.4734 + }; 1.4735 + for (var i = 0, len = tokenChars.length; i < len; ++i) { 1.4736 + tokenChars[i].token = token; 1.4737 + } 1.4738 + return token; 1.4739 + } 1.4740 + 1.4741 + function tokenize(chars, wordOptions, tokenizer) { 1.4742 + var tokenRanges = tokenizer(chars, wordOptions); 1.4743 + var tokens = []; 1.4744 + for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) { 1.4745 + tokens.push( convertCharRangeToToken(chars, tokenRange) ); 1.4746 + } 1.4747 + return tokens; 1.4748 + } 1.4749 + 1.4750 + var defaultCharacterOptions = { 1.4751 + includeBlockContentTrailingSpace: true, 1.4752 + includeSpaceBeforeBr: true, 1.4753 + includeSpaceBeforeBlock: true, 1.4754 + includePreLineTrailingSpace: true, 1.4755 + ignoreCharacters: "" 1.4756 + }; 1.4757 + 1.4758 + function normalizeIgnoredCharacters(ignoredCharacters) { 1.4759 + // Check if character is ignored 1.4760 + var ignoredChars = ignoredCharacters || ""; 1.4761 + 1.4762 + // Normalize ignored characters into a string consisting of characters in ascending order of character code 1.4763 + var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars; 1.4764 + ignoredCharsArray.sort(function(char1, char2) { 1.4765 + return char1.charCodeAt(0) - char2.charCodeAt(0); 1.4766 + }); 1.4767 + 1.4768 + /// Convert back to a string and remove duplicates 1.4769 + return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1"); 1.4770 + } 1.4771 + 1.4772 + var defaultCaretCharacterOptions = { 1.4773 + includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses, 1.4774 + includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses, 1.4775 + includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses, 1.4776 + includePreLineTrailingSpace: true 1.4777 + }; 1.4778 + 1.4779 + var defaultWordOptions = { 1.4780 + "en": { 1.4781 + wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi, 1.4782 + includeTrailingSpace: false, 1.4783 + tokenizer: defaultTokenizer 1.4784 + } 1.4785 + }; 1.4786 + 1.4787 + var defaultFindOptions = { 1.4788 + caseSensitive: false, 1.4789 + withinRange: null, 1.4790 + wholeWordsOnly: false, 1.4791 + wrap: false, 1.4792 + direction: "forward", 1.4793 + wordOptions: null, 1.4794 + characterOptions: null 1.4795 + }; 1.4796 + 1.4797 + var defaultMoveOptions = { 1.4798 + wordOptions: null, 1.4799 + characterOptions: null 1.4800 + }; 1.4801 + 1.4802 + var defaultExpandOptions = { 1.4803 + wordOptions: null, 1.4804 + characterOptions: null, 1.4805 + trim: false, 1.4806 + trimStart: true, 1.4807 + trimEnd: true 1.4808 + }; 1.4809 + 1.4810 + var defaultWordIteratorOptions = { 1.4811 + wordOptions: null, 1.4812 + characterOptions: null, 1.4813 + direction: "forward" 1.4814 + }; 1.4815 + 1.4816 + function createWordOptions(options) { 1.4817 + var lang, defaults; 1.4818 + if (!options) { 1.4819 + return defaultWordOptions[defaultLanguage]; 1.4820 + } else { 1.4821 + lang = options.language || defaultLanguage; 1.4822 + defaults = {}; 1.4823 + extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]); 1.4824 + extend(defaults, options); 1.4825 + return defaults; 1.4826 + } 1.4827 + } 1.4828 + 1.4829 + function createNestedOptions(optionsParam, defaults) { 1.4830 + var options = createOptions(optionsParam, defaults); 1.4831 + if (defaults.hasOwnProperty("wordOptions")) { 1.4832 + options.wordOptions = createWordOptions(options.wordOptions); 1.4833 + } 1.4834 + if (defaults.hasOwnProperty("characterOptions")) { 1.4835 + options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions); 1.4836 + } 1.4837 + return options; 1.4838 + } 1.4839 + 1.4840 + /*----------------------------------------------------------------------------------------------------------------*/ 1.4841 + 1.4842 + /* DOM utility functions */ 1.4843 + var getComputedStyleProperty = dom.getComputedStyleProperty; 1.4844 + 1.4845 + // Create cachable versions of DOM functions 1.4846 + 1.4847 + // Test for old IE's incorrect display properties 1.4848 + var tableCssDisplayBlock; 1.4849 + (function() { 1.4850 + var table = document.createElement("table"); 1.4851 + var body = getBody(document); 1.4852 + body.appendChild(table); 1.4853 + tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block"); 1.4854 + body.removeChild(table); 1.4855 + })(); 1.4856 + 1.4857 + var defaultDisplayValueForTag = { 1.4858 + table: "table", 1.4859 + caption: "table-caption", 1.4860 + colgroup: "table-column-group", 1.4861 + col: "table-column", 1.4862 + thead: "table-header-group", 1.4863 + tbody: "table-row-group", 1.4864 + tfoot: "table-footer-group", 1.4865 + tr: "table-row", 1.4866 + td: "table-cell", 1.4867 + th: "table-cell" 1.4868 + }; 1.4869 + 1.4870 + // Corrects IE's "block" value for table-related elements 1.4871 + function getComputedDisplay(el, win) { 1.4872 + var display = getComputedStyleProperty(el, "display", win); 1.4873 + var tagName = el.tagName.toLowerCase(); 1.4874 + return (display == "block" && 1.4875 + tableCssDisplayBlock && 1.4876 + defaultDisplayValueForTag.hasOwnProperty(tagName)) ? 1.4877 + defaultDisplayValueForTag[tagName] : display; 1.4878 + } 1.4879 + 1.4880 + function isHidden(node) { 1.4881 + var ancestors = getAncestorsAndSelf(node); 1.4882 + for (var i = 0, len = ancestors.length; i < len; ++i) { 1.4883 + if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") { 1.4884 + return true; 1.4885 + } 1.4886 + } 1.4887 + 1.4888 + return false; 1.4889 + } 1.4890 + 1.4891 + function isVisibilityHiddenTextNode(textNode) { 1.4892 + var el; 1.4893 + return textNode.nodeType == 3 && 1.4894 + (el = textNode.parentNode) && 1.4895 + getComputedStyleProperty(el, "visibility") == "hidden"; 1.4896 + } 1.4897 + 1.4898 + /*----------------------------------------------------------------------------------------------------------------*/ 1.4899 + 1.4900 + 1.4901 + // "A block node is either an Element whose "display" property does not have 1.4902 + // resolved value "inline" or "inline-block" or "inline-table" or "none", or a 1.4903 + // Document, or a DocumentFragment." 1.4904 + function isBlockNode(node) { 1.4905 + return node && 1.4906 + ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) || 1.4907 + node.nodeType == 9 || node.nodeType == 11); 1.4908 + } 1.4909 + 1.4910 + function getLastDescendantOrSelf(node) { 1.4911 + var lastChild = node.lastChild; 1.4912 + return lastChild ? getLastDescendantOrSelf(lastChild) : node; 1.4913 + } 1.4914 + 1.4915 + function containsPositions(node) { 1.4916 + return dom.isCharacterDataNode(node) || 1.4917 + !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName); 1.4918 + } 1.4919 + 1.4920 + function getAncestors(node) { 1.4921 + var ancestors = []; 1.4922 + while (node.parentNode) { 1.4923 + ancestors.unshift(node.parentNode); 1.4924 + node = node.parentNode; 1.4925 + } 1.4926 + return ancestors; 1.4927 + } 1.4928 + 1.4929 + function getAncestorsAndSelf(node) { 1.4930 + return getAncestors(node).concat([node]); 1.4931 + } 1.4932 + 1.4933 + function nextNodeDescendants(node) { 1.4934 + while (node && !node.nextSibling) { 1.4935 + node = node.parentNode; 1.4936 + } 1.4937 + if (!node) { 1.4938 + return null; 1.4939 + } 1.4940 + return node.nextSibling; 1.4941 + } 1.4942 + 1.4943 + function nextNode(node, excludeChildren) { 1.4944 + if (!excludeChildren && node.hasChildNodes()) { 1.4945 + return node.firstChild; 1.4946 + } 1.4947 + return nextNodeDescendants(node); 1.4948 + } 1.4949 + 1.4950 + function previousNode(node) { 1.4951 + var previous = node.previousSibling; 1.4952 + if (previous) { 1.4953 + node = previous; 1.4954 + while (node.hasChildNodes()) { 1.4955 + node = node.lastChild; 1.4956 + } 1.4957 + return node; 1.4958 + } 1.4959 + var parent = node.parentNode; 1.4960 + if (parent && parent.nodeType == 1) { 1.4961 + return parent; 1.4962 + } 1.4963 + return null; 1.4964 + } 1.4965 + 1.4966 + // Adpated from Aryeh's code. 1.4967 + // "A whitespace node is either a Text node whose data is the empty string; or 1.4968 + // a Text node whose data consists only of one or more tabs (0x0009), line 1.4969 + // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose 1.4970 + // parent is an Element whose resolved value for "white-space" is "normal" or 1.4971 + // "nowrap"; or a Text node whose data consists only of one or more tabs 1.4972 + // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 1.4973 + // parent is an Element whose resolved value for "white-space" is "pre-line"." 1.4974 + function isWhitespaceNode(node) { 1.4975 + if (!node || node.nodeType != 3) { 1.4976 + return false; 1.4977 + } 1.4978 + var text = node.data; 1.4979 + if (text === "") { 1.4980 + return true; 1.4981 + } 1.4982 + var parent = node.parentNode; 1.4983 + if (!parent || parent.nodeType != 1) { 1.4984 + return false; 1.4985 + } 1.4986 + var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 1.4987 + 1.4988 + return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) || 1.4989 + (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line"); 1.4990 + } 1.4991 + 1.4992 + // Adpated from Aryeh's code. 1.4993 + // "node is a collapsed whitespace node if the following algorithm returns 1.4994 + // true:" 1.4995 + function isCollapsedWhitespaceNode(node) { 1.4996 + // "If node's data is the empty string, return true." 1.4997 + if (node.data === "") { 1.4998 + return true; 1.4999 + } 1.5000 + 1.5001 + // "If node is not a whitespace node, return false." 1.5002 + if (!isWhitespaceNode(node)) { 1.5003 + return false; 1.5004 + } 1.5005 + 1.5006 + // "Let ancestor be node's parent." 1.5007 + var ancestor = node.parentNode; 1.5008 + 1.5009 + // "If ancestor is null, return true." 1.5010 + if (!ancestor) { 1.5011 + return true; 1.5012 + } 1.5013 + 1.5014 + // "If the "display" property of some ancestor of node has resolved value "none", return true." 1.5015 + if (isHidden(node)) { 1.5016 + return true; 1.5017 + } 1.5018 + 1.5019 + return false; 1.5020 + } 1.5021 + 1.5022 + function isCollapsedNode(node) { 1.5023 + var type = node.nodeType; 1.5024 + return type == 7 /* PROCESSING_INSTRUCTION */ || 1.5025 + type == 8 /* COMMENT */ || 1.5026 + isHidden(node) || 1.5027 + /^(script|style)$/i.test(node.nodeName) || 1.5028 + isVisibilityHiddenTextNode(node) || 1.5029 + isCollapsedWhitespaceNode(node); 1.5030 + } 1.5031 + 1.5032 + function isIgnoredNode(node, win) { 1.5033 + var type = node.nodeType; 1.5034 + return type == 7 /* PROCESSING_INSTRUCTION */ || 1.5035 + type == 8 /* COMMENT */ || 1.5036 + (type == 1 && getComputedDisplay(node, win) == "none"); 1.5037 + } 1.5038 + 1.5039 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5040 + 1.5041 + // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down 1.5042 + 1.5043 + function Cache() { 1.5044 + this.store = {}; 1.5045 + } 1.5046 + 1.5047 + Cache.prototype = { 1.5048 + get: function(key) { 1.5049 + return this.store.hasOwnProperty(key) ? this.store[key] : null; 1.5050 + }, 1.5051 + 1.5052 + set: function(key, value) { 1.5053 + return this.store[key] = value; 1.5054 + } 1.5055 + }; 1.5056 + 1.5057 + var cachedCount = 0, uncachedCount = 0; 1.5058 + 1.5059 + function createCachingGetter(methodName, func, objProperty) { 1.5060 + return function(args) { 1.5061 + var cache = this.cache; 1.5062 + if (cache.hasOwnProperty(methodName)) { 1.5063 + cachedCount++; 1.5064 + return cache[methodName]; 1.5065 + } else { 1.5066 + uncachedCount++; 1.5067 + var value = func.call(this, objProperty ? this[objProperty] : this, args); 1.5068 + cache[methodName] = value; 1.5069 + return value; 1.5070 + } 1.5071 + }; 1.5072 + } 1.5073 + 1.5074 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5075 + 1.5076 + function NodeWrapper(node, session) { 1.5077 + this.node = node; 1.5078 + this.session = session; 1.5079 + this.cache = new Cache(); 1.5080 + this.positions = new Cache(); 1.5081 + } 1.5082 + 1.5083 + var nodeProto = { 1.5084 + getPosition: function(offset) { 1.5085 + var positions = this.positions; 1.5086 + return positions.get(offset) || positions.set(offset, new Position(this, offset)); 1.5087 + }, 1.5088 + 1.5089 + toString: function() { 1.5090 + return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]"; 1.5091 + } 1.5092 + }; 1.5093 + 1.5094 + NodeWrapper.prototype = nodeProto; 1.5095 + 1.5096 + var EMPTY = "EMPTY", 1.5097 + NON_SPACE = "NON_SPACE", 1.5098 + UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE", 1.5099 + COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE", 1.5100 + TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK", 1.5101 + TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK", 1.5102 + TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR", 1.5103 + PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK", 1.5104 + TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR", 1.5105 + INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR"; 1.5106 + 1.5107 + extend(nodeProto, { 1.5108 + isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"), 1.5109 + getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"), 1.5110 + getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"), 1.5111 + containsPositions: createCachingGetter("containsPositions", containsPositions, "node"), 1.5112 + isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"), 1.5113 + isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"), 1.5114 + getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"), 1.5115 + isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"), 1.5116 + isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"), 1.5117 + next: createCachingGetter("nextPos", nextNode, "node"), 1.5118 + previous: createCachingGetter("previous", previousNode, "node"), 1.5119 + 1.5120 + getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) { 1.5121 + var spaceRegex = null, collapseSpaces = false; 1.5122 + var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace"); 1.5123 + var preLine = (cssWhitespace == "pre-line"); 1.5124 + if (preLine) { 1.5125 + spaceRegex = spacesMinusLineBreaksRegex; 1.5126 + collapseSpaces = true; 1.5127 + } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") { 1.5128 + spaceRegex = spacesRegex; 1.5129 + collapseSpaces = true; 1.5130 + } 1.5131 + 1.5132 + return { 1.5133 + node: textNode, 1.5134 + text: textNode.data, 1.5135 + spaceRegex: spaceRegex, 1.5136 + collapseSpaces: collapseSpaces, 1.5137 + preLine: preLine 1.5138 + }; 1.5139 + }, "node"), 1.5140 + 1.5141 + hasInnerText: createCachingGetter("hasInnerText", function(el, backward) { 1.5142 + var session = this.session; 1.5143 + var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1); 1.5144 + var firstPosInEl = session.getPosition(el, 0); 1.5145 + 1.5146 + var pos = backward ? posAfterEl : firstPosInEl; 1.5147 + var endPos = backward ? firstPosInEl : posAfterEl; 1.5148 + 1.5149 + /* 1.5150 + <body><p>X </p><p>Y</p></body> 1.5151 + 1.5152 + Positions: 1.5153 + 1.5154 + body:0:"" 1.5155 + p:0:"" 1.5156 + text:0:"" 1.5157 + text:1:"X" 1.5158 + text:2:TRAILING_SPACE_IN_BLOCK 1.5159 + text:3:COLLAPSED_SPACE 1.5160 + p:1:"" 1.5161 + body:1:"\n" 1.5162 + p:0:"" 1.5163 + text:0:"" 1.5164 + text:1:"Y" 1.5165 + 1.5166 + A character is a TRAILING_SPACE_IN_BLOCK iff: 1.5167 + 1.5168 + - There is no uncollapsed character after it within the visible containing block element 1.5169 + 1.5170 + A character is a TRAILING_SPACE_BEFORE_BR iff: 1.5171 + 1.5172 + - There is no uncollapsed character after it preceding a <br> element 1.5173 + 1.5174 + An element has inner text iff 1.5175 + 1.5176 + - It is not hidden 1.5177 + - It contains an uncollapsed character 1.5178 + 1.5179 + All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render. 1.5180 + */ 1.5181 + 1.5182 + while (pos !== endPos) { 1.5183 + pos.prepopulateChar(); 1.5184 + if (pos.isDefinitelyNonEmpty()) { 1.5185 + return true; 1.5186 + } 1.5187 + pos = backward ? pos.previousVisible() : pos.nextVisible(); 1.5188 + } 1.5189 + 1.5190 + return false; 1.5191 + }, "node"), 1.5192 + 1.5193 + isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) { 1.5194 + // Ensure that a block element containing a <br> is considered to have inner text 1.5195 + var brs = el.getElementsByTagName("br"); 1.5196 + for (var i = 0, len = brs.length; i < len; ++i) { 1.5197 + if (!isCollapsedNode(brs[i])) { 1.5198 + return true; 1.5199 + } 1.5200 + } 1.5201 + return this.hasInnerText(); 1.5202 + }, "node"), 1.5203 + 1.5204 + getTrailingSpace: createCachingGetter("trailingSpace", function(el) { 1.5205 + if (el.tagName.toLowerCase() == "br") { 1.5206 + return ""; 1.5207 + } else { 1.5208 + switch (this.getComputedDisplay()) { 1.5209 + case "inline": 1.5210 + var child = el.lastChild; 1.5211 + while (child) { 1.5212 + if (!isIgnoredNode(child)) { 1.5213 + return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : ""; 1.5214 + } 1.5215 + child = child.previousSibling; 1.5216 + } 1.5217 + break; 1.5218 + case "inline-block": 1.5219 + case "inline-table": 1.5220 + case "none": 1.5221 + case "table-column": 1.5222 + case "table-column-group": 1.5223 + break; 1.5224 + case "table-cell": 1.5225 + return "\t"; 1.5226 + default: 1.5227 + return this.isRenderedBlock(true) ? "\n" : ""; 1.5228 + } 1.5229 + } 1.5230 + return ""; 1.5231 + }, "node"), 1.5232 + 1.5233 + getLeadingSpace: createCachingGetter("leadingSpace", function(el) { 1.5234 + switch (this.getComputedDisplay()) { 1.5235 + case "inline": 1.5236 + case "inline-block": 1.5237 + case "inline-table": 1.5238 + case "none": 1.5239 + case "table-column": 1.5240 + case "table-column-group": 1.5241 + case "table-cell": 1.5242 + break; 1.5243 + default: 1.5244 + return this.isRenderedBlock(false) ? "\n" : ""; 1.5245 + } 1.5246 + return ""; 1.5247 + }, "node") 1.5248 + }); 1.5249 + 1.5250 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5251 + 1.5252 + function Position(nodeWrapper, offset) { 1.5253 + this.offset = offset; 1.5254 + this.nodeWrapper = nodeWrapper; 1.5255 + this.node = nodeWrapper.node; 1.5256 + this.session = nodeWrapper.session; 1.5257 + this.cache = new Cache(); 1.5258 + } 1.5259 + 1.5260 + function inspectPosition() { 1.5261 + return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]"; 1.5262 + } 1.5263 + 1.5264 + var positionProto = { 1.5265 + character: "", 1.5266 + characterType: EMPTY, 1.5267 + isBr: false, 1.5268 + 1.5269 + /* 1.5270 + This method: 1.5271 + - Fully populates positions that have characters that can be determined independently of any other characters. 1.5272 + - Populates most types of space positions with a provisional character. The character is finalized later. 1.5273 + */ 1.5274 + prepopulateChar: function() { 1.5275 + var pos = this; 1.5276 + if (!pos.prepopulatedChar) { 1.5277 + var node = pos.node, offset = pos.offset; 1.5278 + var visibleChar = "", charType = EMPTY; 1.5279 + var finalizedChar = false; 1.5280 + if (offset > 0) { 1.5281 + if (node.nodeType == 3) { 1.5282 + var text = node.data; 1.5283 + var textChar = text.charAt(offset - 1); 1.5284 + 1.5285 + var nodeInfo = pos.nodeWrapper.getTextNodeInfo(); 1.5286 + var spaceRegex = nodeInfo.spaceRegex; 1.5287 + if (nodeInfo.collapseSpaces) { 1.5288 + if (spaceRegex.test(textChar)) { 1.5289 + // "If the character at position is from set, append a single space (U+0020) to newdata and advance 1.5290 + // position until the character at position is not from set." 1.5291 + 1.5292 + // We also need to check for the case where we're in a pre-line and we have a space preceding a 1.5293 + // line break, because such spaces are collapsed in some browsers 1.5294 + if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) { 1.5295 + } else if (nodeInfo.preLine && text.charAt(offset) === "\n") { 1.5296 + visibleChar = " "; 1.5297 + charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK; 1.5298 + } else { 1.5299 + visibleChar = " "; 1.5300 + //pos.checkForFollowingLineBreak = true; 1.5301 + charType = COLLAPSIBLE_SPACE; 1.5302 + } 1.5303 + } else { 1.5304 + visibleChar = textChar; 1.5305 + charType = NON_SPACE; 1.5306 + finalizedChar = true; 1.5307 + } 1.5308 + } else { 1.5309 + visibleChar = textChar; 1.5310 + charType = UNCOLLAPSIBLE_SPACE; 1.5311 + finalizedChar = true; 1.5312 + } 1.5313 + } else { 1.5314 + var nodePassed = node.childNodes[offset - 1]; 1.5315 + if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) { 1.5316 + if (nodePassed.tagName.toLowerCase() == "br") { 1.5317 + visibleChar = "\n"; 1.5318 + pos.isBr = true; 1.5319 + charType = COLLAPSIBLE_SPACE; 1.5320 + finalizedChar = false; 1.5321 + } else { 1.5322 + pos.checkForTrailingSpace = true; 1.5323 + } 1.5324 + } 1.5325 + 1.5326 + // Check the leading space of the next node for the case when a block element follows an inline 1.5327 + // element or text node. In that case, there is an implied line break between the two nodes. 1.5328 + if (!visibleChar) { 1.5329 + var nextNode = node.childNodes[offset]; 1.5330 + if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) { 1.5331 + pos.checkForLeadingSpace = true; 1.5332 + } 1.5333 + } 1.5334 + } 1.5335 + } 1.5336 + 1.5337 + pos.prepopulatedChar = true; 1.5338 + pos.character = visibleChar; 1.5339 + pos.characterType = charType; 1.5340 + pos.isCharInvariant = finalizedChar; 1.5341 + } 1.5342 + }, 1.5343 + 1.5344 + isDefinitelyNonEmpty: function() { 1.5345 + var charType = this.characterType; 1.5346 + return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE; 1.5347 + }, 1.5348 + 1.5349 + // Resolve leading and trailing spaces, which may involve prepopulating other positions 1.5350 + resolveLeadingAndTrailingSpaces: function() { 1.5351 + if (!this.prepopulatedChar) { 1.5352 + this.prepopulateChar(); 1.5353 + } 1.5354 + if (this.checkForTrailingSpace) { 1.5355 + var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace(); 1.5356 + if (trailingSpace) { 1.5357 + this.isTrailingSpace = true; 1.5358 + this.character = trailingSpace; 1.5359 + this.characterType = COLLAPSIBLE_SPACE; 1.5360 + } 1.5361 + this.checkForTrailingSpace = false; 1.5362 + } 1.5363 + if (this.checkForLeadingSpace) { 1.5364 + var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace(); 1.5365 + if (leadingSpace) { 1.5366 + this.isLeadingSpace = true; 1.5367 + this.character = leadingSpace; 1.5368 + this.characterType = COLLAPSIBLE_SPACE; 1.5369 + } 1.5370 + this.checkForLeadingSpace = false; 1.5371 + } 1.5372 + }, 1.5373 + 1.5374 + getPrecedingUncollapsedPosition: function(characterOptions) { 1.5375 + var pos = this, character; 1.5376 + while ( (pos = pos.previousVisible()) ) { 1.5377 + character = pos.getCharacter(characterOptions); 1.5378 + if (character !== "") { 1.5379 + return pos; 1.5380 + } 1.5381 + } 1.5382 + 1.5383 + return null; 1.5384 + }, 1.5385 + 1.5386 + getCharacter: function(characterOptions) { 1.5387 + this.resolveLeadingAndTrailingSpaces(); 1.5388 + 1.5389 + var thisChar = this.character, returnChar; 1.5390 + 1.5391 + // Check if character is ignored 1.5392 + var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters); 1.5393 + var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1); 1.5394 + 1.5395 + // Check if this position's character is invariant (i.e. not dependent on character options) and return it 1.5396 + // if so 1.5397 + if (this.isCharInvariant) { 1.5398 + returnChar = isIgnoredCharacter ? "" : thisChar; 1.5399 + return returnChar; 1.5400 + } 1.5401 + 1.5402 + var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_"); 1.5403 + var cachedChar = this.cache.get(cacheKey); 1.5404 + if (cachedChar !== null) { 1.5405 + return cachedChar; 1.5406 + } 1.5407 + 1.5408 + // We need to actually get the character now 1.5409 + var character = ""; 1.5410 + var collapsible = (this.characterType == COLLAPSIBLE_SPACE); 1.5411 + 1.5412 + var nextPos, previousPos; 1.5413 + var gotPreviousPos = false; 1.5414 + var pos = this; 1.5415 + 1.5416 + function getPreviousPos() { 1.5417 + if (!gotPreviousPos) { 1.5418 + previousPos = pos.getPrecedingUncollapsedPosition(characterOptions); 1.5419 + gotPreviousPos = true; 1.5420 + } 1.5421 + return previousPos; 1.5422 + } 1.5423 + 1.5424 + // Disallow a collapsible space that is followed by a line break or is the last character 1.5425 + if (collapsible) { 1.5426 + // Allow a trailing space that we've previously determined should be included 1.5427 + if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) { 1.5428 + character = "\n"; 1.5429 + } 1.5430 + // Disallow a collapsible space that follows a trailing space or line break, or is the first character, 1.5431 + // or follows a collapsible included space 1.5432 + else if (thisChar == " " && 1.5433 + (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) { 1.5434 + } 1.5435 + // Allow a leading line break unless it follows a line break 1.5436 + else if (thisChar == "\n" && this.isLeadingSpace) { 1.5437 + if (getPreviousPos() && previousPos.character != "\n") { 1.5438 + character = "\n"; 1.5439 + } else { 1.5440 + } 1.5441 + } else { 1.5442 + nextPos = this.nextUncollapsed(); 1.5443 + if (nextPos) { 1.5444 + if (nextPos.isBr) { 1.5445 + this.type = TRAILING_SPACE_BEFORE_BR; 1.5446 + } else if (nextPos.isTrailingSpace && nextPos.character == "\n") { 1.5447 + this.type = TRAILING_SPACE_IN_BLOCK; 1.5448 + } else if (nextPos.isLeadingSpace && nextPos.character == "\n") { 1.5449 + this.type = TRAILING_SPACE_BEFORE_BLOCK; 1.5450 + } 1.5451 + 1.5452 + if (nextPos.character == "\n") { 1.5453 + if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) { 1.5454 + } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) { 1.5455 + } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) { 1.5456 + } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) { 1.5457 + } else if (thisChar == "\n") { 1.5458 + if (nextPos.isTrailingSpace) { 1.5459 + if (this.isTrailingSpace) { 1.5460 + } else if (this.isBr) { 1.5461 + nextPos.type = TRAILING_LINE_BREAK_AFTER_BR; 1.5462 + 1.5463 + if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") { 1.5464 + nextPos.character = ""; 1.5465 + } else { 1.5466 + nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR; 1.5467 + } 1.5468 + } 1.5469 + } else { 1.5470 + character = "\n"; 1.5471 + } 1.5472 + } else if (thisChar == " ") { 1.5473 + character = " "; 1.5474 + } else { 1.5475 + } 1.5476 + } else { 1.5477 + character = thisChar; 1.5478 + } 1.5479 + } else { 1.5480 + } 1.5481 + } 1.5482 + } 1.5483 + 1.5484 + if (ignoredChars.indexOf(character) > -1) { 1.5485 + character = ""; 1.5486 + } 1.5487 + 1.5488 + 1.5489 + this.cache.set(cacheKey, character); 1.5490 + 1.5491 + return character; 1.5492 + }, 1.5493 + 1.5494 + equals: function(pos) { 1.5495 + return !!pos && this.node === pos.node && this.offset === pos.offset; 1.5496 + }, 1.5497 + 1.5498 + inspect: inspectPosition, 1.5499 + 1.5500 + toString: function() { 1.5501 + return this.character; 1.5502 + } 1.5503 + }; 1.5504 + 1.5505 + Position.prototype = positionProto; 1.5506 + 1.5507 + extend(positionProto, { 1.5508 + next: createCachingGetter("nextPos", function(pos) { 1.5509 + var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 1.5510 + if (!node) { 1.5511 + return null; 1.5512 + } 1.5513 + var nextNode, nextOffset, child; 1.5514 + if (offset == nodeWrapper.getLength()) { 1.5515 + // Move onto the next node 1.5516 + nextNode = node.parentNode; 1.5517 + nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0; 1.5518 + } else { 1.5519 + if (nodeWrapper.isCharacterDataNode()) { 1.5520 + nextNode = node; 1.5521 + nextOffset = offset + 1; 1.5522 + } else { 1.5523 + child = node.childNodes[offset]; 1.5524 + // Go into the children next, if children there are 1.5525 + if (session.getNodeWrapper(child).containsPositions()) { 1.5526 + nextNode = child; 1.5527 + nextOffset = 0; 1.5528 + } else { 1.5529 + nextNode = node; 1.5530 + nextOffset = offset + 1; 1.5531 + } 1.5532 + } 1.5533 + } 1.5534 + 1.5535 + return nextNode ? session.getPosition(nextNode, nextOffset) : null; 1.5536 + }), 1.5537 + 1.5538 + previous: createCachingGetter("previous", function(pos) { 1.5539 + var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session; 1.5540 + var previousNode, previousOffset, child; 1.5541 + if (offset == 0) { 1.5542 + previousNode = node.parentNode; 1.5543 + previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0; 1.5544 + } else { 1.5545 + if (nodeWrapper.isCharacterDataNode()) { 1.5546 + previousNode = node; 1.5547 + previousOffset = offset - 1; 1.5548 + } else { 1.5549 + child = node.childNodes[offset - 1]; 1.5550 + // Go into the children next, if children there are 1.5551 + if (session.getNodeWrapper(child).containsPositions()) { 1.5552 + previousNode = child; 1.5553 + previousOffset = dom.getNodeLength(child); 1.5554 + } else { 1.5555 + previousNode = node; 1.5556 + previousOffset = offset - 1; 1.5557 + } 1.5558 + } 1.5559 + } 1.5560 + return previousNode ? session.getPosition(previousNode, previousOffset) : null; 1.5561 + }), 1.5562 + 1.5563 + /* 1.5564 + Next and previous position moving functions that filter out 1.5565 + 1.5566 + - Hidden (CSS visibility/display) elements 1.5567 + - Script and style elements 1.5568 + */ 1.5569 + nextVisible: createCachingGetter("nextVisible", function(pos) { 1.5570 + var next = pos.next(); 1.5571 + if (!next) { 1.5572 + return null; 1.5573 + } 1.5574 + var nodeWrapper = next.nodeWrapper, node = next.node; 1.5575 + var newPos = next; 1.5576 + if (nodeWrapper.isCollapsed()) { 1.5577 + // We're skipping this node and all its descendants 1.5578 + newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1); 1.5579 + } 1.5580 + return newPos; 1.5581 + }), 1.5582 + 1.5583 + nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) { 1.5584 + var nextPos = pos; 1.5585 + while ( (nextPos = nextPos.nextVisible()) ) { 1.5586 + nextPos.resolveLeadingAndTrailingSpaces(); 1.5587 + if (nextPos.character !== "") { 1.5588 + return nextPos; 1.5589 + } 1.5590 + } 1.5591 + return null; 1.5592 + }), 1.5593 + 1.5594 + previousVisible: createCachingGetter("previousVisible", function(pos) { 1.5595 + var previous = pos.previous(); 1.5596 + if (!previous) { 1.5597 + return null; 1.5598 + } 1.5599 + var nodeWrapper = previous.nodeWrapper, node = previous.node; 1.5600 + var newPos = previous; 1.5601 + if (nodeWrapper.isCollapsed()) { 1.5602 + // We're skipping this node and all its descendants 1.5603 + newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex()); 1.5604 + } 1.5605 + return newPos; 1.5606 + }) 1.5607 + }); 1.5608 + 1.5609 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5610 + 1.5611 + var currentSession = null; 1.5612 + 1.5613 + var Session = (function() { 1.5614 + function createWrapperCache(nodeProperty) { 1.5615 + var cache = new Cache(); 1.5616 + 1.5617 + return { 1.5618 + get: function(node) { 1.5619 + var wrappersByProperty = cache.get(node[nodeProperty]); 1.5620 + if (wrappersByProperty) { 1.5621 + for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) { 1.5622 + if (wrapper.node === node) { 1.5623 + return wrapper; 1.5624 + } 1.5625 + } 1.5626 + } 1.5627 + return null; 1.5628 + }, 1.5629 + 1.5630 + set: function(nodeWrapper) { 1.5631 + var property = nodeWrapper.node[nodeProperty]; 1.5632 + var wrappersByProperty = cache.get(property) || cache.set(property, []); 1.5633 + wrappersByProperty.push(nodeWrapper); 1.5634 + } 1.5635 + }; 1.5636 + } 1.5637 + 1.5638 + var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID"); 1.5639 + 1.5640 + function Session() { 1.5641 + this.initCaches(); 1.5642 + } 1.5643 + 1.5644 + Session.prototype = { 1.5645 + initCaches: function() { 1.5646 + this.elementCache = uniqueIDSupported ? (function() { 1.5647 + var elementsCache = new Cache(); 1.5648 + 1.5649 + return { 1.5650 + get: function(el) { 1.5651 + return elementsCache.get(el.uniqueID); 1.5652 + }, 1.5653 + 1.5654 + set: function(elWrapper) { 1.5655 + elementsCache.set(elWrapper.node.uniqueID, elWrapper); 1.5656 + } 1.5657 + }; 1.5658 + })() : createWrapperCache("tagName"); 1.5659 + 1.5660 + // Store text nodes keyed by data, although we may need to truncate this 1.5661 + this.textNodeCache = createWrapperCache("data"); 1.5662 + this.otherNodeCache = createWrapperCache("nodeName"); 1.5663 + }, 1.5664 + 1.5665 + getNodeWrapper: function(node) { 1.5666 + var wrapperCache; 1.5667 + switch (node.nodeType) { 1.5668 + case 1: 1.5669 + wrapperCache = this.elementCache; 1.5670 + break; 1.5671 + case 3: 1.5672 + wrapperCache = this.textNodeCache; 1.5673 + break; 1.5674 + default: 1.5675 + wrapperCache = this.otherNodeCache; 1.5676 + break; 1.5677 + } 1.5678 + 1.5679 + var wrapper = wrapperCache.get(node); 1.5680 + if (!wrapper) { 1.5681 + wrapper = new NodeWrapper(node, this); 1.5682 + wrapperCache.set(wrapper); 1.5683 + } 1.5684 + return wrapper; 1.5685 + }, 1.5686 + 1.5687 + getPosition: function(node, offset) { 1.5688 + return this.getNodeWrapper(node).getPosition(offset); 1.5689 + }, 1.5690 + 1.5691 + getRangeBoundaryPosition: function(range, isStart) { 1.5692 + var prefix = isStart ? "start" : "end"; 1.5693 + return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]); 1.5694 + }, 1.5695 + 1.5696 + detach: function() { 1.5697 + this.elementCache = this.textNodeCache = this.otherNodeCache = null; 1.5698 + } 1.5699 + }; 1.5700 + 1.5701 + return Session; 1.5702 + })(); 1.5703 + 1.5704 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5705 + 1.5706 + function startSession() { 1.5707 + endSession(); 1.5708 + return (currentSession = new Session()); 1.5709 + } 1.5710 + 1.5711 + function getSession() { 1.5712 + return currentSession || startSession(); 1.5713 + } 1.5714 + 1.5715 + function endSession() { 1.5716 + if (currentSession) { 1.5717 + currentSession.detach(); 1.5718 + } 1.5719 + currentSession = null; 1.5720 + } 1.5721 + 1.5722 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5723 + 1.5724 + // Extensions to the rangy.dom utility object 1.5725 + 1.5726 + extend(dom, { 1.5727 + nextNode: nextNode, 1.5728 + previousNode: previousNode 1.5729 + }); 1.5730 + 1.5731 + /*----------------------------------------------------------------------------------------------------------------*/ 1.5732 + 1.5733 + function createCharacterIterator(startPos, backward, endPos, characterOptions) { 1.5734 + 1.5735 + // Adjust the end position to ensure that it is actually reached 1.5736 + if (endPos) { 1.5737 + if (backward) { 1.5738 + if (isCollapsedNode(endPos.node)) { 1.5739 + endPos = startPos.previousVisible(); 1.5740 + } 1.5741 + } else { 1.5742 + if (isCollapsedNode(endPos.node)) { 1.5743 + endPos = endPos.nextVisible(); 1.5744 + } 1.5745 + } 1.5746 + } 1.5747 + 1.5748 + var pos = startPos, finished = false; 1.5749 + 1.5750 + function next() { 1.5751 + var charPos = null; 1.5752 + if (backward) { 1.5753 + charPos = pos; 1.5754 + if (!finished) { 1.5755 + pos = pos.previousVisible(); 1.5756 + finished = !pos || (endPos && pos.equals(endPos)); 1.5757 + } 1.5758 + } else { 1.5759 + if (!finished) { 1.5760 + charPos = pos = pos.nextVisible(); 1.5761 + finished = !pos || (endPos && pos.equals(endPos)); 1.5762 + } 1.5763 + } 1.5764 + if (finished) { 1.5765 + pos = null; 1.5766 + } 1.5767 + return charPos; 1.5768 + } 1.5769 + 1.5770 + var previousTextPos, returnPreviousTextPos = false; 1.5771 + 1.5772 + return { 1.5773 + next: function() { 1.5774 + if (returnPreviousTextPos) { 1.5775 + returnPreviousTextPos = false; 1.5776 + return previousTextPos; 1.5777 + } else { 1.5778 + var pos, character; 1.5779 + while ( (pos = next()) ) { 1.5780 + character = pos.getCharacter(characterOptions); 1.5781 + if (character) { 1.5782 + previousTextPos = pos; 1.5783 + return pos; 1.5784 + } 1.5785 + } 1.5786 + return null; 1.5787 + } 1.5788 + }, 1.5789 + 1.5790 + rewind: function() { 1.5791 + if (previousTextPos) { 1.5792 + returnPreviousTextPos = true; 1.5793 + } else { 1.5794 + throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound."); 1.5795 + } 1.5796 + }, 1.5797 + 1.5798 + dispose: function() { 1.5799 + startPos = endPos = null; 1.5800 + } 1.5801 + }; 1.5802 + } 1.5803 + 1.5804 + var arrayIndexOf = Array.prototype.indexOf ? 1.5805 + function(arr, val) { 1.5806 + return arr.indexOf(val); 1.5807 + } : 1.5808 + function(arr, val) { 1.5809 + for (var i = 0, len = arr.length; i < len; ++i) { 1.5810 + if (arr[i] === val) { 1.5811 + return i; 1.5812 + } 1.5813 + } 1.5814 + return -1; 1.5815 + }; 1.5816 + 1.5817 + // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next() 1.5818 + // is called and there is no more tokenized text 1.5819 + function createTokenizedTextProvider(pos, characterOptions, wordOptions) { 1.5820 + var forwardIterator = createCharacterIterator(pos, false, null, characterOptions); 1.5821 + var backwardIterator = createCharacterIterator(pos, true, null, characterOptions); 1.5822 + var tokenizer = wordOptions.tokenizer; 1.5823 + 1.5824 + // Consumes a word and the whitespace beyond it 1.5825 + function consumeWord(forward) { 1.5826 + var pos, textChar; 1.5827 + var newChars = [], it = forward ? forwardIterator : backwardIterator; 1.5828 + 1.5829 + var passedWordBoundary = false, insideWord = false; 1.5830 + 1.5831 + while ( (pos = it.next()) ) { 1.5832 + textChar = pos.character; 1.5833 + 1.5834 + 1.5835 + if (allWhiteSpaceRegex.test(textChar)) { 1.5836 + if (insideWord) { 1.5837 + insideWord = false; 1.5838 + passedWordBoundary = true; 1.5839 + } 1.5840 + } else { 1.5841 + if (passedWordBoundary) { 1.5842 + it.rewind(); 1.5843 + break; 1.5844 + } else { 1.5845 + insideWord = true; 1.5846 + } 1.5847 + } 1.5848 + newChars.push(pos); 1.5849 + } 1.5850 + 1.5851 + 1.5852 + return newChars; 1.5853 + } 1.5854 + 1.5855 + // Get initial word surrounding initial position and tokenize it 1.5856 + var forwardChars = consumeWord(true); 1.5857 + var backwardChars = consumeWord(false).reverse(); 1.5858 + var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer); 1.5859 + 1.5860 + // Create initial token buffers 1.5861 + var forwardTokensBuffer = forwardChars.length ? 1.5862 + tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : []; 1.5863 + 1.5864 + var backwardTokensBuffer = backwardChars.length ? 1.5865 + tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : []; 1.5866 + 1.5867 + function inspectBuffer(buffer) { 1.5868 + var textPositions = ["[" + buffer.length + "]"]; 1.5869 + for (var i = 0; i < buffer.length; ++i) { 1.5870 + textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")"); 1.5871 + } 1.5872 + return textPositions; 1.5873 + } 1.5874 + 1.5875 + 1.5876 + return { 1.5877 + nextEndToken: function() { 1.5878 + var lastToken, forwardChars; 1.5879 + 1.5880 + // If we're down to the last token, consume character chunks until we have a word or run out of 1.5881 + // characters to consume 1.5882 + while ( forwardTokensBuffer.length == 1 && 1.5883 + !(lastToken = forwardTokensBuffer[0]).isWord && 1.5884 + (forwardChars = consumeWord(true)).length > 0) { 1.5885 + 1.5886 + // Merge trailing non-word into next word and tokenize 1.5887 + forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer); 1.5888 + } 1.5889 + 1.5890 + return forwardTokensBuffer.shift(); 1.5891 + }, 1.5892 + 1.5893 + previousStartToken: function() { 1.5894 + var lastToken, backwardChars; 1.5895 + 1.5896 + // If we're down to the last token, consume character chunks until we have a word or run out of 1.5897 + // characters to consume 1.5898 + while ( backwardTokensBuffer.length == 1 && 1.5899 + !(lastToken = backwardTokensBuffer[0]).isWord && 1.5900 + (backwardChars = consumeWord(false)).length > 0) { 1.5901 + 1.5902 + // Merge leading non-word into next word and tokenize 1.5903 + backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer); 1.5904 + } 1.5905 + 1.5906 + return backwardTokensBuffer.pop(); 1.5907 + }, 1.5908 + 1.5909 + dispose: function() { 1.5910 + forwardIterator.dispose(); 1.5911 + backwardIterator.dispose(); 1.5912 + forwardTokensBuffer = backwardTokensBuffer = null; 1.5913 + } 1.5914 + }; 1.5915 + } 1.5916 + 1.5917 + function movePositionBy(pos, unit, count, characterOptions, wordOptions) { 1.5918 + var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token; 1.5919 + if (count !== 0) { 1.5920 + var backward = (count < 0); 1.5921 + 1.5922 + switch (unit) { 1.5923 + case CHARACTER: 1.5924 + charIterator = createCharacterIterator(pos, backward, null, characterOptions); 1.5925 + while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) { 1.5926 + ++unitsMoved; 1.5927 + newPos = currentPos; 1.5928 + } 1.5929 + nextPos = currentPos; 1.5930 + charIterator.dispose(); 1.5931 + break; 1.5932 + case WORD: 1.5933 + var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions); 1.5934 + var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken; 1.5935 + 1.5936 + while ( (token = next()) && unitsMoved < absCount ) { 1.5937 + if (token.isWord) { 1.5938 + ++unitsMoved; 1.5939 + newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1]; 1.5940 + } 1.5941 + } 1.5942 + break; 1.5943 + default: 1.5944 + throw new Error("movePositionBy: unit '" + unit + "' not implemented"); 1.5945 + } 1.5946 + 1.5947 + // Perform any necessary position tweaks 1.5948 + if (backward) { 1.5949 + newPos = newPos.previousVisible(); 1.5950 + unitsMoved = -unitsMoved; 1.5951 + } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) { 1.5952 + // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space 1.5953 + // before a block element (for example, the line break between "1" and "2" in the following HTML: 1.5954 + // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which 1.5955 + // corresponds with a different selection position in most browsers from the one we want (i.e. at the 1.5956 + // start of the contents of the block element). We get round this by advancing the position returned to 1.5957 + // the last possible equivalent visible position. 1.5958 + if (unit == WORD) { 1.5959 + charIterator = createCharacterIterator(pos, false, null, characterOptions); 1.5960 + nextPos = charIterator.next(); 1.5961 + charIterator.dispose(); 1.5962 + } 1.5963 + if (nextPos) { 1.5964 + newPos = nextPos.previousVisible(); 1.5965 + } 1.5966 + } 1.5967 + } 1.5968 + 1.5969 + 1.5970 + return { 1.5971 + position: newPos, 1.5972 + unitsMoved: unitsMoved 1.5973 + }; 1.5974 + } 1.5975 + 1.5976 + function createRangeCharacterIterator(session, range, characterOptions, backward) { 1.5977 + var rangeStart = session.getRangeBoundaryPosition(range, true); 1.5978 + var rangeEnd = session.getRangeBoundaryPosition(range, false); 1.5979 + var itStart = backward ? rangeEnd : rangeStart; 1.5980 + var itEnd = backward ? rangeStart : rangeEnd; 1.5981 + 1.5982 + return createCharacterIterator(itStart, !!backward, itEnd, characterOptions); 1.5983 + } 1.5984 + 1.5985 + function getRangeCharacters(session, range, characterOptions) { 1.5986 + 1.5987 + var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos; 1.5988 + while ( (pos = it.next()) ) { 1.5989 + chars.push(pos); 1.5990 + } 1.5991 + 1.5992 + it.dispose(); 1.5993 + return chars; 1.5994 + } 1.5995 + 1.5996 + function isWholeWord(startPos, endPos, wordOptions) { 1.5997 + var range = api.createRange(startPos.node); 1.5998 + range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset); 1.5999 + return !range.expand("word", { wordOptions: wordOptions }); 1.6000 + } 1.6001 + 1.6002 + function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) { 1.6003 + var backward = isDirectionBackward(findOptions.direction); 1.6004 + var it = createCharacterIterator( 1.6005 + initialPos, 1.6006 + backward, 1.6007 + initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward), 1.6008 + findOptions.characterOptions 1.6009 + ); 1.6010 + var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex; 1.6011 + var result, insideRegexMatch; 1.6012 + var returnValue = null; 1.6013 + 1.6014 + function handleMatch(startIndex, endIndex) { 1.6015 + var startPos = chars[startIndex].previousVisible(); 1.6016 + var endPos = chars[endIndex - 1]; 1.6017 + var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions)); 1.6018 + 1.6019 + return { 1.6020 + startPos: startPos, 1.6021 + endPos: endPos, 1.6022 + valid: valid 1.6023 + }; 1.6024 + } 1.6025 + 1.6026 + while ( (pos = it.next()) ) { 1.6027 + currentChar = pos.character; 1.6028 + if (!isRegex && !findOptions.caseSensitive) { 1.6029 + currentChar = currentChar.toLowerCase(); 1.6030 + } 1.6031 + 1.6032 + if (backward) { 1.6033 + chars.unshift(pos); 1.6034 + text = currentChar + text; 1.6035 + } else { 1.6036 + chars.push(pos); 1.6037 + text += currentChar; 1.6038 + } 1.6039 + 1.6040 + if (isRegex) { 1.6041 + result = searchTerm.exec(text); 1.6042 + if (result) { 1.6043 + matchStartIndex = result.index; 1.6044 + matchEndIndex = matchStartIndex + result[0].length; 1.6045 + if (insideRegexMatch) { 1.6046 + // Check whether the match is now over 1.6047 + if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) { 1.6048 + returnValue = handleMatch(matchStartIndex, matchEndIndex); 1.6049 + break; 1.6050 + } 1.6051 + } else { 1.6052 + insideRegexMatch = true; 1.6053 + } 1.6054 + } 1.6055 + } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) { 1.6056 + returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length); 1.6057 + break; 1.6058 + } 1.6059 + } 1.6060 + 1.6061 + // Check whether regex match extends to the end of the range 1.6062 + if (insideRegexMatch) { 1.6063 + returnValue = handleMatch(matchStartIndex, matchEndIndex); 1.6064 + } 1.6065 + it.dispose(); 1.6066 + 1.6067 + return returnValue; 1.6068 + } 1.6069 + 1.6070 + function createEntryPointFunction(func) { 1.6071 + return function() { 1.6072 + var sessionRunning = !!currentSession; 1.6073 + var session = getSession(); 1.6074 + var args = [session].concat( util.toArray(arguments) ); 1.6075 + var returnValue = func.apply(this, args); 1.6076 + if (!sessionRunning) { 1.6077 + endSession(); 1.6078 + } 1.6079 + return returnValue; 1.6080 + }; 1.6081 + } 1.6082 + 1.6083 + /*----------------------------------------------------------------------------------------------------------------*/ 1.6084 + 1.6085 + // Extensions to the Rangy Range object 1.6086 + 1.6087 + function createRangeBoundaryMover(isStart, collapse) { 1.6088 + /* 1.6089 + Unit can be "character" or "word" 1.6090 + Options: 1.6091 + 1.6092 + - includeTrailingSpace 1.6093 + - wordRegex 1.6094 + - tokenizer 1.6095 + - collapseSpaceBeforeLineBreak 1.6096 + */ 1.6097 + return createEntryPointFunction( 1.6098 + function(session, unit, count, moveOptions) { 1.6099 + if (typeof count == UNDEF) { 1.6100 + count = unit; 1.6101 + unit = CHARACTER; 1.6102 + } 1.6103 + moveOptions = createNestedOptions(moveOptions, defaultMoveOptions); 1.6104 + 1.6105 + var boundaryIsStart = isStart; 1.6106 + if (collapse) { 1.6107 + boundaryIsStart = (count >= 0); 1.6108 + this.collapse(!boundaryIsStart); 1.6109 + } 1.6110 + var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions); 1.6111 + var newPos = moveResult.position; 1.6112 + this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset); 1.6113 + return moveResult.unitsMoved; 1.6114 + } 1.6115 + ); 1.6116 + } 1.6117 + 1.6118 + function createRangeTrimmer(isStart) { 1.6119 + return createEntryPointFunction( 1.6120 + function(session, characterOptions) { 1.6121 + characterOptions = createOptions(characterOptions, defaultCharacterOptions); 1.6122 + var pos; 1.6123 + var it = createRangeCharacterIterator(session, this, characterOptions, !isStart); 1.6124 + var trimCharCount = 0; 1.6125 + while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) { 1.6126 + ++trimCharCount; 1.6127 + } 1.6128 + it.dispose(); 1.6129 + var trimmed = (trimCharCount > 0); 1.6130 + if (trimmed) { 1.6131 + this[isStart ? "moveStart" : "moveEnd"]( 1.6132 + "character", 1.6133 + isStart ? trimCharCount : -trimCharCount, 1.6134 + { characterOptions: characterOptions } 1.6135 + ); 1.6136 + } 1.6137 + return trimmed; 1.6138 + } 1.6139 + ); 1.6140 + } 1.6141 + 1.6142 + extend(api.rangePrototype, { 1.6143 + moveStart: createRangeBoundaryMover(true, false), 1.6144 + 1.6145 + moveEnd: createRangeBoundaryMover(false, false), 1.6146 + 1.6147 + move: createRangeBoundaryMover(true, true), 1.6148 + 1.6149 + trimStart: createRangeTrimmer(true), 1.6150 + 1.6151 + trimEnd: createRangeTrimmer(false), 1.6152 + 1.6153 + trim: createEntryPointFunction( 1.6154 + function(session, characterOptions) { 1.6155 + var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions); 1.6156 + return startTrimmed || endTrimmed; 1.6157 + } 1.6158 + ), 1.6159 + 1.6160 + expand: createEntryPointFunction( 1.6161 + function(session, unit, expandOptions) { 1.6162 + var moved = false; 1.6163 + expandOptions = createNestedOptions(expandOptions, defaultExpandOptions); 1.6164 + var characterOptions = expandOptions.characterOptions; 1.6165 + if (!unit) { 1.6166 + unit = CHARACTER; 1.6167 + } 1.6168 + if (unit == WORD) { 1.6169 + var wordOptions = expandOptions.wordOptions; 1.6170 + var startPos = session.getRangeBoundaryPosition(this, true); 1.6171 + var endPos = session.getRangeBoundaryPosition(this, false); 1.6172 + 1.6173 + var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions); 1.6174 + var startToken = startTokenizedTextProvider.nextEndToken(); 1.6175 + var newStartPos = startToken.chars[0].previousVisible(); 1.6176 + var endToken, newEndPos; 1.6177 + 1.6178 + if (this.collapsed) { 1.6179 + endToken = startToken; 1.6180 + } else { 1.6181 + var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions); 1.6182 + endToken = endTokenizedTextProvider.previousStartToken(); 1.6183 + } 1.6184 + newEndPos = endToken.chars[endToken.chars.length - 1]; 1.6185 + 1.6186 + if (!newStartPos.equals(startPos)) { 1.6187 + this.setStart(newStartPos.node, newStartPos.offset); 1.6188 + moved = true; 1.6189 + } 1.6190 + if (newEndPos && !newEndPos.equals(endPos)) { 1.6191 + this.setEnd(newEndPos.node, newEndPos.offset); 1.6192 + moved = true; 1.6193 + } 1.6194 + 1.6195 + if (expandOptions.trim) { 1.6196 + if (expandOptions.trimStart) { 1.6197 + moved = this.trimStart(characterOptions) || moved; 1.6198 + } 1.6199 + if (expandOptions.trimEnd) { 1.6200 + moved = this.trimEnd(characterOptions) || moved; 1.6201 + } 1.6202 + } 1.6203 + 1.6204 + return moved; 1.6205 + } else { 1.6206 + return this.moveEnd(CHARACTER, 1, expandOptions); 1.6207 + } 1.6208 + } 1.6209 + ), 1.6210 + 1.6211 + text: createEntryPointFunction( 1.6212 + function(session, characterOptions) { 1.6213 + return this.collapsed ? 1.6214 + "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join(""); 1.6215 + } 1.6216 + ), 1.6217 + 1.6218 + selectCharacters: createEntryPointFunction( 1.6219 + function(session, containerNode, startIndex, endIndex, characterOptions) { 1.6220 + var moveOptions = { characterOptions: characterOptions }; 1.6221 + if (!containerNode) { 1.6222 + containerNode = getBody( this.getDocument() ); 1.6223 + } 1.6224 + this.selectNodeContents(containerNode); 1.6225 + this.collapse(true); 1.6226 + this.moveStart("character", startIndex, moveOptions); 1.6227 + this.collapse(true); 1.6228 + this.moveEnd("character", endIndex - startIndex, moveOptions); 1.6229 + } 1.6230 + ), 1.6231 + 1.6232 + // Character indexes are relative to the start of node 1.6233 + toCharacterRange: createEntryPointFunction( 1.6234 + function(session, containerNode, characterOptions) { 1.6235 + if (!containerNode) { 1.6236 + containerNode = getBody( this.getDocument() ); 1.6237 + } 1.6238 + var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode); 1.6239 + var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1); 1.6240 + var rangeBetween = this.cloneRange(); 1.6241 + var startIndex, endIndex; 1.6242 + if (rangeStartsBeforeNode) { 1.6243 + rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex); 1.6244 + startIndex = -rangeBetween.text(characterOptions).length; 1.6245 + } else { 1.6246 + rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset); 1.6247 + startIndex = rangeBetween.text(characterOptions).length; 1.6248 + } 1.6249 + endIndex = startIndex + this.text(characterOptions).length; 1.6250 + 1.6251 + return { 1.6252 + start: startIndex, 1.6253 + end: endIndex 1.6254 + }; 1.6255 + } 1.6256 + ), 1.6257 + 1.6258 + findText: createEntryPointFunction( 1.6259 + function(session, searchTermParam, findOptions) { 1.6260 + // Set up options 1.6261 + findOptions = createNestedOptions(findOptions, defaultFindOptions); 1.6262 + 1.6263 + // Create word options if we're matching whole words only 1.6264 + if (findOptions.wholeWordsOnly) { 1.6265 + // We don't ever want trailing spaces for search results 1.6266 + findOptions.wordOptions.includeTrailingSpace = false; 1.6267 + } 1.6268 + 1.6269 + var backward = isDirectionBackward(findOptions.direction); 1.6270 + 1.6271 + // Create a range representing the search scope if none was provided 1.6272 + var searchScopeRange = findOptions.withinRange; 1.6273 + if (!searchScopeRange) { 1.6274 + searchScopeRange = api.createRange(); 1.6275 + searchScopeRange.selectNodeContents(this.getDocument()); 1.6276 + } 1.6277 + 1.6278 + // Examine and prepare the search term 1.6279 + var searchTerm = searchTermParam, isRegex = false; 1.6280 + if (typeof searchTerm == "string") { 1.6281 + if (!findOptions.caseSensitive) { 1.6282 + searchTerm = searchTerm.toLowerCase(); 1.6283 + } 1.6284 + } else { 1.6285 + isRegex = true; 1.6286 + } 1.6287 + 1.6288 + var initialPos = session.getRangeBoundaryPosition(this, !backward); 1.6289 + 1.6290 + // Adjust initial position if it lies outside the search scope 1.6291 + var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset); 1.6292 + 1.6293 + if (comparison === -1) { 1.6294 + initialPos = session.getRangeBoundaryPosition(searchScopeRange, true); 1.6295 + } else if (comparison === 1) { 1.6296 + initialPos = session.getRangeBoundaryPosition(searchScopeRange, false); 1.6297 + } 1.6298 + 1.6299 + var pos = initialPos; 1.6300 + var wrappedAround = false; 1.6301 + 1.6302 + // Try to find a match and ignore invalid ones 1.6303 + var findResult; 1.6304 + while (true) { 1.6305 + findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions); 1.6306 + 1.6307 + if (findResult) { 1.6308 + if (findResult.valid) { 1.6309 + this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset); 1.6310 + return true; 1.6311 + } else { 1.6312 + // We've found a match that is not a whole word, so we carry on searching from the point immediately 1.6313 + // after the match 1.6314 + pos = backward ? findResult.startPos : findResult.endPos; 1.6315 + } 1.6316 + } else if (findOptions.wrap && !wrappedAround) { 1.6317 + // No result found but we're wrapping around and limiting the scope to the unsearched part of the range 1.6318 + searchScopeRange = searchScopeRange.cloneRange(); 1.6319 + pos = session.getRangeBoundaryPosition(searchScopeRange, !backward); 1.6320 + searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward); 1.6321 + wrappedAround = true; 1.6322 + } else { 1.6323 + // Nothing found and we can't wrap around, so we're done 1.6324 + return false; 1.6325 + } 1.6326 + } 1.6327 + } 1.6328 + ), 1.6329 + 1.6330 + pasteHtml: function(html) { 1.6331 + this.deleteContents(); 1.6332 + if (html) { 1.6333 + var frag = this.createContextualFragment(html); 1.6334 + var lastChild = frag.lastChild; 1.6335 + this.insertNode(frag); 1.6336 + this.collapseAfter(lastChild); 1.6337 + } 1.6338 + } 1.6339 + }); 1.6340 + 1.6341 + /*----------------------------------------------------------------------------------------------------------------*/ 1.6342 + 1.6343 + // Extensions to the Rangy Selection object 1.6344 + 1.6345 + function createSelectionTrimmer(methodName) { 1.6346 + return createEntryPointFunction( 1.6347 + function(session, characterOptions) { 1.6348 + var trimmed = false; 1.6349 + this.changeEachRange(function(range) { 1.6350 + trimmed = range[methodName](characterOptions) || trimmed; 1.6351 + }); 1.6352 + return trimmed; 1.6353 + } 1.6354 + ); 1.6355 + } 1.6356 + 1.6357 + extend(api.selectionPrototype, { 1.6358 + expand: createEntryPointFunction( 1.6359 + function(session, unit, expandOptions) { 1.6360 + this.changeEachRange(function(range) { 1.6361 + range.expand(unit, expandOptions); 1.6362 + }); 1.6363 + } 1.6364 + ), 1.6365 + 1.6366 + move: createEntryPointFunction( 1.6367 + function(session, unit, count, options) { 1.6368 + var unitsMoved = 0; 1.6369 + if (this.focusNode) { 1.6370 + this.collapse(this.focusNode, this.focusOffset); 1.6371 + var range = this.getRangeAt(0); 1.6372 + if (!options) { 1.6373 + options = {}; 1.6374 + } 1.6375 + options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions); 1.6376 + unitsMoved = range.move(unit, count, options); 1.6377 + this.setSingleRange(range); 1.6378 + } 1.6379 + return unitsMoved; 1.6380 + } 1.6381 + ), 1.6382 + 1.6383 + trimStart: createSelectionTrimmer("trimStart"), 1.6384 + trimEnd: createSelectionTrimmer("trimEnd"), 1.6385 + trim: createSelectionTrimmer("trim"), 1.6386 + 1.6387 + selectCharacters: createEntryPointFunction( 1.6388 + function(session, containerNode, startIndex, endIndex, direction, characterOptions) { 1.6389 + var range = api.createRange(containerNode); 1.6390 + range.selectCharacters(containerNode, startIndex, endIndex, characterOptions); 1.6391 + this.setSingleRange(range, direction); 1.6392 + } 1.6393 + ), 1.6394 + 1.6395 + saveCharacterRanges: createEntryPointFunction( 1.6396 + function(session, containerNode, characterOptions) { 1.6397 + var ranges = this.getAllRanges(), rangeCount = ranges.length; 1.6398 + var rangeInfos = []; 1.6399 + 1.6400 + var backward = rangeCount == 1 && this.isBackward(); 1.6401 + 1.6402 + for (var i = 0, len = ranges.length; i < len; ++i) { 1.6403 + rangeInfos[i] = { 1.6404 + characterRange: ranges[i].toCharacterRange(containerNode, characterOptions), 1.6405 + backward: backward, 1.6406 + characterOptions: characterOptions 1.6407 + }; 1.6408 + } 1.6409 + 1.6410 + return rangeInfos; 1.6411 + } 1.6412 + ), 1.6413 + 1.6414 + restoreCharacterRanges: createEntryPointFunction( 1.6415 + function(session, containerNode, saved) { 1.6416 + this.removeAllRanges(); 1.6417 + for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) { 1.6418 + rangeInfo = saved[i]; 1.6419 + characterRange = rangeInfo.characterRange; 1.6420 + range = api.createRange(containerNode); 1.6421 + range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions); 1.6422 + this.addRange(range, rangeInfo.backward); 1.6423 + } 1.6424 + } 1.6425 + ), 1.6426 + 1.6427 + text: createEntryPointFunction( 1.6428 + function(session, characterOptions) { 1.6429 + var rangeTexts = []; 1.6430 + for (var i = 0, len = this.rangeCount; i < len; ++i) { 1.6431 + rangeTexts[i] = this.getRangeAt(i).text(characterOptions); 1.6432 + } 1.6433 + return rangeTexts.join(""); 1.6434 + } 1.6435 + ) 1.6436 + }); 1.6437 + 1.6438 + /*----------------------------------------------------------------------------------------------------------------*/ 1.6439 + 1.6440 + // Extensions to the core rangy object 1.6441 + 1.6442 + api.innerText = function(el, characterOptions) { 1.6443 + var range = api.createRange(el); 1.6444 + range.selectNodeContents(el); 1.6445 + var text = range.text(characterOptions); 1.6446 + return text; 1.6447 + }; 1.6448 + 1.6449 + api.createWordIterator = function(startNode, startOffset, iteratorOptions) { 1.6450 + var session = getSession(); 1.6451 + iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions); 1.6452 + var startPos = session.getPosition(startNode, startOffset); 1.6453 + var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions); 1.6454 + var backward = isDirectionBackward(iteratorOptions.direction); 1.6455 + 1.6456 + return { 1.6457 + next: function() { 1.6458 + return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken(); 1.6459 + }, 1.6460 + 1.6461 + dispose: function() { 1.6462 + tokenizedTextProvider.dispose(); 1.6463 + this.next = function() {}; 1.6464 + } 1.6465 + }; 1.6466 + }; 1.6467 + 1.6468 + /*----------------------------------------------------------------------------------------------------------------*/ 1.6469 + 1.6470 + api.noMutation = function(func) { 1.6471 + var session = getSession(); 1.6472 + func(session); 1.6473 + endSession(); 1.6474 + }; 1.6475 + 1.6476 + api.noMutation.createEntryPointFunction = createEntryPointFunction; 1.6477 + 1.6478 + api.textRange = { 1.6479 + isBlockNode: isBlockNode, 1.6480 + isCollapsedWhitespaceNode: isCollapsedWhitespaceNode, 1.6481 + 1.6482 + createPosition: createEntryPointFunction( 1.6483 + function(session, node, offset) { 1.6484 + return session.getPosition(node, offset); 1.6485 + } 1.6486 + ) 1.6487 + }; 1.6488 +}); 1.6489 + 1.6490 +/** 1.6491 + * Detect browser support for specific features 1.6492 + */ 1.6493 +wysihtml.browser = (function() { 1.6494 + var userAgent = navigator.userAgent, 1.6495 + testElement = document.createElement("div"), 1.6496 + // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect 1.6497 + // We need to be extra careful about Microsoft as it shows increasing tendency of tainting its userAgent strings with false feathers 1.6498 + isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1 && !isIE(), 1.6499 + isWebKit = userAgent.indexOf("AppleWebKit/") !== -1 && !isIE(), 1.6500 + isChrome = userAgent.indexOf("Chrome/") !== -1 && !isIE(), 1.6501 + isOpera = userAgent.indexOf("Opera/") !== -1 && !isIE(); 1.6502 + 1.6503 + function iosVersion(userAgent) { 1.6504 + return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1]; 1.6505 + } 1.6506 + 1.6507 + function androidVersion(userAgent) { 1.6508 + return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1]; 1.6509 + } 1.6510 + 1.6511 + function isIE(version, equation) { 1.6512 + var rv = -1, 1.6513 + re; 1.6514 + 1.6515 + if (navigator.appName == 'Microsoft Internet Explorer') { 1.6516 + re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); 1.6517 + } else if (navigator.appName == 'Netscape') { 1.6518 + if (navigator.userAgent.indexOf("Trident") > -1) { 1.6519 + re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})"); 1.6520 + } else if ((/Edge\/(\d+)./i).test(navigator.userAgent)) { 1.6521 + re = /Edge\/(\d+)./i; 1.6522 + } 1.6523 + } 1.6524 + 1.6525 + if (re && re.exec(navigator.userAgent) != null) { 1.6526 + rv = parseFloat(RegExp.$1); 1.6527 + } 1.6528 + 1.6529 + if (rv === -1) { return false; } 1.6530 + if (!version) { return true; } 1.6531 + if (!equation) { return version === rv; } 1.6532 + if (equation === "<") { return version < rv; } 1.6533 + if (equation === ">") { return version > rv; } 1.6534 + if (equation === "<=") { return version <= rv; } 1.6535 + if (equation === ">=") { return version >= rv; } 1.6536 + } 1.6537 + 1.6538 + return { 1.6539 + // Static variable needed, publicly accessible, to be able override it in unit tests 1.6540 + USER_AGENT: userAgent, 1.6541 + 1.6542 + /** 1.6543 + * Exclude browsers that are not capable of displaying and handling 1.6544 + * contentEditable as desired: 1.6545 + * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable 1.6546 + * - IE < 8 create invalid markup and crash randomly from time to time 1.6547 + * 1.6548 + * @return {Boolean} 1.6549 + */ 1.6550 + supported: function() { 1.6551 + var userAgent = this.USER_AGENT.toLowerCase(), 1.6552 + // Essential for making html elements editable 1.6553 + hasContentEditableSupport = "contentEditable" in testElement, 1.6554 + // Following methods are needed in order to interact with the contentEditable area 1.6555 + hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, 1.6556 + // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ 1.6557 + hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, 1.6558 + // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) 1.6559 + isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; 1.6560 + return hasContentEditableSupport 1.6561 + && hasEditingApiSupport 1.6562 + && hasQuerySelectorSupport 1.6563 + && !isIncompatibleMobileBrowser; 1.6564 + }, 1.6565 + 1.6566 + isTouchDevice: function() { 1.6567 + return this.supportsEvent("touchmove"); 1.6568 + }, 1.6569 + 1.6570 + isIos: function() { 1.6571 + return (/ipad|iphone|ipod/i).test(this.USER_AGENT); 1.6572 + }, 1.6573 + 1.6574 + isAndroid: function() { 1.6575 + return this.USER_AGENT.indexOf("Android") !== -1; 1.6576 + }, 1.6577 + 1.6578 + /** 1.6579 + * Whether the browser supports sandboxed iframes 1.6580 + * Currently only IE 6+ offers such feature <iframe security="restricted"> 1.6581 + * 1.6582 + * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx 1.6583 + * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx 1.6584 + * 1.6585 + * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage) 1.6586 + */ 1.6587 + supportsSandboxedIframes: function() { 1.6588 + return isIE(); 1.6589 + }, 1.6590 + 1.6591 + /** 1.6592 + * IE6+7 throw a mixed content warning when the src of an iframe 1.6593 + * is empty/unset or about:blank 1.6594 + * window.querySelector is implemented as of IE8 1.6595 + */ 1.6596 + throwsMixedContentWarningWhenIframeSrcIsEmpty: function() { 1.6597 + return !("querySelector" in document); 1.6598 + }, 1.6599 + 1.6600 + /** 1.6601 + * Whether the caret is correctly displayed in contentEditable elements 1.6602 + * Firefox sometimes shows a huge caret in the beginning after focusing 1.6603 + */ 1.6604 + displaysCaretInEmptyContentEditableCorrectly: function() { 1.6605 + return isIE(12, ">"); 1.6606 + }, 1.6607 + 1.6608 + /** 1.6609 + * Opera and IE are the only browsers who offer the css value 1.6610 + * in the original unit, thx to the currentStyle object 1.6611 + * All other browsers provide the computed style in px via window.getComputedStyle 1.6612 + */ 1.6613 + hasCurrentStyleProperty: function() { 1.6614 + return "currentStyle" in testElement; 1.6615 + }, 1.6616 + 1.6617 + /** 1.6618 + * Whether the browser inserts a <br> when pressing enter in a contentEditable element 1.6619 + */ 1.6620 + insertsLineBreaksOnReturn: function() { 1.6621 + return isGecko; 1.6622 + }, 1.6623 + 1.6624 + supportsPlaceholderAttributeOn: function(element) { 1.6625 + return "placeholder" in element; 1.6626 + }, 1.6627 + 1.6628 + supportsEvent: function(eventName) { 1.6629 + return "on" + eventName in testElement || (function() { 1.6630 + testElement.setAttribute("on" + eventName, "return;"); 1.6631 + return typeof(testElement["on" + eventName]) === "function"; 1.6632 + })(); 1.6633 + }, 1.6634 + 1.6635 + /** 1.6636 + * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe 1.6637 + */ 1.6638 + supportsEventsInIframeCorrectly: function() { 1.6639 + return !isOpera; 1.6640 + }, 1.6641 + 1.6642 + /** 1.6643 + * Everything below IE9 doesn't know how to treat HTML5 tags 1.6644 + * 1.6645 + * @param {Object} context The document object on which to check HTML5 support 1.6646 + * 1.6647 + * @example 1.6648 + * wysihtml.browser.supportsHTML5Tags(document); 1.6649 + */ 1.6650 + supportsHTML5Tags: function(context) { 1.6651 + var element = context.createElement("div"), 1.6652 + html5 = "<article>foo</article>"; 1.6653 + element.innerHTML = html5; 1.6654 + return element.innerHTML.toLowerCase() === html5; 1.6655 + }, 1.6656 + 1.6657 + /** 1.6658 + * Checks whether a document supports a certain queryCommand 1.6659 + * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree 1.6660 + * in oder to report correct results 1.6661 + * 1.6662 + * @param {Object} doc Document object on which to check for a query command 1.6663 + * @param {String} command The query command to check for 1.6664 + * @return {Boolean} 1.6665 + * 1.6666 + * @example 1.6667 + * wysihtml.browser.supportsCommand(document, "bold"); 1.6668 + */ 1.6669 + supportsCommand: (function() { 1.6670 + // Following commands are supported but contain bugs in some browsers 1.6671 + // TODO: investigate if some of these bugs can be tested without altering selection on page, instead of targeting browsers and versions directly 1.6672 + var buggyCommands = { 1.6673 + // formatBlock fails with some tags (eg. <blockquote>) 1.6674 + "formatBlock": isIE(10, "<="), 1.6675 + // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets 1.6676 + // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>) 1.6677 + // IE and Opera act a bit different here as they convert the entire content of the current block element into a list 1.6678 + "insertUnorderedList": isIE(), 1.6679 + "insertOrderedList": isIE() 1.6680 + }; 1.6681 + 1.6682 + // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands 1.6683 + var supported = { 1.6684 + "insertHTML": isGecko 1.6685 + }; 1.6686 + 1.6687 + return function(doc, command) { 1.6688 + var isBuggy = buggyCommands[command]; 1.6689 + if (!isBuggy) { 1.6690 + // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled 1.6691 + try { 1.6692 + return doc.queryCommandSupported(command); 1.6693 + } catch(e1) {} 1.6694 + 1.6695 + try { 1.6696 + return doc.queryCommandEnabled(command); 1.6697 + } catch(e2) { 1.6698 + return !!supported[command]; 1.6699 + } 1.6700 + } 1.6701 + return false; 1.6702 + }; 1.6703 + })(), 1.6704 + 1.6705 + /** 1.6706 + * IE: URLs starting with: 1.6707 + * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://, 1.6708 + * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url: 1.6709 + * will automatically be auto-linked when either the user inserts them via copy&paste or presses the 1.6710 + * space bar when the caret is directly after such an url. 1.6711 + * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll 1.6712 + * (related blog post on msdn 1.6713 + * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx). 1.6714 + */ 1.6715 + doesAutoLinkingInContentEditable: function() { 1.6716 + return isIE(); 1.6717 + }, 1.6718 + 1.6719 + /** 1.6720 + * As stated above, IE auto links urls typed into contentEditable elements 1.6721 + * Since IE9 it's possible to prevent this behavior 1.6722 + */ 1.6723 + canDisableAutoLinking: function() { 1.6724 + return this.supportsCommand(document, "AutoUrlDetect"); 1.6725 + }, 1.6726 + 1.6727 + /** 1.6728 + * IE leaves an empty paragraph in the contentEditable element after clearing it 1.6729 + * Chrome/Safari sometimes an empty <div> 1.6730 + */ 1.6731 + clearsContentEditableCorrectly: function() { 1.6732 + return isGecko || isOpera || isWebKit; 1.6733 + }, 1.6734 + 1.6735 + /** 1.6736 + * IE gives wrong results for getAttribute 1.6737 + */ 1.6738 + supportsGetAttributeCorrectly: function() { 1.6739 + var td = document.createElement("td"); 1.6740 + return td.getAttribute("rowspan") != "1"; 1.6741 + }, 1.6742 + 1.6743 + /** 1.6744 + * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them. 1.6745 + * Chrome and Safari both don't support this 1.6746 + */ 1.6747 + canSelectImagesInContentEditable: function() { 1.6748 + return isGecko || isIE() || isOpera; 1.6749 + }, 1.6750 + 1.6751 + /** 1.6752 + * All browsers except Safari and Chrome automatically scroll the range/caret position into view 1.6753 + */ 1.6754 + autoScrollsToCaret: function() { 1.6755 + return !isWebKit; 1.6756 + }, 1.6757 + 1.6758 + /** 1.6759 + * Check whether the browser automatically closes tags that don't need to be opened 1.6760 + */ 1.6761 + autoClosesUnclosedTags: function() { 1.6762 + var clonedTestElement = testElement.cloneNode(false), 1.6763 + returnValue, 1.6764 + innerHTML; 1.6765 + 1.6766 + clonedTestElement.innerHTML = "<p><div></div>"; 1.6767 + innerHTML = clonedTestElement.innerHTML.toLowerCase(); 1.6768 + returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>"; 1.6769 + 1.6770 + // Cache result by overwriting current function 1.6771 + this.autoClosesUnclosedTags = function() { return returnValue; }; 1.6772 + 1.6773 + return returnValue; 1.6774 + }, 1.6775 + 1.6776 + /** 1.6777 + * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists 1.6778 + */ 1.6779 + supportsNativeGetElementsByClassName: function() { 1.6780 + return String(document.getElementsByClassName).indexOf("[native code]") !== -1; 1.6781 + }, 1.6782 + 1.6783 + /** 1.6784 + * As of now (19.04.2011) only supported by Firefox 4 and Chrome 1.6785 + * See https://developer.mozilla.org/en/DOM/Selection/modify 1.6786 + */ 1.6787 + supportsSelectionModify: function() { 1.6788 + return "getSelection" in window && "modify" in window.getSelection(); 1.6789 + }, 1.6790 + 1.6791 + /** 1.6792 + * Opera needs a white space after a <br> in order to position the caret correctly 1.6793 + */ 1.6794 + needsSpaceAfterLineBreak: function() { 1.6795 + return isOpera; 1.6796 + }, 1.6797 + 1.6798 + /** 1.6799 + * Whether the browser supports the speech api on the given element 1.6800 + * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ 1.6801 + * 1.6802 + * @example 1.6803 + * var input = document.createElement("input"); 1.6804 + * if (wysihtml.browser.supportsSpeechApiOn(input)) { 1.6805 + * // ... 1.6806 + * } 1.6807 + */ 1.6808 + supportsSpeechApiOn: function(input) { 1.6809 + var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0]; 1.6810 + return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input); 1.6811 + }, 1.6812 + 1.6813 + /** 1.6814 + * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest 1.6815 + * See https://connect.microsoft.com/ie/feedback/details/650112 1.6816 + * or try the POC http://tifftiff.de/ie9_crash/ 1.6817 + */ 1.6818 + crashesWhenDefineProperty: function(property) { 1.6819 + return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest"); 1.6820 + }, 1.6821 + 1.6822 + /** 1.6823 + * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element 1.6824 + */ 1.6825 + doesAsyncFocus: function() { 1.6826 + return isIE(12, ">"); 1.6827 + }, 1.6828 + 1.6829 + /** 1.6830 + * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document 1.6831 + */ 1.6832 + hasProblemsSettingCaretAfterImg: function() { 1.6833 + return isIE(); 1.6834 + }, 1.6835 + 1.6836 + /* In IE when deleting with caret at the begining of LI, List get broken into half instead of merging the LI with previous */ 1.6837 + hasLiDeletingProblem: function() { 1.6838 + return isIE(); 1.6839 + }, 1.6840 + 1.6841 + hasUndoInContextMenu: function() { 1.6842 + return isGecko || isChrome || isOpera; 1.6843 + }, 1.6844 + 1.6845 + /** 1.6846 + * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode) 1.6847 + * is used (regardless if rangy or native) 1.6848 + * This especially happens when the caret is positioned right after a <br> because then 1.6849 + * insertNode() will insert the node right before the <br> 1.6850 + */ 1.6851 + hasInsertNodeIssue: function() { 1.6852 + return isOpera; 1.6853 + }, 1.6854 + 1.6855 + /** 1.6856 + * IE 8+9 don't fire the focus event of the <body> when the iframe gets focused (even though the caret gets set into the <body>) 1.6857 + */ 1.6858 + hasIframeFocusIssue: function() { 1.6859 + return isIE(); 1.6860 + }, 1.6861 + 1.6862 + /** 1.6863 + * Chrome + Safari create invalid nested markup after paste 1.6864 + * 1.6865 + * <p> 1.6866 + * foo 1.6867 + * <p>bar</p> <!-- BOO! --> 1.6868 + * </p> 1.6869 + */ 1.6870 + createsNestedInvalidMarkupAfterPaste: function() { 1.6871 + return isWebKit; 1.6872 + }, 1.6873 + 1.6874 + // In all webkit browsers there are some places where caret can not be placed at the end of blocks and directly before block level element 1.6875 + // when startContainer is element. 1.6876 + hasCaretBlockElementIssue: function() { 1.6877 + return isWebKit; 1.6878 + }, 1.6879 + 1.6880 + supportsMutationEvents: function() { 1.6881 + return ("MutationEvent" in window); 1.6882 + }, 1.6883 + 1.6884 + /** 1.6885 + IE (at least up to 11) does not support clipboardData on event. 1.6886 + It is on window but cannot return text/html 1.6887 + Should actually check for clipboardData on paste event, but cannot in firefox 1.6888 + */ 1.6889 + supportsModernPaste: function () { 1.6890 + return !isIE(); 1.6891 + }, 1.6892 + 1.6893 + // Unifies the property names of element.style by returning the suitable property name for current browser 1.6894 + // Input property key must be the standard 1.6895 + fixStyleKey: function(key) { 1.6896 + if (key === "cssFloat") { 1.6897 + return ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"; 1.6898 + } 1.6899 + return key; 1.6900 + }, 1.6901 + 1.6902 + usesControlRanges: function() { 1.6903 + return document.body && "createControlRange" in document.body; 1.6904 + }, 1.6905 + 1.6906 + // Webkit browsers have an issue that when caret is at the end of link it is moved outside of link while inserting new characters, 1.6907 + // so all inserted content will be after link. Selection before inserion is reported to be in link though. 1.6908 + // This makes changing link texts from problematic to impossible (if link is just 1 characer long) for the user. 1.6909 + // TODO: needs to be tested better than just browser as it some day might get fixed 1.6910 + hasCaretAtLinkEndInsertionProblems: function() { 1.6911 + return isWebKit; 1.6912 + } 1.6913 + }; 1.6914 +})(); 1.6915 + 1.6916 +wysihtml.lang.array = function(arr) { 1.6917 + return { 1.6918 + /** 1.6919 + * Check whether a given object exists in an array 1.6920 + * 1.6921 + * @example 1.6922 + * wysihtml.lang.array([1, 2]).contains(1); 1.6923 + * // => true 1.6924 + * 1.6925 + * Can be used to match array with array. If intersection is found true is returned 1.6926 + */ 1.6927 + contains: function(needle) { 1.6928 + if (Array.isArray(needle)) { 1.6929 + for (var i = needle.length; i--;) { 1.6930 + if (wysihtml.lang.array(arr).indexOf(needle[i]) !== -1) { 1.6931 + return true; 1.6932 + } 1.6933 + } 1.6934 + return false; 1.6935 + } else { 1.6936 + return wysihtml.lang.array(arr).indexOf(needle) !== -1; 1.6937 + } 1.6938 + }, 1.6939 + 1.6940 + /** 1.6941 + * Check whether a given object exists in an array and return index 1.6942 + * If no elelemt found returns -1 1.6943 + * 1.6944 + * @example 1.6945 + * wysihtml.lang.array([1, 2]).indexOf(2); 1.6946 + * // => 1 1.6947 + */ 1.6948 + indexOf: function(needle) { 1.6949 + if (arr.indexOf) { 1.6950 + return arr.indexOf(needle); 1.6951 + } else { 1.6952 + for (var i=0, length=arr.length; i<length; i++) { 1.6953 + if (arr[i] === needle) { return i; } 1.6954 + } 1.6955 + return -1; 1.6956 + } 1.6957 + }, 1.6958 + 1.6959 + /** 1.6960 + * Substract one array from another 1.6961 + * 1.6962 + * @example 1.6963 + * wysihtml.lang.array([1, 2, 3, 4]).without([3, 4]); 1.6964 + * // => [1, 2] 1.6965 + */ 1.6966 + without: function(arrayToSubstract) { 1.6967 + arrayToSubstract = wysihtml.lang.array(arrayToSubstract); 1.6968 + var newArr = [], 1.6969 + i = 0, 1.6970 + length = arr.length; 1.6971 + for (; i<length; i++) { 1.6972 + if (!arrayToSubstract.contains(arr[i])) { 1.6973 + newArr.push(arr[i]); 1.6974 + } 1.6975 + } 1.6976 + return newArr; 1.6977 + }, 1.6978 + 1.6979 + /** 1.6980 + * Return a clean native array 1.6981 + * 1.6982 + * Following will convert a Live NodeList to a proper Array 1.6983 + * @example 1.6984 + * var childNodes = wysihtml.lang.array(document.body.childNodes).get(); 1.6985 + */ 1.6986 + get: function() { 1.6987 + var i = 0, 1.6988 + length = arr.length, 1.6989 + newArray = []; 1.6990 + for (; i<length; i++) { 1.6991 + newArray.push(arr[i]); 1.6992 + } 1.6993 + return newArray; 1.6994 + }, 1.6995 + 1.6996 + /** 1.6997 + * Creates a new array with the results of calling a provided function on every element in this array. 1.6998 + * optionally this can be provided as second argument 1.6999 + * 1.7000 + * @example 1.7001 + * var childNodes = wysihtml.lang.array([1,2,3,4]).map(function (value, index, array) { 1.7002 + return value * 2; 1.7003 + * }); 1.7004 + * // => [2,4,6,8] 1.7005 + */ 1.7006 + map: function(callback, thisArg) { 1.7007 + if (Array.prototype.map) { 1.7008 + return arr.map(callback, thisArg); 1.7009 + } else { 1.7010 + var len = arr.length >>> 0, 1.7011 + A = new Array(len), 1.7012 + i = 0; 1.7013 + for (; i < len; i++) { 1.7014 + A[i] = callback.call(thisArg, arr[i], i, arr); 1.7015 + } 1.7016 + return A; 1.7017 + } 1.7018 + }, 1.7019 + 1.7020 + /* ReturnS new array without duplicate entries 1.7021 + * 1.7022 + * @example 1.7023 + * var uniq = wysihtml.lang.array([1,2,3,2,1,4]).unique(); 1.7024 + * // => [1,2,3,4] 1.7025 + */ 1.7026 + unique: function() { 1.7027 + var vals = [], 1.7028 + max = arr.length, 1.7029 + idx = 0; 1.7030 + 1.7031 + while (idx < max) { 1.7032 + if (!wysihtml.lang.array(vals).contains(arr[idx])) { 1.7033 + vals.push(arr[idx]); 1.7034 + } 1.7035 + idx++; 1.7036 + } 1.7037 + return vals; 1.7038 + } 1.7039 + 1.7040 + }; 1.7041 +}; 1.7042 + 1.7043 +wysihtml.lang.Dispatcher = Base.extend( 1.7044 + /** @scope wysihtml.lang.Dialog.prototype */ { 1.7045 + on: function(eventName, handler) { 1.7046 + this.events = this.events || {}; 1.7047 + this.events[eventName] = this.events[eventName] || []; 1.7048 + this.events[eventName].push(handler); 1.7049 + return this; 1.7050 + }, 1.7051 + 1.7052 + off: function(eventName, handler) { 1.7053 + this.events = this.events || {}; 1.7054 + var i = 0, 1.7055 + handlers, 1.7056 + newHandlers; 1.7057 + if (eventName) { 1.7058 + handlers = this.events[eventName] || [], 1.7059 + newHandlers = []; 1.7060 + for (; i<handlers.length; i++) { 1.7061 + if (handlers[i] !== handler && handler) { 1.7062 + newHandlers.push(handlers[i]); 1.7063 + } 1.7064 + } 1.7065 + this.events[eventName] = newHandlers; 1.7066 + } else { 1.7067 + // Clean up all events 1.7068 + this.events = {}; 1.7069 + } 1.7070 + return this; 1.7071 + }, 1.7072 + 1.7073 + fire: function(eventName, payload) { 1.7074 + this.events = this.events || {}; 1.7075 + var handlers = this.events[eventName] || [], 1.7076 + i = 0; 1.7077 + for (; i<handlers.length; i++) { 1.7078 + handlers[i].call(this, payload); 1.7079 + } 1.7080 + return this; 1.7081 + }, 1.7082 + 1.7083 + // deprecated, use .on() 1.7084 + observe: function() { 1.7085 + return this.on.apply(this, arguments); 1.7086 + }, 1.7087 + 1.7088 + // deprecated, use .off() 1.7089 + stopObserving: function() { 1.7090 + return this.off.apply(this, arguments); 1.7091 + } 1.7092 +}); 1.7093 + 1.7094 +wysihtml.lang.object = function(obj) { 1.7095 + return { 1.7096 + /** 1.7097 + * @example 1.7098 + * wysihtml.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get(); 1.7099 + * // => { foo: 1, bar: 2, baz: 3 } 1.7100 + */ 1.7101 + merge: function(otherObj, deep) { 1.7102 + for (var i in otherObj) { 1.7103 + if (deep && wysihtml.lang.object(otherObj[i]).isPlainObject() && (typeof obj[i] === "undefined" || wysihtml.lang.object(obj[i]).isPlainObject())) { 1.7104 + if (typeof obj[i] === "undefined") { 1.7105 + obj[i] = wysihtml.lang.object(otherObj[i]).clone(true); 1.7106 + } else { 1.7107 + wysihtml.lang.object(obj[i]).merge(wysihtml.lang.object(otherObj[i]).clone(true)); 1.7108 + } 1.7109 + } else { 1.7110 + obj[i] = wysihtml.lang.object(otherObj[i]).isPlainObject() ? wysihtml.lang.object(otherObj[i]).clone(true) : otherObj[i]; 1.7111 + } 1.7112 + } 1.7113 + return this; 1.7114 + }, 1.7115 + 1.7116 + difference: function (otherObj) { 1.7117 + var diffObj = {}; 1.7118 + 1.7119 + // Get old values not in comparing object 1.7120 + for (var i in obj) { 1.7121 + if (obj.hasOwnProperty(i)) { 1.7122 + if (!otherObj.hasOwnProperty(i)) { 1.7123 + diffObj[i] = obj[i]; 1.7124 + } 1.7125 + } 1.7126 + } 1.7127 + 1.7128 + // Get new and different values in comparing object 1.7129 + for (var o in otherObj) { 1.7130 + if (otherObj.hasOwnProperty(o)) { 1.7131 + if (!obj.hasOwnProperty(o) || obj[o] !== otherObj[o]) { 1.7132 + diffObj[0] = obj[0]; 1.7133 + } 1.7134 + } 1.7135 + } 1.7136 + return diffObj; 1.7137 + }, 1.7138 + 1.7139 + get: function() { 1.7140 + return obj; 1.7141 + }, 1.7142 + 1.7143 + /** 1.7144 + * @example 1.7145 + * wysihtml.lang.object({ foo: 1 }).clone(); 1.7146 + * // => { foo: 1 } 1.7147 + * 1.7148 + * v0.4.14 adds options for deep clone : wysihtml.lang.object({ foo: 1 }).clone(true); 1.7149 + */ 1.7150 + clone: function(deep) { 1.7151 + var newObj = {}, 1.7152 + i; 1.7153 + 1.7154 + if (obj === null || !wysihtml.lang.object(obj).isPlainObject()) { 1.7155 + return obj; 1.7156 + } 1.7157 + 1.7158 + for (i in obj) { 1.7159 + if(obj.hasOwnProperty(i)) { 1.7160 + if (deep) { 1.7161 + newObj[i] = wysihtml.lang.object(obj[i]).clone(deep); 1.7162 + } else { 1.7163 + newObj[i] = obj[i]; 1.7164 + } 1.7165 + } 1.7166 + } 1.7167 + return newObj; 1.7168 + }, 1.7169 + 1.7170 + /** 1.7171 + * @example 1.7172 + * wysihtml.lang.object([]).isArray(); 1.7173 + * // => true 1.7174 + */ 1.7175 + isArray: function() { 1.7176 + return Object.prototype.toString.call(obj) === "[object Array]"; 1.7177 + }, 1.7178 + 1.7179 + /** 1.7180 + * @example 1.7181 + * wysihtml.lang.object(function() {}).isFunction(); 1.7182 + * // => true 1.7183 + */ 1.7184 + isFunction: function() { 1.7185 + return Object.prototype.toString.call(obj) === '[object Function]'; 1.7186 + }, 1.7187 + 1.7188 + isPlainObject: function () { 1.7189 + return obj && Object.prototype.toString.call(obj) === '[object Object]' && !(("Node" in window) ? obj instanceof Node : obj instanceof Element || obj instanceof Text); 1.7190 + }, 1.7191 + 1.7192 + /** 1.7193 + * @example 1.7194 + * wysihtml.lang.object({}).isEmpty(); 1.7195 + * // => true 1.7196 + */ 1.7197 + isEmpty: function() { 1.7198 + for (var i in obj) { 1.7199 + if (obj.hasOwnProperty(i)) { 1.7200 + return false; 1.7201 + } 1.7202 + } 1.7203 + return true; 1.7204 + } 1.7205 + }; 1.7206 +}; 1.7207 + 1.7208 +(function() { 1.7209 + var WHITE_SPACE_START = /^\s+/, 1.7210 + WHITE_SPACE_END = /\s+$/, 1.7211 + ENTITY_REG_EXP = /[&<>\t"]/g, 1.7212 + ENTITY_MAP = { 1.7213 + '&': '&', 1.7214 + '<': '<', 1.7215 + '>': '>', 1.7216 + '"': """, 1.7217 + '\t':" " 1.7218 + }; 1.7219 + wysihtml.lang.string = function(str) { 1.7220 + str = String(str); 1.7221 + return { 1.7222 + /** 1.7223 + * @example 1.7224 + * wysihtml.lang.string(" foo ").trim(); 1.7225 + * // => "foo" 1.7226 + */ 1.7227 + trim: function() { 1.7228 + return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, ""); 1.7229 + }, 1.7230 + 1.7231 + /** 1.7232 + * @example 1.7233 + * wysihtml.lang.string("Hello #{name}").interpolate({ name: "Christopher" }); 1.7234 + * // => "Hello Christopher" 1.7235 + */ 1.7236 + interpolate: function(vars) { 1.7237 + for (var i in vars) { 1.7238 + str = this.replace("#{" + i + "}").by(vars[i]); 1.7239 + } 1.7240 + return str; 1.7241 + }, 1.7242 + 1.7243 + /** 1.7244 + * @example 1.7245 + * wysihtml.lang.string("Hello Tom").replace("Tom").with("Hans"); 1.7246 + * // => "Hello Hans" 1.7247 + */ 1.7248 + replace: function(search) { 1.7249 + return { 1.7250 + by: function(replace) { 1.7251 + return str.split(search).join(replace); 1.7252 + } 1.7253 + }; 1.7254 + }, 1.7255 + 1.7256 + /** 1.7257 + * @example 1.7258 + * wysihtml.lang.string("hello<br>").escapeHTML(); 1.7259 + * // => "hello<br>" 1.7260 + */ 1.7261 + escapeHTML: function(linebreaks, convertSpaces) { 1.7262 + var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; }); 1.7263 + if (linebreaks) { 1.7264 + html = html.replace(/(?:\r\n|\r|\n)/g, '<br />'); 1.7265 + } 1.7266 + if (convertSpaces) { 1.7267 + html = html.replace(/ /gi, " "); 1.7268 + } 1.7269 + return html; 1.7270 + } 1.7271 + }; 1.7272 + }; 1.7273 +})(); 1.7274 + 1.7275 +/** 1.7276 + * Find urls in descendant text nodes of an element and auto-links them 1.7277 + * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/ 1.7278 + * 1.7279 + * @param {Element} element Container element in which to search for urls 1.7280 + * 1.7281 + * @example 1.7282 + * <div id="text-container">Please click here: www.google.com</div> 1.7283 + * <script>wysihtml.dom.autoLink(document.getElementById("text-container"));</script> 1.7284 + */ 1.7285 +(function(wysihtml) { 1.7286 + var /** 1.7287 + * Don't auto-link urls that are contained in the following elements: 1.7288 + */ 1.7289 + IGNORE_URLS_IN = wysihtml.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]), 1.7290 + /** 1.7291 + * revision 1: 1.7292 + * /(\S+\.{1}[^\s\,\.\!]+)/g 1.7293 + * 1.7294 + * revision 2: 1.7295 + * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim 1.7296 + * 1.7297 + * put this in the beginning if you don't wan't to match within a word 1.7298 + * (^|[\>\(\{\[\s\>]) 1.7299 + */ 1.7300 + URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, 1.7301 + TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, 1.7302 + MAX_DISPLAY_LENGTH = 100, 1.7303 + BRACKETS = { ")": "(", "]": "[", "}": "{" }; 1.7304 + 1.7305 + function autoLink(element, ignoreInClasses) { 1.7306 + if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) { 1.7307 + return element; 1.7308 + } 1.7309 + 1.7310 + if (element === element.ownerDocument.documentElement) { 1.7311 + element = element.ownerDocument.body; 1.7312 + } 1.7313 + 1.7314 + return _parseNode(element, ignoreInClasses); 1.7315 + } 1.7316 + 1.7317 + /** 1.7318 + * This is basically a rebuild of 1.7319 + * the rails auto_link_urls text helper 1.7320 + */ 1.7321 + function _convertUrlsToLinks(str) { 1.7322 + return str.replace(URL_REG_EXP, function(match, url) { 1.7323 + var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "", 1.7324 + opening = BRACKETS[punctuation]; 1.7325 + url = url.replace(TRAILING_CHAR_REG_EXP, ""); 1.7326 + 1.7327 + if (url.split(opening).length > url.split(punctuation).length) { 1.7328 + url = url + punctuation; 1.7329 + punctuation = ""; 1.7330 + } 1.7331 + var realUrl = url, 1.7332 + displayUrl = url; 1.7333 + if (url.length > MAX_DISPLAY_LENGTH) { 1.7334 + displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "..."; 1.7335 + } 1.7336 + // Add http prefix if necessary 1.7337 + if (realUrl.substr(0, 4) === "www.") { 1.7338 + realUrl = "http://" + realUrl; 1.7339 + } 1.7340 + 1.7341 + return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; 1.7342 + }); 1.7343 + } 1.7344 + 1.7345 + /** 1.7346 + * Creates or (if already cached) returns a temp element 1.7347 + * for the given document object 1.7348 + */ 1.7349 + function _getTempElement(context) { 1.7350 + var tempElement = context._wysihtml_tempElement; 1.7351 + if (!tempElement) { 1.7352 + tempElement = context._wysihtml_tempElement = context.createElement("div"); 1.7353 + } 1.7354 + return tempElement; 1.7355 + } 1.7356 + 1.7357 + /** 1.7358 + * Replaces the original text nodes with the newly auto-linked dom tree 1.7359 + */ 1.7360 + function _wrapMatchesInNode(textNode) { 1.7361 + var parentNode = textNode.parentNode, 1.7362 + nodeValue = wysihtml.lang.string(textNode.data).escapeHTML(), 1.7363 + tempElement = _getTempElement(parentNode.ownerDocument); 1.7364 + 1.7365 + // We need to insert an empty/temporary <span /> to fix IE quirks 1.7366 + // Elsewise IE would strip white space in the beginning 1.7367 + tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue); 1.7368 + tempElement.removeChild(tempElement.firstChild); 1.7369 + 1.7370 + while (tempElement.firstChild) { 1.7371 + // inserts tempElement.firstChild before textNode 1.7372 + parentNode.insertBefore(tempElement.firstChild, textNode); 1.7373 + } 1.7374 + parentNode.removeChild(textNode); 1.7375 + } 1.7376 + 1.7377 + function _hasParentThatShouldBeIgnored(node, ignoreInClasses) { 1.7378 + var nodeName; 1.7379 + while (node.parentNode) { 1.7380 + node = node.parentNode; 1.7381 + nodeName = node.nodeName; 1.7382 + if (node.className && wysihtml.lang.array(node.className.split(' ')).contains(ignoreInClasses)) { 1.7383 + return true; 1.7384 + } 1.7385 + if (IGNORE_URLS_IN.contains(nodeName)) { 1.7386 + return true; 1.7387 + } else if (nodeName === "body") { 1.7388 + return false; 1.7389 + } 1.7390 + } 1.7391 + return false; 1.7392 + } 1.7393 + 1.7394 + function _parseNode(element, ignoreInClasses) { 1.7395 + if (IGNORE_URLS_IN.contains(element.nodeName)) { 1.7396 + return; 1.7397 + } 1.7398 + 1.7399 + if (element.className && wysihtml.lang.array(element.className.split(' ')).contains(ignoreInClasses)) { 1.7400 + return; 1.7401 + } 1.7402 + 1.7403 + if (element.nodeType === wysihtml.TEXT_NODE && element.data.match(URL_REG_EXP)) { 1.7404 + _wrapMatchesInNode(element); 1.7405 + return; 1.7406 + } 1.7407 + 1.7408 + var childNodes = wysihtml.lang.array(element.childNodes).get(), 1.7409 + childNodesLength = childNodes.length, 1.7410 + i = 0; 1.7411 + 1.7412 + for (; i<childNodesLength; i++) { 1.7413 + _parseNode(childNodes[i], ignoreInClasses); 1.7414 + } 1.7415 + 1.7416 + return element; 1.7417 + } 1.7418 + 1.7419 + wysihtml.dom.autoLink = autoLink; 1.7420 + 1.7421 + // Reveal url reg exp to the outside 1.7422 + wysihtml.dom.autoLink.URL_REG_EXP = URL_REG_EXP; 1.7423 +})(wysihtml); 1.7424 + 1.7425 +(function(wysihtml) { 1.7426 + var api = wysihtml.dom; 1.7427 + 1.7428 + api.addClass = function(element, className) { 1.7429 + var classList = element.classList; 1.7430 + if (classList) { 1.7431 + return classList.add(className); 1.7432 + } 1.7433 + if (api.hasClass(element, className)) { 1.7434 + return; 1.7435 + } 1.7436 + element.className += " " + className; 1.7437 + }; 1.7438 + 1.7439 + api.removeClass = function(element, className) { 1.7440 + var classList = element.classList; 1.7441 + if (classList) { 1.7442 + return classList.remove(className); 1.7443 + } 1.7444 + 1.7445 + element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); 1.7446 + }; 1.7447 + 1.7448 + api.hasClass = function(element, className) { 1.7449 + var classList = element.classList; 1.7450 + if (classList) { 1.7451 + return classList.contains(className); 1.7452 + } 1.7453 + 1.7454 + var elementClassName = element.className; 1.7455 + return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); 1.7456 + }; 1.7457 +})(wysihtml); 1.7458 + 1.7459 +wysihtml.dom.compareDocumentPosition = (function() { 1.7460 + var documentElement = document.documentElement; 1.7461 + if (documentElement.compareDocumentPosition) { 1.7462 + return function(container, element) { 1.7463 + return container.compareDocumentPosition(element); 1.7464 + }; 1.7465 + } else { 1.7466 + return function( container, element ) { 1.7467 + // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license 1.7468 + var thisOwner, otherOwner; 1.7469 + 1.7470 + if( container.nodeType === 9) // Node.DOCUMENT_NODE 1.7471 + thisOwner = container; 1.7472 + else 1.7473 + thisOwner = container.ownerDocument; 1.7474 + 1.7475 + if( element.nodeType === 9) // Node.DOCUMENT_NODE 1.7476 + otherOwner = element; 1.7477 + else 1.7478 + otherOwner = element.ownerDocument; 1.7479 + 1.7480 + if( container === element ) return 0; 1.7481 + if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY; 1.7482 + if( container.ownerDocument === element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS; 1.7483 + if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED; 1.7484 + 1.7485 + // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child. 1.7486 + if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml.lang.array(container.childNodes).indexOf( element ) !== -1) 1.7487 + return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY; 1.7488 + 1.7489 + if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml.lang.array(element.childNodes).indexOf( container ) !== -1) 1.7490 + return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS; 1.7491 + 1.7492 + var point = container; 1.7493 + var parents = [ ]; 1.7494 + var previous = null; 1.7495 + while( point ) { 1.7496 + if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS; 1.7497 + parents.push( point ); 1.7498 + point = point.parentNode; 1.7499 + } 1.7500 + point = element; 1.7501 + previous = null; 1.7502 + while( point ) { 1.7503 + if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY; 1.7504 + var location_index = wysihtml.lang.array(parents).indexOf( point ); 1.7505 + if( location_index !== -1) { 1.7506 + var smallest_common_ancestor = parents[ location_index ]; 1.7507 + 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] ); 1.7508 + var other_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous ); 1.7509 + if( this_index > other_index ) { 1.7510 + return 2; //Node.DOCUMENT_POSITION_PRECEDING; 1.7511 + } 1.7512 + else { 1.7513 + return 4; //Node.DOCUMENT_POSITION_FOLLOWING; 1.7514 + } 1.7515 + } 1.7516 + previous = point; 1.7517 + point = point.parentNode; 1.7518 + } 1.7519 + return 1; //Node.DOCUMENT_POSITION_DISCONNECTED; 1.7520 + }; 1.7521 + } 1.7522 +})(); 1.7523 + 1.7524 +wysihtml.dom.contains = (function() { 1.7525 + var documentElement = document.documentElement; 1.7526 + if (documentElement.contains) { 1.7527 + return function(container, element) { 1.7528 + if (element.nodeType !== wysihtml.ELEMENT_NODE) { 1.7529 + if (element.parentNode === container) { 1.7530 + return true; 1.7531 + } 1.7532 + element = element.parentNode; 1.7533 + } 1.7534 + return container !== element && container.contains(element); 1.7535 + }; 1.7536 + } else if (documentElement.compareDocumentPosition) { 1.7537 + return function(container, element) { 1.7538 + // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition 1.7539 + return !!(container.compareDocumentPosition(element) & 16); 1.7540 + }; 1.7541 + } 1.7542 +})(); 1.7543 + 1.7544 +(function(wysihtml) { 1.7545 + var doc = document; 1.7546 + wysihtml.dom.ContentEditableArea = Base.extend({ 1.7547 + getContentEditable: function() { 1.7548 + return this.element; 1.7549 + }, 1.7550 + 1.7551 + getWindow: function() { 1.7552 + return this.element.ownerDocument.defaultView || this.element.ownerDocument.parentWindow; 1.7553 + }, 1.7554 + 1.7555 + getDocument: function() { 1.7556 + return this.element.ownerDocument; 1.7557 + }, 1.7558 + 1.7559 + constructor: function(readyCallback, config, contentEditable) { 1.7560 + this.callback = readyCallback || wysihtml.EMPTY_FUNCTION; 1.7561 + this.config = wysihtml.lang.object({}).merge(config).get(); 1.7562 + if (!this.config.className) { 1.7563 + this.config.className = "wysihtml-sandbox"; 1.7564 + } 1.7565 + if (contentEditable) { 1.7566 + this.element = this._bindElement(contentEditable); 1.7567 + } else { 1.7568 + this.element = this._createElement(); 1.7569 + } 1.7570 + }, 1.7571 + 1.7572 + destroy: function() { 1.7573 + 1.7574 + }, 1.7575 + 1.7576 + // creates a new contenteditable and initiates it 1.7577 + _createElement: function() { 1.7578 + var element = doc.createElement("div"); 1.7579 + element.className = this.config.className; 1.7580 + this._loadElement(element); 1.7581 + return element; 1.7582 + }, 1.7583 + 1.7584 + // initiates an allready existent contenteditable 1.7585 + _bindElement: function(contentEditable) { 1.7586 + contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml-sandbox" : "wysihtml-sandbox"; 1.7587 + this._loadElement(contentEditable, true); 1.7588 + return contentEditable; 1.7589 + }, 1.7590 + 1.7591 + _loadElement: function(element, contentExists) { 1.7592 + var that = this; 1.7593 + 1.7594 + if (!contentExists) { 1.7595 + var innerHtml = this._getHtml(); 1.7596 + element.innerHTML = innerHtml; 1.7597 + } 1.7598 + 1.7599 + this.loaded = true; 1.7600 + // Trigger the callback 1.7601 + setTimeout(function() { that.callback(that); }, 0); 1.7602 + }, 1.7603 + 1.7604 + _getHtml: function(templateVars) { 1.7605 + return ''; 1.7606 + } 1.7607 + 1.7608 + }); 1.7609 +})(wysihtml); 1.7610 + 1.7611 +/** 1.7612 + * Converts an HTML fragment/element into a unordered/ordered list 1.7613 + * 1.7614 + * @param {Element} element The element which should be turned into a list 1.7615 + * @param {String} listType The list type in which to convert the tree (either "ul" or "ol") 1.7616 + * @return {Element} The created list 1.7617 + * 1.7618 + * @example 1.7619 + * <!-- Assume the following dom: --> 1.7620 + * <span id="pseudo-list"> 1.7621 + * eminem<br> 1.7622 + * dr. dre 1.7623 + * <div>50 Cent</div> 1.7624 + * </span> 1.7625 + * 1.7626 + * <script> 1.7627 + * wysihtml.dom.convertToList(document.getElementById("pseudo-list"), "ul"); 1.7628 + * </script> 1.7629 + * 1.7630 + * <!-- Will result in: --> 1.7631 + * <ul> 1.7632 + * <li>eminem</li> 1.7633 + * <li>dr. dre</li> 1.7634 + * <li>50 Cent</li> 1.7635 + * </ul> 1.7636 + */ 1.7637 +wysihtml.dom.convertToList = (function() { 1.7638 + function _createListItem(doc, list) { 1.7639 + var listItem = doc.createElement("li"); 1.7640 + list.appendChild(listItem); 1.7641 + return listItem; 1.7642 + } 1.7643 + 1.7644 + function _createList(doc, type) { 1.7645 + return doc.createElement(type); 1.7646 + } 1.7647 + 1.7648 + function convertToList(element, listType, uneditableClass) { 1.7649 + if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { 1.7650 + // Already a list 1.7651 + return element; 1.7652 + } 1.7653 + 1.7654 + var doc = element.ownerDocument, 1.7655 + list = _createList(doc, listType), 1.7656 + lineBreaks = element.querySelectorAll("br"), 1.7657 + lineBreaksLength = lineBreaks.length, 1.7658 + childNodes, 1.7659 + childNodesLength, 1.7660 + childNode, 1.7661 + lineBreak, 1.7662 + parentNode, 1.7663 + isBlockElement, 1.7664 + isLineBreak, 1.7665 + currentListItem, 1.7666 + i; 1.7667 + 1.7668 + // First find <br> at the end of inline elements and move them behind them 1.7669 + for (i=0; i<lineBreaksLength; i++) { 1.7670 + lineBreak = lineBreaks[i]; 1.7671 + while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) { 1.7672 + if (wysihtml.dom.getStyle("display").from(parentNode) === "block") { 1.7673 + parentNode.removeChild(lineBreak); 1.7674 + break; 1.7675 + } 1.7676 + wysihtml.dom.insert(lineBreak).after(lineBreak.parentNode); 1.7677 + } 1.7678 + } 1.7679 + 1.7680 + childNodes = wysihtml.lang.array(element.childNodes).get(); 1.7681 + childNodesLength = childNodes.length; 1.7682 + 1.7683 + for (i=0; i<childNodesLength; i++) { 1.7684 + currentListItem = currentListItem || _createListItem(doc, list); 1.7685 + childNode = childNodes[i]; 1.7686 + isBlockElement = wysihtml.dom.getStyle("display").from(childNode) === "block"; 1.7687 + isLineBreak = childNode.nodeName === "BR"; 1.7688 + 1.7689 + // consider uneditable as an inline element 1.7690 + if (isBlockElement && (!uneditableClass || !wysihtml.dom.hasClass(childNode, uneditableClass))) { 1.7691 + // Append blockElement to current <li> if empty, otherwise create a new one 1.7692 + currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; 1.7693 + currentListItem.appendChild(childNode); 1.7694 + currentListItem = null; 1.7695 + continue; 1.7696 + } 1.7697 + 1.7698 + if (isLineBreak) { 1.7699 + // Only create a new list item in the next iteration when the current one has already content 1.7700 + currentListItem = currentListItem.firstChild ? null : currentListItem; 1.7701 + continue; 1.7702 + } 1.7703 + 1.7704 + currentListItem.appendChild(childNode); 1.7705 + } 1.7706 + 1.7707 + if (childNodes.length === 0) { 1.7708 + _createListItem(doc, list); 1.7709 + } 1.7710 + 1.7711 + element.parentNode.replaceChild(list, element); 1.7712 + return list; 1.7713 + } 1.7714 + 1.7715 + return convertToList; 1.7716 +})(); 1.7717 + 1.7718 +/** 1.7719 + * Copy a set of attributes from one element to another 1.7720 + * 1.7721 + * @param {Array} attributesToCopy List of attributes which should be copied 1.7722 + * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to 1.7723 + * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked 1.7724 + * with the element where to copy the attributes to (see example) 1.7725 + * 1.7726 + * @example 1.7727 + * var textarea = document.querySelector("textarea"), 1.7728 + * div = document.querySelector("div[contenteditable=true]"), 1.7729 + * anotherDiv = document.querySelector("div.preview"); 1.7730 + * wysihtml.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); 1.7731 + * 1.7732 + */ 1.7733 +wysihtml.dom.copyAttributes = function(attributesToCopy) { 1.7734 + return { 1.7735 + from: function(elementToCopyFrom) { 1.7736 + return { 1.7737 + to: function pasteElementAttributesTo(elementToCopyTo) { 1.7738 + var attribute, 1.7739 + i = 0, 1.7740 + length = attributesToCopy.length; 1.7741 + for (; i<length; i++) { 1.7742 + attribute = attributesToCopy[i]; 1.7743 + if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") { 1.7744 + elementToCopyTo[attribute] = elementToCopyFrom[attribute]; 1.7745 + } 1.7746 + } 1.7747 + return { andTo: pasteElementAttributesTo }; 1.7748 + } 1.7749 + }; 1.7750 + } 1.7751 + }; 1.7752 +}; 1.7753 + 1.7754 +/** 1.7755 + * Copy a set of styles from one element to another 1.7756 + * Please note that this only works properly across browsers when the element from which to copy the styles 1.7757 + * is in the dom 1.7758 + * 1.7759 + * Interesting article on how to copy styles 1.7760 + * 1.7761 + * @param {Array} stylesToCopy List of styles which should be copied 1.7762 + * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to 1.7763 + * copy the styles from., this again returns an object which provides a method named "to" which can be invoked 1.7764 + * with the element where to copy the styles to (see example) 1.7765 + * 1.7766 + * @example 1.7767 + * var textarea = document.querySelector("textarea"), 1.7768 + * div = document.querySelector("div[contenteditable=true]"), 1.7769 + * anotherDiv = document.querySelector("div.preview"); 1.7770 + * wysihtml.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); 1.7771 + * 1.7772 + */ 1.7773 +(function(dom) { 1.7774 + 1.7775 + /** 1.7776 + * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set 1.7777 + * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then 1.7778 + * its computed css width will be 198px 1.7779 + * 1.7780 + * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992 1.7781 + */ 1.7782 + var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; 1.7783 + 1.7784 + var shouldIgnoreBoxSizingBorderBox = function(element) { 1.7785 + if (hasBoxSizingBorderBox(element)) { 1.7786 + return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth; 1.7787 + } 1.7788 + return false; 1.7789 + }; 1.7790 + 1.7791 + var hasBoxSizingBorderBox = function(element) { 1.7792 + var i = 0, 1.7793 + length = BOX_SIZING_PROPERTIES.length; 1.7794 + for (; i<length; i++) { 1.7795 + if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") { 1.7796 + return BOX_SIZING_PROPERTIES[i]; 1.7797 + } 1.7798 + } 1.7799 + }; 1.7800 + 1.7801 + dom.copyStyles = function(stylesToCopy) { 1.7802 + return { 1.7803 + from: function(element) { 1.7804 + if (shouldIgnoreBoxSizingBorderBox(element)) { 1.7805 + stylesToCopy = wysihtml.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); 1.7806 + } 1.7807 + 1.7808 + var cssText = "", 1.7809 + length = stylesToCopy.length, 1.7810 + i = 0, 1.7811 + property; 1.7812 + for (; i<length; i++) { 1.7813 + property = stylesToCopy[i]; 1.7814 + cssText += property + ":" + dom.getStyle(property).from(element) + ";"; 1.7815 + } 1.7816 + 1.7817 + return { 1.7818 + to: function pasteStylesTo(element) { 1.7819 + dom.setStyles(cssText).on(element); 1.7820 + return { andTo: pasteStylesTo }; 1.7821 + } 1.7822 + }; 1.7823 + } 1.7824 + }; 1.7825 + }; 1.7826 +})(wysihtml.dom); 1.7827 + 1.7828 +/** 1.7829 + * Event Delegation 1.7830 + * 1.7831 + * @example 1.7832 + * wysihtml.dom.delegate(document.body, "a", "click", function() { 1.7833 + * // foo 1.7834 + * }); 1.7835 + */ 1.7836 +(function(wysihtml) { 1.7837 + wysihtml.dom.delegate = function(container, selector, eventName, handler) { 1.7838 + var callback = function(event) { 1.7839 + var target = event.target, 1.7840 + element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes 1.7841 + matches = container.querySelectorAll(selector); 1.7842 + 1.7843 + for (var i = 0, max = matches.length; i < max; i++) { 1.7844 + if (matches[i].contains(element)) { 1.7845 + handler.call(matches[i], event); 1.7846 + } 1.7847 + } 1.7848 + }; 1.7849 + 1.7850 + container.addEventListener(eventName, callback, false); 1.7851 + return { 1.7852 + stop: function() { 1.7853 + container.removeEventListener(eventName, callback, false); 1.7854 + } 1.7855 + }; 1.7856 + }; 1.7857 +})(wysihtml); 1.7858 + 1.7859 +// TODO: Refactor dom tree traversing here 1.7860 +(function(wysihtml) { 1.7861 + 1.7862 + // Finds parents of a node, returning the outermost node first in Array 1.7863 + // if contain node is given parents search is stopped at the container 1.7864 + function parents(node, container) { 1.7865 + var nodes = [node], n = node; 1.7866 + 1.7867 + // iterate parents while parent exists and it is not container element 1.7868 + while((container && n && n !== container) || (!container && n)) { 1.7869 + nodes.unshift(n); 1.7870 + n = n.parentNode; 1.7871 + } 1.7872 + return nodes; 1.7873 + } 1.7874 + 1.7875 + wysihtml.dom.domNode = function(node) { 1.7876 + var defaultNodeTypes = [wysihtml.ELEMENT_NODE, wysihtml.TEXT_NODE]; 1.7877 + 1.7878 + return { 1.7879 + 1.7880 + is: { 1.7881 + emptyTextNode: function(ignoreWhitespace) { 1.7882 + var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g); 1.7883 + return node && node.nodeType === wysihtml.TEXT_NODE && (regx).test(node.data); 1.7884 + }, 1.7885 + 1.7886 + // 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) 1.7887 + rangyBookmark: function() { 1.7888 + return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'); 1.7889 + }, 1.7890 + 1.7891 + visible: function() { 1.7892 + var isVisible = !(/^\s*$/g).test(wysihtml.dom.getTextContent(node)); 1.7893 + 1.7894 + if (!isVisible) { 1.7895 + if (node.nodeType === 1 && node.querySelector('img, br, hr, object, embed, canvas, input, textarea')) { 1.7896 + isVisible = true; 1.7897 + } 1.7898 + } 1.7899 + return isVisible; 1.7900 + }, 1.7901 + lineBreak: function() { 1.7902 + return node && node.nodeType === 1 && node.nodeName === "BR"; 1.7903 + }, 1.7904 + block: function() { 1.7905 + return node && node.nodeType === 1 && node.ownerDocument.defaultView.getComputedStyle(node).display === "block"; 1.7906 + }, 1.7907 + // Void elements are elemens that can not have content 1.7908 + // In most cases browsers should solve the cases for you when you try to insert content into those, 1.7909 + // but IE does not and it is not nice to do so anyway. 1.7910 + voidElement: function() { 1.7911 + return wysihtml.dom.domNode(node).test({ 1.7912 + query: wysihtml.VOID_ELEMENTS 1.7913 + }); 1.7914 + } 1.7915 + }, 1.7916 + 1.7917 + // var node = wysihtml.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.7918 + prev: function(options) { 1.7919 + var prevNode = node.previousSibling, 1.7920 + types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes; 1.7921 + 1.7922 + if (!prevNode) { 1.7923 + return null; 1.7924 + } 1.7925 + 1.7926 + if ( 1.7927 + wysihtml.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) 1.7928 + (!wysihtml.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check. 1.7929 + (options && options.ignoreBlankTexts && wysihtml.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set 1.7930 + ) { 1.7931 + return wysihtml.dom.domNode(prevNode).prev(options); 1.7932 + } 1.7933 + 1.7934 + return prevNode; 1.7935 + }, 1.7936 + 1.7937 + // var node = wysihtml.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.7938 + next: function(options) { 1.7939 + var nextNode = node.nextSibling, 1.7940 + types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes; 1.7941 + 1.7942 + if (!nextNode) { 1.7943 + return null; 1.7944 + } 1.7945 + 1.7946 + if ( 1.7947 + wysihtml.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) 1.7948 + (!wysihtml.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check. 1.7949 + (options && options.ignoreBlankTexts && wysihtml.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set 1.7950 + ) { 1.7951 + return wysihtml.dom.domNode(nextNode).next(options); 1.7952 + } 1.7953 + 1.7954 + return nextNode; 1.7955 + }, 1.7956 + 1.7957 + // Finds the common acnestor container of two nodes 1.7958 + // If container given stops search at the container 1.7959 + // If no common ancestor found returns null 1.7960 + // var node = wysihtml.dom.domNode(element).commonAncestor(node2, container); 1.7961 + commonAncestor: function(node2, container) { 1.7962 + var parents1 = parents(node, container), 1.7963 + parents2 = parents(node2, container); 1.7964 + 1.7965 + // Ensure we have found a common ancestor, which will be the first one if anything 1.7966 + if (parents1[0] != parents2[0]) { 1.7967 + return null; 1.7968 + } 1.7969 + 1.7970 + // Traverse up the hierarchy of parents until we reach where they're no longer 1.7971 + // the same. Then return previous which was the common ancestor. 1.7972 + for (var i = 0; i < parents1.length; i++) { 1.7973 + if (parents1[i] != parents2[i]) { 1.7974 + return parents1[i - 1]; 1.7975 + } 1.7976 + } 1.7977 + 1.7978 + return null; 1.7979 + }, 1.7980 + 1.7981 + // Traverses a node for last children and their chidren (including itself), and finds the last node that has no children. 1.7982 + // Array of classes for forced last-leaves (ex: uneditable-container) can be defined (options = {leafClasses: [...]}) 1.7983 + // Useful for finding the actually visible element before cursor 1.7984 + lastLeafNode: function(options) { 1.7985 + var lastChild; 1.7986 + 1.7987 + // Returns non-element nodes 1.7988 + if (node.nodeType !== 1) { 1.7989 + return node; 1.7990 + } 1.7991 + 1.7992 + // Returns if element is leaf 1.7993 + lastChild = node.lastChild; 1.7994 + if (!lastChild) { 1.7995 + return node; 1.7996 + } 1.7997 + 1.7998 + // Returns if element is of of options.leafClasses leaf 1.7999 + if (options && options.leafClasses) { 1.8000 + for (var i = options.leafClasses.length; i--;) { 1.8001 + if (wysihtml.dom.hasClass(node, options.leafClasses[i])) { 1.8002 + return node; 1.8003 + } 1.8004 + } 1.8005 + } 1.8006 + 1.8007 + return wysihtml.dom.domNode(lastChild).lastLeafNode(options); 1.8008 + }, 1.8009 + 1.8010 + // Splits element at childnode and extracts the childNode out of the element context 1.8011 + // Example: 1.8012 + // var node = wysihtml.dom.domNode(node).escapeParent(parentNode); 1.8013 + escapeParent: function(element, newWrapper) { 1.8014 + var parent, split2, nodeWrap, 1.8015 + curNode = node; 1.8016 + 1.8017 + // Stop if node is not a descendant of element 1.8018 + if (!wysihtml.dom.contains(element, node)) { 1.8019 + throw new Error("Child is not a descendant of node."); 1.8020 + } 1.8021 + 1.8022 + // Climb up the node tree untill node is reached 1.8023 + do { 1.8024 + // Get current parent of node 1.8025 + parent = curNode.parentNode; 1.8026 + 1.8027 + // Move after nodes to new clone wrapper 1.8028 + split2 = parent.cloneNode(false); 1.8029 + while (parent.lastChild && parent.lastChild !== curNode) { 1.8030 + split2.insertBefore(parent.lastChild, split2.firstChild); 1.8031 + } 1.8032 + 1.8033 + // Move node up a level. If parent is not yet the container to escape, clone the parent around node, so inner nodes are escaped out too 1.8034 + if (parent !== element) { 1.8035 + nodeWrap = parent.cloneNode(false); 1.8036 + nodeWrap.appendChild(curNode); 1.8037 + curNode = nodeWrap; 1.8038 + } 1.8039 + parent.parentNode.insertBefore(curNode, parent.nextSibling); 1.8040 + 1.8041 + // Add after nodes (unless empty) 1.8042 + if (split2.innerHTML !== '') { 1.8043 + // if contents are empty insert without wrap 1.8044 + if ((/^\s+$/).test(split2.innerHTML)) { 1.8045 + while (split2.lastChild) { 1.8046 + parent.parentNode.insertBefore(split2.lastChild, curNode.nextSibling); 1.8047 + } 1.8048 + } else { 1.8049 + parent.parentNode.insertBefore(split2, curNode.nextSibling); 1.8050 + } 1.8051 + } 1.8052 + 1.8053 + // If the node left behind before the split (parent) is now empty then remove 1.8054 + if (parent.innerHTML === '') { 1.8055 + parent.parentNode.removeChild(parent); 1.8056 + } else if ((/^\s+$/).test(parent.innerHTML)) { 1.8057 + while (parent.firstChild) { 1.8058 + parent.parentNode.insertBefore(parent.firstChild, parent); 1.8059 + } 1.8060 + parent.parentNode.removeChild(parent); 1.8061 + } 1.8062 + 1.8063 + } while (parent && parent !== element); 1.8064 + 1.8065 + if (newWrapper && curNode) { 1.8066 + curNode.parentNode.insertBefore(newWrapper, curNode); 1.8067 + newWrapper.appendChild(curNode); 1.8068 + } 1.8069 + }, 1.8070 + 1.8071 + transferContentTo: function(targetNode, removeOldWrapper) { 1.8072 + if (node.nodeType === 1) { 1.8073 + if (wysihtml.dom.domNode(targetNode).is.voidElement() || targetNode.nodeType === 3) { 1.8074 + while (node.lastChild) { 1.8075 + targetNode.parentNode.insertBefore(node.lastChild, targetNode.nextSibling); 1.8076 + } 1.8077 + } else { 1.8078 + while (node.firstChild) { 1.8079 + targetNode.appendChild(node.firstChild); 1.8080 + } 1.8081 + } 1.8082 + if (removeOldWrapper) { 1.8083 + node.parentNode.removeChild(node); 1.8084 + } 1.8085 + } else if (node.nodeType === 3 || node.nodeType === 8){ 1.8086 + if (wysihtml.dom.domNode(targetNode).is.voidElement()) { 1.8087 + targetNode.parentNode.insertBefore(node, targetNode.nextSibling); 1.8088 + } else { 1.8089 + targetNode.appendChild(node); 1.8090 + } 1.8091 + } 1.8092 + }, 1.8093 + 1.8094 + /* 1.8095 + Tests a node against properties, and returns true if matches. 1.8096 + Tests on principle that all properties defined must have at least one match. 1.8097 + styleValue parameter works in context of styleProperty and has no effect otherwise. 1.8098 + Returns true if element matches and false if it does not. 1.8099 + 1.8100 + Properties for filtering element: 1.8101 + { 1.8102 + query: selector string, 1.8103 + nodeName: string (uppercase), 1.8104 + className: string, 1.8105 + classRegExp: regex, 1.8106 + styleProperty: string or [], 1.8107 + styleValue: string, [] or regex 1.8108 + } 1.8109 + 1.8110 + Example: 1.8111 + var node = wysihtml.dom.domNode(element).test({}) 1.8112 + */ 1.8113 + test: function(properties) { 1.8114 + var prop; 1.8115 + 1.8116 + // return false if properties object is not defined 1.8117 + if (!properties) { 1.8118 + return false; 1.8119 + } 1.8120 + 1.8121 + // Only element nodes can be tested for these properties 1.8122 + if (node.nodeType !== 1) { 1.8123 + return false; 1.8124 + } 1.8125 + 1.8126 + if (properties.query) { 1.8127 + if (!node.matches(properties.query)) { 1.8128 + return false; 1.8129 + } 1.8130 + } 1.8131 + 1.8132 + if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) { 1.8133 + return false; 1.8134 + } 1.8135 + 1.8136 + if (properties.className && !node.classList.contains(properties.className)) { 1.8137 + return false; 1.8138 + } 1.8139 + 1.8140 + // classRegExp check (useful for classname begins with logic) 1.8141 + if (properties.classRegExp) { 1.8142 + var matches = (node.className || "").match(properties.classRegExp) || []; 1.8143 + if (matches.length === 0) { 1.8144 + return false; 1.8145 + } 1.8146 + } 1.8147 + 1.8148 + // styleProperty check 1.8149 + if (properties.styleProperty && properties.styleProperty.length > 0) { 1.8150 + var hasOneStyle = false, 1.8151 + styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty]; 1.8152 + for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) { 1.8153 + // Some old IE-s have different property name for cssFloat 1.8154 + prop = wysihtml.browser.fixStyleKey(styles[j]); 1.8155 + if (node.style[prop]) { 1.8156 + if (properties.styleValue) { 1.8157 + // Style value as additional parameter 1.8158 + if (properties.styleValue instanceof RegExp) { 1.8159 + // style value as Regexp 1.8160 + if (node.style[prop].trim().match(properties.styleValue).length > 0) { 1.8161 + hasOneStyle = true; 1.8162 + break; 1.8163 + } 1.8164 + } else if (Array.isArray(properties.styleValue)) { 1.8165 + // style value as array 1.8166 + if (properties.styleValue.indexOf(node.style[prop].trim())) { 1.8167 + hasOneStyle = true; 1.8168 + break; 1.8169 + } 1.8170 + } else { 1.8171 + // style value as string 1.8172 + if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) { 1.8173 + hasOneStyle = true; 1.8174 + break; 1.8175 + } 1.8176 + } 1.8177 + } else { 1.8178 + hasOneStyle = true; 1.8179 + break; 1.8180 + } 1.8181 + } 1.8182 + if (!hasOneStyle) { 1.8183 + return false; 1.8184 + } 1.8185 + } 1.8186 + } 1.8187 + 1.8188 + if (properties.attribute) { 1.8189 + var attr = wysihtml.dom.getAttributes(node), 1.8190 + attrList = [], 1.8191 + hasOneAttribute = false; 1.8192 + 1.8193 + if (Array.isArray(properties.attribute)) { 1.8194 + attrList = properties.attribute; 1.8195 + } else { 1.8196 + attrList[properties.attribute] = properties.attributeValue; 1.8197 + } 1.8198 + 1.8199 + for (var a in attrList) { 1.8200 + if (attrList.hasOwnProperty(a)) { 1.8201 + if (typeof attrList[a] === "undefined") { 1.8202 + if (typeof attr[a] !== "undefined") { 1.8203 + hasOneAttribute = true; 1.8204 + break; 1.8205 + } 1.8206 + } else if (attr[a] === attrList[a]) { 1.8207 + hasOneAttribute = true; 1.8208 + break; 1.8209 + } 1.8210 + } 1.8211 + } 1.8212 + 1.8213 + if (!hasOneAttribute) { 1.8214 + return false; 1.8215 + } 1.8216 + 1.8217 + } 1.8218 + 1.8219 + return true; 1.8220 + } 1.8221 + 1.8222 + }; 1.8223 + }; 1.8224 +})(wysihtml); 1.8225 + 1.8226 +/** 1.8227 + * Returns the given html wrapped in a div element 1.8228 + * 1.8229 + * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly 1.8230 + * when inserted via innerHTML 1.8231 + * 1.8232 + * @param {String} html The html which should be wrapped in a dom element 1.8233 + * @param {Obejct} [context] Document object of the context the html belongs to 1.8234 + * 1.8235 + * @example 1.8236 + * wysihtml.dom.getAsDom("<article>foo</article>"); 1.8237 + */ 1.8238 +wysihtml.dom.getAsDom = (function() { 1.8239 + 1.8240 + var _innerHTMLShiv = function(html, context) { 1.8241 + var tempElement = context.createElement("div"); 1.8242 + tempElement.style.display = "none"; 1.8243 + context.body.appendChild(tempElement); 1.8244 + // IE throws an exception when trying to insert <frameset></frameset> via innerHTML 1.8245 + try { tempElement.innerHTML = html; } catch(e) {} 1.8246 + context.body.removeChild(tempElement); 1.8247 + return tempElement; 1.8248 + }; 1.8249 + 1.8250 + /** 1.8251 + * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element 1.8252 + */ 1.8253 + var _ensureHTML5Compatibility = function(context) { 1.8254 + if (context._wysihtml_supportsHTML5Tags) { 1.8255 + return; 1.8256 + } 1.8257 + for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) { 1.8258 + context.createElement(HTML5_ELEMENTS[i]); 1.8259 + } 1.8260 + context._wysihtml_supportsHTML5Tags = true; 1.8261 + }; 1.8262 + 1.8263 + 1.8264 + /** 1.8265 + * List of html5 tags 1.8266 + * taken from http://simon.html5.org/html5-elements 1.8267 + */ 1.8268 + var HTML5_ELEMENTS = [ 1.8269 + "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", 1.8270 + "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", 1.8271 + "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" 1.8272 + ]; 1.8273 + 1.8274 + return function(html, context) { 1.8275 + context = context || document; 1.8276 + var tempElement; 1.8277 + if (typeof(html) === "object" && html.nodeType) { 1.8278 + tempElement = context.createElement("div"); 1.8279 + tempElement.appendChild(html); 1.8280 + } else if (wysihtml.browser.supportsHTML5Tags(context)) { 1.8281 + tempElement = context.createElement("div"); 1.8282 + tempElement.innerHTML = html; 1.8283 + } else { 1.8284 + _ensureHTML5Compatibility(context); 1.8285 + tempElement = _innerHTMLShiv(html, context); 1.8286 + } 1.8287 + return tempElement; 1.8288 + }; 1.8289 +})(); 1.8290 + 1.8291 +/** 1.8292 + * Get a set of attribute from one element 1.8293 + * 1.8294 + * IE gives wrong results for hasAttribute/getAttribute, for example: 1.8295 + * var td = document.createElement("td"); 1.8296 + * td.getAttribute("rowspan"); // => "1" in IE 1.8297 + * 1.8298 + * Therefore we have to check the element's outerHTML for the attribute 1.8299 +*/ 1.8300 + 1.8301 +wysihtml.dom.getAttribute = function(node, attributeName) { 1.8302 + var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(); 1.8303 + attributeName = attributeName.toLowerCase(); 1.8304 + var nodeName = node.nodeName; 1.8305 + if (nodeName == "IMG" && attributeName == "src" && wysihtml.dom.isLoadedImage(node) === true) { 1.8306 + // Get 'src' attribute value via object property since this will always contain the 1.8307 + // full absolute url (http://...) 1.8308 + // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host 1.8309 + // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) 1.8310 + return node.src; 1.8311 + } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { 1.8312 + // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML 1.8313 + var outerHTML = node.outerHTML.toLowerCase(), 1.8314 + // TODO: This might not work for attributes without value: <input disabled> 1.8315 + hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; 1.8316 + 1.8317 + return hasAttribute ? node.getAttribute(attributeName) : null; 1.8318 + } else{ 1.8319 + return node.getAttribute(attributeName); 1.8320 + } 1.8321 +}; 1.8322 + 1.8323 +/** 1.8324 + * Get all attributes of an element 1.8325 + * 1.8326 + * IE gives wrong results for hasAttribute/getAttribute, for example: 1.8327 + * var td = document.createElement("td"); 1.8328 + * td.getAttribute("rowspan"); // => "1" in IE 1.8329 + * 1.8330 + * Therefore we have to check the element's outerHTML for the attribute 1.8331 +*/ 1.8332 + 1.8333 +wysihtml.dom.getAttributes = function(node) { 1.8334 + var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(), 1.8335 + nodeName = node.nodeName, 1.8336 + attributes = [], 1.8337 + attr; 1.8338 + 1.8339 + for (attr in node.attributes) { 1.8340 + if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr))) { 1.8341 + if (node.attributes[attr].specified) { 1.8342 + if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml.dom.isLoadedImage(node) === true) { 1.8343 + attributes['src'] = node.src; 1.8344 + } else if (wysihtml.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) { 1.8345 + if (node.attributes[attr].value !== 1) { 1.8346 + attributes[node.attributes[attr].name] = node.attributes[attr].value; 1.8347 + } 1.8348 + } else { 1.8349 + attributes[node.attributes[attr].name] = node.attributes[attr].value; 1.8350 + } 1.8351 + } 1.8352 + } 1.8353 + } 1.8354 + return attributes; 1.8355 +}; 1.8356 + 1.8357 +/** 1.8358 + * Walks the dom tree from the given node up until it finds a match 1.8359 + * 1.8360 + * @param {Element} node The from which to check the parent nodes 1.8361 + * @param {Object} matchingSet Object to match against, Properties for filtering element: 1.8362 + * { 1.8363 + * query: selector string, 1.8364 + * classRegExp: regex, 1.8365 + * styleProperty: string or [], 1.8366 + * styleValue: string, [] or regex 1.8367 + * } 1.8368 + * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50) 1.8369 + * @param {Element} Optional, defines the container that limits the search 1.8370 + * 1.8371 + * @return {null|Element} Returns the first element that matched the desiredNodeName(s) 1.8372 +*/ 1.8373 + 1.8374 +wysihtml.dom.getParentElement = (function() { 1.8375 + 1.8376 + return function(node, properties, levels, container) { 1.8377 + levels = levels || 50; 1.8378 + while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) { 1.8379 + if (wysihtml.dom.domNode(node).test(properties)) { 1.8380 + return node; 1.8381 + } 1.8382 + node = node.parentNode; 1.8383 + } 1.8384 + return null; 1.8385 + }; 1.8386 + 1.8387 +})(); 1.8388 + 1.8389 +/* 1.8390 + * Methods for fetching pasted html before it gets inserted into content 1.8391 +**/ 1.8392 + 1.8393 +/* Modern event.clipboardData driven approach. 1.8394 + * Advantage is that it does not have to loose selection or modify dom to catch the data. 1.8395 + * IE does not support though. 1.8396 +**/ 1.8397 +wysihtml.dom.getPastedHtml = function(event) { 1.8398 + var html; 1.8399 + if (wysihtml.browser.supportsModernPaste() && event.clipboardData) { 1.8400 + if (wysihtml.lang.array(event.clipboardData.types).contains('text/html')) { 1.8401 + html = event.clipboardData.getData('text/html'); 1.8402 + } else if (wysihtml.lang.array(event.clipboardData.types).contains('text/plain')) { 1.8403 + html = wysihtml.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true); 1.8404 + } 1.8405 + } 1.8406 + return html; 1.8407 +}; 1.8408 + 1.8409 +/* Older temprorary contenteditable as paste source catcher method for fallbacks */ 1.8410 +wysihtml.dom.getPastedHtmlWithDiv = function (composer, f) { 1.8411 + var selBookmark = composer.selection.getBookmark(), 1.8412 + doc = composer.element.ownerDocument, 1.8413 + cleanerDiv = doc.createElement('DIV'), 1.8414 + scrollPos = composer.getScrollPos(); 1.8415 + 1.8416 + doc.body.appendChild(cleanerDiv); 1.8417 + 1.8418 + cleanerDiv.style.width = "1px"; 1.8419 + cleanerDiv.style.height = "1px"; 1.8420 + cleanerDiv.style.overflow = "hidden"; 1.8421 + cleanerDiv.style.position = "absolute"; 1.8422 + cleanerDiv.style.top = scrollPos.y + "px"; 1.8423 + cleanerDiv.style.left = scrollPos.x + "px"; 1.8424 + 1.8425 + cleanerDiv.setAttribute('contenteditable', 'true'); 1.8426 + cleanerDiv.focus(); 1.8427 + 1.8428 + setTimeout(function () { 1.8429 + var html; 1.8430 + 1.8431 + composer.selection.setBookmark(selBookmark); 1.8432 + html = cleanerDiv.innerHTML; 1.8433 + if (html && (/^<br\/?>$/i).test(html.trim())) { 1.8434 + html = false; 1.8435 + } 1.8436 + f(html); 1.8437 + cleanerDiv.parentNode.removeChild(cleanerDiv); 1.8438 + }, 0); 1.8439 +}; 1.8440 + 1.8441 +/** 1.8442 + * Get element's style for a specific css property 1.8443 + * 1.8444 + * @param {Element} element The element on which to retrieve the style 1.8445 + * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) 1.8446 + * 1.8447 + * @example 1.8448 + * wysihtml.dom.getStyle("display").from(document.body); 1.8449 + * // => "block" 1.8450 + */ 1.8451 +wysihtml.dom.getStyle = (function() { 1.8452 + var stylePropertyMapping = { 1.8453 + "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" 1.8454 + }, 1.8455 + REG_EXP_CAMELIZE = /\-[a-z]/g; 1.8456 + 1.8457 + function camelize(str) { 1.8458 + return str.replace(REG_EXP_CAMELIZE, function(match) { 1.8459 + return match.charAt(1).toUpperCase(); 1.8460 + }); 1.8461 + } 1.8462 + 1.8463 + return function(property) { 1.8464 + return { 1.8465 + from: function(element) { 1.8466 + if (element.nodeType !== wysihtml.ELEMENT_NODE) { 1.8467 + return; 1.8468 + } 1.8469 + 1.8470 + var doc = element.ownerDocument, 1.8471 + camelizedProperty = stylePropertyMapping[property] || camelize(property), 1.8472 + style = element.style, 1.8473 + currentStyle = element.currentStyle, 1.8474 + styleValue = style[camelizedProperty]; 1.8475 + if (styleValue) { 1.8476 + return styleValue; 1.8477 + } 1.8478 + 1.8479 + // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant 1.8480 + // window.getComputedStyle, since it returns css property values in their original unit: 1.8481 + // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle 1.8482 + // gives you the original "50%". 1.8483 + // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio 1.8484 + if (currentStyle) { 1.8485 + try { 1.8486 + return currentStyle[camelizedProperty]; 1.8487 + } catch(e) { 1.8488 + //ie will occasionally fail for unknown reasons. swallowing exception 1.8489 + } 1.8490 + } 1.8491 + 1.8492 + var win = doc.defaultView || doc.parentWindow, 1.8493 + needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA", 1.8494 + originalOverflow, 1.8495 + returnValue; 1.8496 + 1.8497 + if (win.getComputedStyle) { 1.8498 + // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars 1.8499 + // therfore we remove and restore the scrollbar and calculate the value in between 1.8500 + if (needsOverflowReset) { 1.8501 + originalOverflow = style.overflow; 1.8502 + style.overflow = "hidden"; 1.8503 + } 1.8504 + returnValue = win.getComputedStyle(element, null).getPropertyValue(property); 1.8505 + if (needsOverflowReset) { 1.8506 + style.overflow = originalOverflow || ""; 1.8507 + } 1.8508 + return returnValue; 1.8509 + } 1.8510 + } 1.8511 + }; 1.8512 + }; 1.8513 +})(); 1.8514 + 1.8515 +wysihtml.dom.getTextNodes = function(node, ingoreEmpty){ 1.8516 + var all = []; 1.8517 + for (node=node.firstChild;node;node=node.nextSibling){ 1.8518 + if (node.nodeType == 3) { 1.8519 + if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) { 1.8520 + all.push(node); 1.8521 + } 1.8522 + } else { 1.8523 + all = all.concat(wysihtml.dom.getTextNodes(node, ingoreEmpty)); 1.8524 + } 1.8525 + } 1.8526 + return all; 1.8527 +}; 1.8528 + 1.8529 +/** 1.8530 + * High performant way to check whether an element with a specific class name is in the given document 1.8531 + * Optimized for being heavily executed 1.8532 + * Unleashes the power of live node lists 1.8533 + * 1.8534 + * @param {Object} doc The document object of the context where to check 1.8535 + * @param {String} tagName Upper cased tag name 1.8536 + * @example 1.8537 + * wysihtml.dom.hasElementWithClassName(document, "foobar"); 1.8538 + */ 1.8539 +(function(wysihtml) { 1.8540 + var LIVE_CACHE = {}, 1.8541 + DOCUMENT_IDENTIFIER = 1; 1.8542 + 1.8543 + function _getDocumentIdentifier(doc) { 1.8544 + return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++); 1.8545 + } 1.8546 + 1.8547 + wysihtml.dom.hasElementWithClassName = function(doc, className) { 1.8548 + // getElementsByClassName is not supported by IE<9 1.8549 + // but is sometimes mocked via library code (which then doesn't return live node lists) 1.8550 + if (!wysihtml.browser.supportsNativeGetElementsByClassName()) { 1.8551 + return !!doc.querySelector("." + className); 1.8552 + } 1.8553 + 1.8554 + var key = _getDocumentIdentifier(doc) + ":" + className, 1.8555 + cacheEntry = LIVE_CACHE[key]; 1.8556 + if (!cacheEntry) { 1.8557 + cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); 1.8558 + } 1.8559 + 1.8560 + return cacheEntry.length > 0; 1.8561 + }; 1.8562 +})(wysihtml); 1.8563 + 1.8564 +/** 1.8565 + * High performant way to check whether an element with a specific tag name is in the given document 1.8566 + * Optimized for being heavily executed 1.8567 + * Unleashes the power of live node lists 1.8568 + * 1.8569 + * @param {Object} doc The document object of the context where to check 1.8570 + * @param {String} tagName Upper cased tag name 1.8571 + * @example 1.8572 + * wysihtml.dom.hasElementWithTagName(document, "IMG"); 1.8573 + */ 1.8574 +wysihtml.dom.hasElementWithTagName = (function() { 1.8575 + var LIVE_CACHE = {}, 1.8576 + DOCUMENT_IDENTIFIER = 1; 1.8577 + 1.8578 + function _getDocumentIdentifier(doc) { 1.8579 + return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++); 1.8580 + } 1.8581 + 1.8582 + return function(doc, tagName) { 1.8583 + var key = _getDocumentIdentifier(doc) + ":" + tagName, 1.8584 + cacheEntry = LIVE_CACHE[key]; 1.8585 + if (!cacheEntry) { 1.8586 + cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); 1.8587 + } 1.8588 + 1.8589 + return cacheEntry.length > 0; 1.8590 + }; 1.8591 +})(); 1.8592 + 1.8593 +wysihtml.dom.insert = function(elementToInsert) { 1.8594 + return { 1.8595 + after: function(element) { 1.8596 + element.parentNode.insertBefore(elementToInsert, element.nextSibling); 1.8597 + }, 1.8598 + 1.8599 + before: function(element) { 1.8600 + element.parentNode.insertBefore(elementToInsert, element); 1.8601 + }, 1.8602 + 1.8603 + into: function(element) { 1.8604 + element.appendChild(elementToInsert); 1.8605 + } 1.8606 + }; 1.8607 +}; 1.8608 + 1.8609 +wysihtml.dom.insertCSS = function(rules) { 1.8610 + rules = rules.join("\n"); 1.8611 + 1.8612 + return { 1.8613 + into: function(doc) { 1.8614 + var styleElement = doc.createElement("style"); 1.8615 + styleElement.type = "text/css"; 1.8616 + 1.8617 + if (styleElement.styleSheet) { 1.8618 + styleElement.styleSheet.cssText = rules; 1.8619 + } else { 1.8620 + styleElement.appendChild(doc.createTextNode(rules)); 1.8621 + } 1.8622 + 1.8623 + var link = doc.querySelector("head link"); 1.8624 + if (link) { 1.8625 + link.parentNode.insertBefore(styleElement, link); 1.8626 + return; 1.8627 + } else { 1.8628 + var head = doc.querySelector("head"); 1.8629 + if (head) { 1.8630 + head.appendChild(styleElement); 1.8631 + } 1.8632 + } 1.8633 + } 1.8634 + }; 1.8635 +}; 1.8636 + 1.8637 +/** 1.8638 + * Check whether the given node is a proper loaded image 1.8639 + * FIXME: Returns undefined when unknown (Chrome, Safari) 1.8640 +*/ 1.8641 + 1.8642 +wysihtml.dom.isLoadedImage = function (node) { 1.8643 + try { 1.8644 + return node.complete && !node.mozMatchesSelector(":-moz-broken"); 1.8645 + } catch(e) { 1.8646 + if (node.complete && node.readyState === "complete") { 1.8647 + return true; 1.8648 + } 1.8649 + } 1.8650 +}; 1.8651 + 1.8652 +// TODO: Refactor dom tree traversing here 1.8653 +(function(wysihtml) { 1.8654 + wysihtml.dom.lineBreaks = function(node) { 1.8655 + 1.8656 + function _isLineBreak(n) { 1.8657 + return n.nodeName === "BR"; 1.8658 + } 1.8659 + 1.8660 + /** 1.8661 + * Checks whether the elment causes a visual line break 1.8662 + * (<br> or block elements) 1.8663 + */ 1.8664 + function _isLineBreakOrBlockElement(element) { 1.8665 + if (_isLineBreak(element)) { 1.8666 + return true; 1.8667 + } 1.8668 + 1.8669 + if (wysihtml.dom.getStyle("display").from(element) === "block") { 1.8670 + return true; 1.8671 + } 1.8672 + 1.8673 + return false; 1.8674 + } 1.8675 + 1.8676 + return { 1.8677 + 1.8678 + /* wysihtml.dom.lineBreaks(element).add(); 1.8679 + * 1.8680 + * Adds line breaks before and after the given node if the previous and next siblings 1.8681 + * aren't already causing a visual line break (block element or <br>) 1.8682 + */ 1.8683 + add: function(options) { 1.8684 + var doc = node.ownerDocument, 1.8685 + nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}), 1.8686 + previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true}); 1.8687 + 1.8688 + if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { 1.8689 + wysihtml.dom.insert(doc.createElement("br")).after(node); 1.8690 + } 1.8691 + if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { 1.8692 + wysihtml.dom.insert(doc.createElement("br")).before(node); 1.8693 + } 1.8694 + }, 1.8695 + 1.8696 + /* wysihtml.dom.lineBreaks(element).remove(); 1.8697 + * 1.8698 + * Removes line breaks before and after the given node 1.8699 + */ 1.8700 + remove: function(options) { 1.8701 + var nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}), 1.8702 + previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true}); 1.8703 + 1.8704 + if (nextSibling && _isLineBreak(nextSibling)) { 1.8705 + nextSibling.parentNode.removeChild(nextSibling); 1.8706 + } 1.8707 + if (previousSibling && _isLineBreak(previousSibling)) { 1.8708 + previousSibling.parentNode.removeChild(previousSibling); 1.8709 + } 1.8710 + } 1.8711 + }; 1.8712 + }; 1.8713 +})(wysihtml); 1.8714 +/** 1.8715 + * Method to set dom events 1.8716 + * 1.8717 + * @example 1.8718 + * wysihtml.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); 1.8719 + */ 1.8720 +wysihtml.dom.observe = function(element, eventNames, handler) { 1.8721 + eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; 1.8722 + 1.8723 + var handlerWrapper, 1.8724 + eventName, 1.8725 + i = 0, 1.8726 + length = eventNames.length; 1.8727 + 1.8728 + for (; i<length; i++) { 1.8729 + eventName = eventNames[i]; 1.8730 + if (element.addEventListener) { 1.8731 + element.addEventListener(eventName, handler, false); 1.8732 + } else { 1.8733 + handlerWrapper = function(event) { 1.8734 + if (!("target" in event)) { 1.8735 + event.target = event.srcElement; 1.8736 + } 1.8737 + event.preventDefault = event.preventDefault || function() { 1.8738 + this.returnValue = false; 1.8739 + }; 1.8740 + event.stopPropagation = event.stopPropagation || function() { 1.8741 + this.cancelBubble = true; 1.8742 + }; 1.8743 + handler.call(element, event); 1.8744 + }; 1.8745 + element.attachEvent("on" + eventName, handlerWrapper); 1.8746 + } 1.8747 + } 1.8748 + 1.8749 + return { 1.8750 + stop: function() { 1.8751 + var eventName, 1.8752 + i = 0, 1.8753 + length = eventNames.length; 1.8754 + for (; i<length; i++) { 1.8755 + eventName = eventNames[i]; 1.8756 + if (element.removeEventListener) { 1.8757 + element.removeEventListener(eventName, handler, false); 1.8758 + } else { 1.8759 + element.detachEvent("on" + eventName, handlerWrapper); 1.8760 + } 1.8761 + } 1.8762 + } 1.8763 + }; 1.8764 +}; 1.8765 + 1.8766 +/** 1.8767 + * HTML Sanitizer 1.8768 + * Rewrites the HTML based on given rules 1.8769 + * 1.8770 + * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized 1.8771 + * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will 1.8772 + * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the 1.8773 + * desired substitution. 1.8774 + * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing 1.8775 + * 1.8776 + * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element. 1.8777 + * 1.8778 + * @example 1.8779 + * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>'; 1.8780 + * wysihtml.dom.parse(userHTML, { 1.8781 + * tags { 1.8782 + * p: "div", // Rename p tags to div tags 1.8783 + * font: "span" // Rename font tags to span tags 1.8784 + * div: true, // Keep them, also possible (same result when passing: "div" or true) 1.8785 + * script: undefined // Remove script elements 1.8786 + * } 1.8787 + * }); 1.8788 + * // => <div><div><span>foo bar</span></div></div> 1.8789 + * 1.8790 + * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>'; 1.8791 + * wysihtml.dom.parse(userHTML); 1.8792 + * // => '<span><span><span><span>I'm a table!</span></span></span></span>' 1.8793 + * 1.8794 + * var userHTML = '<div>foobar<br>foobar</div>'; 1.8795 + * wysihtml.dom.parse(userHTML, { 1.8796 + * tags: { 1.8797 + * div: undefined, 1.8798 + * br: true 1.8799 + * } 1.8800 + * }); 1.8801 + * // => '' 1.8802 + * 1.8803 + * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>'; 1.8804 + * wysihtml.dom.parse(userHTML, { 1.8805 + * classes: { 1.8806 + * red: 1, 1.8807 + * green: 1 1.8808 + * }, 1.8809 + * tags: { 1.8810 + * div: { 1.8811 + * rename_tag: "p" 1.8812 + * } 1.8813 + * } 1.8814 + * }); 1.8815 + * // => '<p class="red">foo</p><p>bar</p>' 1.8816 + */ 1.8817 + 1.8818 +wysihtml.dom.parse = function(elementOrHtml_current, config_current) { 1.8819 + /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors. 1.8820 + * Refactor whole code as this method while workind is kind of awkward too */ 1.8821 + 1.8822 + /** 1.8823 + * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML 1.8824 + * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the 1.8825 + * node isn't closed 1.8826 + * 1.8827 + * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML. 1.8828 + */ 1.8829 + var NODE_TYPE_MAPPING = { 1.8830 + "1": _handleElement, 1.8831 + "3": _handleText, 1.8832 + "8": _handleComment 1.8833 + }, 1.8834 + // Rename unknown tags to this 1.8835 + DEFAULT_NODE_NAME = "span", 1.8836 + WHITE_SPACE_REG_EXP = /\s+/, 1.8837 + defaultRules = { tags: {}, classes: {} }, 1.8838 + currentRules = {}, 1.8839 + blockElements = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" , 1.8840 + "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU", 1.8841 + "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"]; 1.8842 + 1.8843 + /** 1.8844 + * Iterates over all childs of the element, recreates them, appends them into a document fragment 1.8845 + * which later replaces the entire body content 1.8846 + */ 1.8847 + function parse(elementOrHtml, config) { 1.8848 + wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get(); 1.8849 + 1.8850 + var context = config.context || elementOrHtml.ownerDocument || document, 1.8851 + fragment = context.createDocumentFragment(), 1.8852 + isString = typeof(elementOrHtml) === "string", 1.8853 + clearInternals = false, 1.8854 + element, 1.8855 + newNode, 1.8856 + firstChild; 1.8857 + 1.8858 + if (config.clearInternals === true) { 1.8859 + clearInternals = true; 1.8860 + } 1.8861 + 1.8862 + if (isString) { 1.8863 + element = wysihtml.dom.getAsDom(elementOrHtml, context); 1.8864 + } else { 1.8865 + element = elementOrHtml; 1.8866 + } 1.8867 + 1.8868 + if (currentRules.selectors) { 1.8869 + _applySelectorRules(element, currentRules.selectors); 1.8870 + } 1.8871 + 1.8872 + while (element.firstChild) { 1.8873 + firstChild = element.firstChild; 1.8874 + newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass); 1.8875 + if (newNode) { 1.8876 + fragment.appendChild(newNode); 1.8877 + } 1.8878 + if (firstChild !== newNode) { 1.8879 + element.removeChild(firstChild); 1.8880 + } 1.8881 + } 1.8882 + 1.8883 + if (config.unjoinNbsps) { 1.8884 + // replace joined non-breakable spaces with unjoined 1.8885 + var txtnodes = wysihtml.dom.getTextNodes(fragment); 1.8886 + for (var n = txtnodes.length; n--;) { 1.8887 + txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 "); 1.8888 + } 1.8889 + } 1.8890 + 1.8891 + // Clear element contents 1.8892 + element.innerHTML = ""; 1.8893 + 1.8894 + // Insert new DOM tree 1.8895 + element.appendChild(fragment); 1.8896 + 1.8897 + return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element; 1.8898 + } 1.8899 + 1.8900 + function _convert(oldNode, cleanUp, clearInternals, uneditableClass) { 1.8901 + var oldNodeType = oldNode.nodeType, 1.8902 + oldChilds = oldNode.childNodes, 1.8903 + oldChildsLength = oldChilds.length, 1.8904 + method = NODE_TYPE_MAPPING[oldNodeType], 1.8905 + i = 0, 1.8906 + fragment, 1.8907 + newNode, 1.8908 + newChild, 1.8909 + nodeDisplay; 1.8910 + 1.8911 + // Passes directly elemets with uneditable class 1.8912 + if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) { 1.8913 + return oldNode; 1.8914 + } 1.8915 + 1.8916 + newNode = method && method(oldNode, clearInternals); 1.8917 + 1.8918 + // Remove or unwrap node in case of return value null or false 1.8919 + if (!newNode) { 1.8920 + if (newNode === false) { 1.8921 + // false defines that tag should be removed but contents should remain (unwrap) 1.8922 + fragment = oldNode.ownerDocument.createDocumentFragment(); 1.8923 + 1.8924 + for (i = oldChildsLength; i--;) { 1.8925 + if (oldChilds[i]) { 1.8926 + newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass); 1.8927 + if (newChild) { 1.8928 + if (oldChilds[i] === newChild) { 1.8929 + i--; 1.8930 + } 1.8931 + fragment.insertBefore(newChild, fragment.firstChild); 1.8932 + } 1.8933 + } 1.8934 + } 1.8935 + 1.8936 + nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode); 1.8937 + 1.8938 + if (nodeDisplay === '') { 1.8939 + // Handle display style when element not in dom 1.8940 + nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : ""; 1.8941 + } 1.8942 + if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) { 1.8943 + fragment.appendChild(oldNode.ownerDocument.createElement("br")); 1.8944 + } 1.8945 + 1.8946 + // TODO: try to minimize surplus spaces 1.8947 + if (wysihtml.lang.array([ 1.8948 + "div", "pre", "p", 1.8949 + "table", "td", "th", 1.8950 + "ul", "ol", "li", 1.8951 + "dd", "dl", 1.8952 + "footer", "header", "section", 1.8953 + "h1", "h2", "h3", "h4", "h5", "h6" 1.8954 + ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) { 1.8955 + // add space at first when unwraping non-textflow elements 1.8956 + if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) { 1.8957 + fragment.appendChild(oldNode.ownerDocument.createTextNode(" ")); 1.8958 + } 1.8959 + } 1.8960 + 1.8961 + if (fragment.normalize) { 1.8962 + fragment.normalize(); 1.8963 + } 1.8964 + return fragment; 1.8965 + } else { 1.8966 + // Remove 1.8967 + return null; 1.8968 + } 1.8969 + } 1.8970 + 1.8971 + // Converts all childnodes 1.8972 + for (i=0; i<oldChildsLength; i++) { 1.8973 + if (oldChilds[i]) { 1.8974 + newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass); 1.8975 + if (newChild) { 1.8976 + if (oldChilds[i] === newChild) { 1.8977 + i--; 1.8978 + } 1.8979 + newNode.appendChild(newChild); 1.8980 + } 1.8981 + } 1.8982 + } 1.8983 + 1.8984 + // Cleanup senseless <span> elements 1.8985 + if (cleanUp && 1.8986 + newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && 1.8987 + (!newNode.childNodes.length || 1.8988 + ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) || 1.8989 + !newNode.attributes.length) 1.8990 + ) { 1.8991 + fragment = newNode.ownerDocument.createDocumentFragment(); 1.8992 + while (newNode.firstChild) { 1.8993 + fragment.appendChild(newNode.firstChild); 1.8994 + } 1.8995 + if (fragment.normalize) { 1.8996 + fragment.normalize(); 1.8997 + } 1.8998 + return fragment; 1.8999 + } 1.9000 + 1.9001 + if (newNode.normalize) { 1.9002 + newNode.normalize(); 1.9003 + } 1.9004 + return newNode; 1.9005 + } 1.9006 + 1.9007 + function _applySelectorRules (element, selectorRules) { 1.9008 + var sel, method, els; 1.9009 + 1.9010 + for (sel in selectorRules) { 1.9011 + if (selectorRules.hasOwnProperty(sel)) { 1.9012 + if (wysihtml.lang.object(selectorRules[sel]).isFunction()) { 1.9013 + method = selectorRules[sel]; 1.9014 + } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) { 1.9015 + method = elementHandlingMethods[selectorRules[sel]]; 1.9016 + } 1.9017 + els = element.querySelectorAll(sel); 1.9018 + for (var i = els.length; i--;) { 1.9019 + method(els[i]); 1.9020 + } 1.9021 + } 1.9022 + } 1.9023 + } 1.9024 + 1.9025 + function _handleElement(oldNode, clearInternals) { 1.9026 + var rule, 1.9027 + newNode, 1.9028 + tagRules = currentRules.tags, 1.9029 + nodeName = oldNode.nodeName.toLowerCase(), 1.9030 + scopeName = oldNode.scopeName, 1.9031 + renameTag; 1.9032 + 1.9033 + /** 1.9034 + * We already parsed that element 1.9035 + * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) 1.9036 + */ 1.9037 + if (oldNode._wysihtml) { 1.9038 + return null; 1.9039 + } 1.9040 + oldNode._wysihtml = 1; 1.9041 + 1.9042 + if (oldNode.className === "wysihtml-temp") { 1.9043 + return null; 1.9044 + } 1.9045 + 1.9046 + /** 1.9047 + * IE is the only browser who doesn't include the namespace in the 1.9048 + * nodeName, that's why we have to prepend it by ourselves 1.9049 + * scopeName is a proprietary IE feature 1.9050 + * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx 1.9051 + */ 1.9052 + if (scopeName && scopeName != "HTML") { 1.9053 + nodeName = scopeName + ":" + nodeName; 1.9054 + } 1.9055 + /** 1.9056 + * Repair node 1.9057 + * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags 1.9058 + * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout 1.9059 + */ 1.9060 + if ("outerHTML" in oldNode) { 1.9061 + if (!wysihtml.browser.autoClosesUnclosedTags() && 1.9062 + oldNode.nodeName === "P" && 1.9063 + oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") { 1.9064 + nodeName = "div"; 1.9065 + } 1.9066 + } 1.9067 + 1.9068 + if (nodeName in tagRules) { 1.9069 + rule = tagRules[nodeName]; 1.9070 + if (!rule || rule.remove) { 1.9071 + return null; 1.9072 + } else if (rule.unwrap) { 1.9073 + return false; 1.9074 + } 1.9075 + rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; 1.9076 + } else if (oldNode.firstChild) { 1.9077 + rule = { rename_tag: DEFAULT_NODE_NAME }; 1.9078 + } else { 1.9079 + // Remove empty unknown elements 1.9080 + return null; 1.9081 + } 1.9082 + 1.9083 + // tests if type condition is met or node should be removed/unwrapped/renamed 1.9084 + if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) { 1.9085 + if (rule.remove_action) { 1.9086 + if (rule.remove_action === "unwrap") { 1.9087 + return false; 1.9088 + } else if (rule.remove_action === "rename") { 1.9089 + renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME; 1.9090 + } else { 1.9091 + return null; 1.9092 + } 1.9093 + } else { 1.9094 + return null; 1.9095 + } 1.9096 + } 1.9097 + 1.9098 + newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName); 1.9099 + _handleAttributes(oldNode, newNode, rule, clearInternals); 1.9100 + _handleStyles(oldNode, newNode, rule); 1.9101 + 1.9102 + oldNode = null; 1.9103 + 1.9104 + if (newNode.normalize) { newNode.normalize(); } 1.9105 + return newNode; 1.9106 + } 1.9107 + 1.9108 + function _testTypes(oldNode, rules, types, clearInternals) { 1.9109 + var definition, type; 1.9110 + 1.9111 + // do not interfere with placeholder span or pasting caret position is not maintained 1.9112 + if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) { 1.9113 + return true; 1.9114 + } 1.9115 + 1.9116 + for (type in types) { 1.9117 + if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) { 1.9118 + definition = rules.type_definitions[type]; 1.9119 + if (_testType(oldNode, definition)) { 1.9120 + return true; 1.9121 + } 1.9122 + } 1.9123 + } 1.9124 + return false; 1.9125 + } 1.9126 + 1.9127 + function array_contains(a, obj) { 1.9128 + var i = a.length; 1.9129 + while (i--) { 1.9130 + if (a[i] === obj) { 1.9131 + return true; 1.9132 + } 1.9133 + } 1.9134 + return false; 1.9135 + } 1.9136 + 1.9137 + function _testType(oldNode, definition) { 1.9138 + 1.9139 + var nodeClasses = oldNode.getAttribute("class"), 1.9140 + nodeStyles = oldNode.getAttribute("style"), 1.9141 + classesLength, s, s_corrected, a, attr, currentClass, styleProp; 1.9142 + 1.9143 + // test for methods 1.9144 + if (definition.methods) { 1.9145 + for (var m in definition.methods) { 1.9146 + if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) { 1.9147 + 1.9148 + if (typeCeckMethods[m](oldNode)) { 1.9149 + return true; 1.9150 + } 1.9151 + } 1.9152 + } 1.9153 + } 1.9154 + 1.9155 + // test for classes, if one found return true 1.9156 + if (nodeClasses && definition.classes) { 1.9157 + nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP); 1.9158 + classesLength = nodeClasses.length; 1.9159 + for (var i = 0; i < classesLength; i++) { 1.9160 + if (definition.classes[nodeClasses[i]]) { 1.9161 + return true; 1.9162 + } 1.9163 + } 1.9164 + } 1.9165 + 1.9166 + // test for styles, if one found return true 1.9167 + if (nodeStyles && definition.styles) { 1.9168 + 1.9169 + nodeStyles = nodeStyles.split(';'); 1.9170 + for (s in definition.styles) { 1.9171 + if (definition.styles.hasOwnProperty(s)) { 1.9172 + for (var sp = nodeStyles.length; sp--;) { 1.9173 + styleProp = nodeStyles[sp].split(':'); 1.9174 + 1.9175 + if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) { 1.9176 + if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) { 1.9177 + return true; 1.9178 + } 1.9179 + } 1.9180 + } 1.9181 + } 1.9182 + } 1.9183 + } 1.9184 + 1.9185 + // test for attributes in general against regex match 1.9186 + if (definition.attrs) { 1.9187 + for (a in definition.attrs) { 1.9188 + if (definition.attrs.hasOwnProperty(a)) { 1.9189 + attr = wysihtml.dom.getAttribute(oldNode, a); 1.9190 + if (typeof(attr) === "string") { 1.9191 + if (attr.search(definition.attrs[a]) > -1) { 1.9192 + return true; 1.9193 + } 1.9194 + } 1.9195 + } 1.9196 + } 1.9197 + } 1.9198 + return false; 1.9199 + } 1.9200 + 1.9201 + function _handleStyles(oldNode, newNode, rule) { 1.9202 + var s, v; 1.9203 + if(rule && rule.keep_styles) { 1.9204 + for (s in rule.keep_styles) { 1.9205 + if (rule.keep_styles.hasOwnProperty(s)) { 1.9206 + v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s]; 1.9207 + // value can be regex and if so should match or style skipped 1.9208 + if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) { 1.9209 + continue; 1.9210 + } 1.9211 + if (s === "float") { 1.9212 + // IE compability 1.9213 + newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v; 1.9214 + } else if (oldNode.style[s]) { 1.9215 + newNode.style[s] = v; 1.9216 + } 1.9217 + } 1.9218 + } 1.9219 + } 1.9220 + }; 1.9221 + 1.9222 + function _getAttributesBeginningWith(beginning, attributes) { 1.9223 + var returnAttributes = []; 1.9224 + for (var attr in attributes) { 1.9225 + if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) { 1.9226 + returnAttributes.push(attr); 1.9227 + } 1.9228 + } 1.9229 + return returnAttributes; 1.9230 + } 1.9231 + 1.9232 + function _checkAttribute(attributeName, attributeValue, methodName, nodeName) { 1.9233 + var method = wysihtml.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName], 1.9234 + newAttributeValue; 1.9235 + 1.9236 + if (method) { 1.9237 + newAttributeValue = method(attributeValue, nodeName); 1.9238 + if (typeof(newAttributeValue) === "string") { 1.9239 + return newAttributeValue; 1.9240 + } 1.9241 + } 1.9242 + 1.9243 + return false; 1.9244 + } 1.9245 + 1.9246 + function _checkAttributes(oldNode, local_attributes) { 1.9247 + var globalAttributes = wysihtml.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes 1.9248 + checkAttributes = wysihtml.lang.object(globalAttributes).merge( wysihtml.lang.object(local_attributes || {}).clone()).get(), 1.9249 + attributes = {}, 1.9250 + oldAttributes = wysihtml.dom.getAttributes(oldNode), 1.9251 + attributeName, newValue, matchingAttributes; 1.9252 + 1.9253 + for (attributeName in checkAttributes) { 1.9254 + if ((/\*$/).test(attributeName)) { 1.9255 + 1.9256 + matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes); 1.9257 + for (var i = 0, imax = matchingAttributes.length; i < imax; i++) { 1.9258 + 1.9259 + newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName); 1.9260 + if (newValue !== false) { 1.9261 + attributes[matchingAttributes[i]] = newValue; 1.9262 + } 1.9263 + } 1.9264 + } else { 1.9265 + newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName); 1.9266 + if (newValue !== false) { 1.9267 + attributes[attributeName] = newValue; 1.9268 + } 1.9269 + } 1.9270 + } 1.9271 + 1.9272 + return attributes; 1.9273 + } 1.9274 + 1.9275 + // TODO: refactor. Too long to read 1.9276 + function _handleAttributes(oldNode, newNode, rule, clearInternals) { 1.9277 + var attributes = {}, // fresh new set of attributes to set on newNode 1.9278 + setClass = rule.set_class, // classes to set 1.9279 + addClass = rule.add_class, // add classes based on existing attributes 1.9280 + addStyle = rule.add_style, // add styles based on existing attributes 1.9281 + setAttributes = rule.set_attributes, // attributes to set on the current node 1.9282 + allowedClasses = currentRules.classes, 1.9283 + i = 0, 1.9284 + classes = [], 1.9285 + styles = [], 1.9286 + newClasses = [], 1.9287 + oldClasses = [], 1.9288 + classesLength, 1.9289 + newClassesLength, 1.9290 + currentClass, 1.9291 + newClass, 1.9292 + attributeName, 1.9293 + method; 1.9294 + 1.9295 + if (setAttributes) { 1.9296 + attributes = wysihtml.lang.object(setAttributes).clone(); 1.9297 + } 1.9298 + 1.9299 + // check/convert values of attributes 1.9300 + attributes = wysihtml.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get(); 1.9301 + 1.9302 + if (setClass) { 1.9303 + classes.push(setClass); 1.9304 + } 1.9305 + 1.9306 + if (addClass) { 1.9307 + for (attributeName in addClass) { 1.9308 + method = addClassMethods[addClass[attributeName]]; 1.9309 + if (!method) { 1.9310 + continue; 1.9311 + } 1.9312 + newClass = method(wysihtml.dom.getAttribute(oldNode, attributeName)); 1.9313 + if (typeof(newClass) === "string") { 1.9314 + classes.push(newClass); 1.9315 + } 1.9316 + } 1.9317 + } 1.9318 + 1.9319 + if (addStyle) { 1.9320 + for (attributeName in addStyle) { 1.9321 + method = addStyleMethods[addStyle[attributeName]]; 1.9322 + if (!method) { 1.9323 + continue; 1.9324 + } 1.9325 + 1.9326 + newStyle = method(wysihtml.dom.getAttribute(oldNode, attributeName)); 1.9327 + if (typeof(newStyle) === "string") { 1.9328 + styles.push(newStyle); 1.9329 + } 1.9330 + } 1.9331 + } 1.9332 + 1.9333 + 1.9334 + if (typeof(allowedClasses) === "string" && allowedClasses === "any") { 1.9335 + if (oldNode.getAttribute("class")) { 1.9336 + if (currentRules.classes_blacklist) { 1.9337 + oldClasses = oldNode.getAttribute("class"); 1.9338 + if (oldClasses) { 1.9339 + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); 1.9340 + } 1.9341 + 1.9342 + classesLength = classes.length; 1.9343 + for (; i<classesLength; i++) { 1.9344 + currentClass = classes[i]; 1.9345 + if (!currentRules.classes_blacklist[currentClass]) { 1.9346 + newClasses.push(currentClass); 1.9347 + } 1.9348 + } 1.9349 + 1.9350 + if (newClasses.length) { 1.9351 + attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" "); 1.9352 + } 1.9353 + 1.9354 + } else { 1.9355 + attributes["class"] = oldNode.getAttribute("class"); 1.9356 + } 1.9357 + } else { 1.9358 + if(classes && classes.length > 0) { 1.9359 + attributes["class"] = wysihtml.lang.array(classes).unique().join(" "); 1.9360 + } 1.9361 + } 1.9362 + } else { 1.9363 + // make sure that wysihtml temp class doesn't get stripped out 1.9364 + if (!clearInternals) { 1.9365 + allowedClasses["_wysihtml-temp-placeholder"] = 1; 1.9366 + allowedClasses["_rangySelectionBoundary"] = 1; 1.9367 + allowedClasses["wysiwyg-tmp-selected-cell"] = 1; 1.9368 + } 1.9369 + 1.9370 + // add old classes last 1.9371 + oldClasses = oldNode.getAttribute("class"); 1.9372 + if (oldClasses) { 1.9373 + classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); 1.9374 + } 1.9375 + classesLength = classes.length; 1.9376 + for (; i<classesLength; i++) { 1.9377 + currentClass = classes[i]; 1.9378 + if (allowedClasses[currentClass]) { 1.9379 + newClasses.push(currentClass); 1.9380 + } 1.9381 + } 1.9382 + 1.9383 + if (newClasses.length) { 1.9384 + attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" "); 1.9385 + } 1.9386 + } 1.9387 + 1.9388 + // remove table selection class if present 1.9389 + if (attributes["class"] && clearInternals) { 1.9390 + attributes["class"] = attributes["class"].replace("wysiwyg-tmp-selected-cell", ""); 1.9391 + if ((/^\s*$/g).test(attributes["class"])) { 1.9392 + delete attributes["class"]; 1.9393 + } 1.9394 + } 1.9395 + 1.9396 + if (styles.length) { 1.9397 + attributes["style"] = wysihtml.lang.array(styles).unique().join(" "); 1.9398 + } 1.9399 + 1.9400 + // set attributes on newNode 1.9401 + for (attributeName in attributes) { 1.9402 + // Setting attributes can cause a js error in IE under certain circumstances 1.9403 + // eg. on a <img> under https when it's new attribute value is non-https 1.9404 + // TODO: Investigate this further and check for smarter handling 1.9405 + try { 1.9406 + newNode.setAttribute(attributeName, attributes[attributeName]); 1.9407 + } catch(e) {} 1.9408 + } 1.9409 + 1.9410 + // IE8 sometimes loses the width/height attributes when those are set before the "src" 1.9411 + // so we make sure to set them again 1.9412 + if (attributes.src) { 1.9413 + if (typeof(attributes.width) !== "undefined") { 1.9414 + newNode.setAttribute("width", attributes.width); 1.9415 + } 1.9416 + if (typeof(attributes.height) !== "undefined") { 1.9417 + newNode.setAttribute("height", attributes.height); 1.9418 + } 1.9419 + } 1.9420 + } 1.9421 + 1.9422 + function _handleText(oldNode) { 1.9423 + var nextSibling = oldNode.nextSibling; 1.9424 + if (nextSibling && nextSibling.nodeType === wysihtml.TEXT_NODE) { 1.9425 + // Concatenate text nodes 1.9426 + nextSibling.data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); 1.9427 + } else { 1.9428 + // \uFEFF = wysihtml.INVISIBLE_SPACE (used as a hack in certain rich text editing situations) 1.9429 + var data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); 1.9430 + return oldNode.ownerDocument.createTextNode(data); 1.9431 + } 1.9432 + } 1.9433 + 1.9434 + function _handleComment(oldNode) { 1.9435 + if (currentRules.comments) { 1.9436 + return oldNode.ownerDocument.createComment(oldNode.nodeValue); 1.9437 + } 1.9438 + } 1.9439 + 1.9440 + // ------------ attribute checks ------------ \\ 1.9441 + var attributeCheckMethods = { 1.9442 + url: (function() { 1.9443 + var REG_EXP = /^https?:\/\//i; 1.9444 + return function(attributeValue) { 1.9445 + if (!attributeValue || !attributeValue.match(REG_EXP)) { 1.9446 + return null; 1.9447 + } 1.9448 + return attributeValue.replace(REG_EXP, function(match) { 1.9449 + return match.toLowerCase(); 1.9450 + }); 1.9451 + }; 1.9452 + })(), 1.9453 + 1.9454 + src: (function() { 1.9455 + var REG_EXP = /^(\/|https?:\/\/)/i; 1.9456 + return function(attributeValue) { 1.9457 + if (!attributeValue || !attributeValue.match(REG_EXP)) { 1.9458 + return null; 1.9459 + } 1.9460 + return attributeValue.replace(REG_EXP, function(match) { 1.9461 + return match.toLowerCase(); 1.9462 + }); 1.9463 + }; 1.9464 + })(), 1.9465 + 1.9466 + href: (function() { 1.9467 + var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i; 1.9468 + return function(attributeValue) { 1.9469 + if (!attributeValue || !attributeValue.match(REG_EXP)) { 1.9470 + return null; 1.9471 + } 1.9472 + return attributeValue.replace(REG_EXP, function(match) { 1.9473 + return match.toLowerCase(); 1.9474 + }); 1.9475 + }; 1.9476 + })(), 1.9477 + 1.9478 + alt: (function() { 1.9479 + var REG_EXP = /[^ a-z0-9_\-]/gi; 1.9480 + return function(attributeValue, nodeName) { 1.9481 + if (!attributeValue) { 1.9482 + if (nodeName === "IMG") { 1.9483 + return ""; 1.9484 + } else { 1.9485 + return null; 1.9486 + } 1.9487 + } 1.9488 + return attributeValue.replace(REG_EXP, ""); 1.9489 + }; 1.9490 + })(), 1.9491 + 1.9492 + // Integers. Does not work with floating point numbers and units 1.9493 + numbers: (function() { 1.9494 + var REG_EXP = /\D/g; 1.9495 + return function(attributeValue) { 1.9496 + attributeValue = (attributeValue || "").replace(REG_EXP, ""); 1.9497 + return attributeValue || null; 1.9498 + }; 1.9499 + })(), 1.9500 + 1.9501 + // Useful for with/height attributes where floating points and percentages are allowed 1.9502 + dimension: (function() { 1.9503 + var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/; 1.9504 + return function(attributeValue) { 1.9505 + attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3"); 1.9506 + return attributeValue || null; 1.9507 + }; 1.9508 + })(), 1.9509 + 1.9510 + any: (function() { 1.9511 + return function(attributeValue) { 1.9512 + if (!attributeValue) { 1.9513 + return null; 1.9514 + } 1.9515 + return attributeValue; 1.9516 + }; 1.9517 + })() 1.9518 + }; 1.9519 + 1.9520 + // ------------ style converter (converts an html attribute to a style) ------------ \\ 1.9521 + var addStyleMethods = { 1.9522 + align_text: (function() { 1.9523 + var mapping = { 1.9524 + left: "text-align: left;", 1.9525 + right: "text-align: right;", 1.9526 + center: "text-align: center;" 1.9527 + }; 1.9528 + return function(attributeValue) { 1.9529 + return mapping[String(attributeValue).toLowerCase()]; 1.9530 + }; 1.9531 + })(), 1.9532 + }; 1.9533 + 1.9534 + // ------------ class converter (converts an html attribute to a class name) ------------ \\ 1.9535 + var addClassMethods = { 1.9536 + align_img: (function() { 1.9537 + var mapping = { 1.9538 + left: "wysiwyg-float-left", 1.9539 + right: "wysiwyg-float-right" 1.9540 + }; 1.9541 + return function(attributeValue) { 1.9542 + return mapping[String(attributeValue).toLowerCase()]; 1.9543 + }; 1.9544 + })(), 1.9545 + 1.9546 + align_text: (function() { 1.9547 + var mapping = { 1.9548 + left: "wysiwyg-text-align-left", 1.9549 + right: "wysiwyg-text-align-right", 1.9550 + center: "wysiwyg-text-align-center", 1.9551 + justify: "wysiwyg-text-align-justify" 1.9552 + }; 1.9553 + return function(attributeValue) { 1.9554 + return mapping[String(attributeValue).toLowerCase()]; 1.9555 + }; 1.9556 + })(), 1.9557 + 1.9558 + clear_br: (function() { 1.9559 + var mapping = { 1.9560 + left: "wysiwyg-clear-left", 1.9561 + right: "wysiwyg-clear-right", 1.9562 + both: "wysiwyg-clear-both", 1.9563 + all: "wysiwyg-clear-both" 1.9564 + }; 1.9565 + return function(attributeValue) { 1.9566 + return mapping[String(attributeValue).toLowerCase()]; 1.9567 + }; 1.9568 + })(), 1.9569 + 1.9570 + size_font: (function() { 1.9571 + var mapping = { 1.9572 + "1": "wysiwyg-font-size-xx-small", 1.9573 + "2": "wysiwyg-font-size-small", 1.9574 + "3": "wysiwyg-font-size-medium", 1.9575 + "4": "wysiwyg-font-size-large", 1.9576 + "5": "wysiwyg-font-size-x-large", 1.9577 + "6": "wysiwyg-font-size-xx-large", 1.9578 + "7": "wysiwyg-font-size-xx-large", 1.9579 + "-": "wysiwyg-font-size-smaller", 1.9580 + "+": "wysiwyg-font-size-larger" 1.9581 + }; 1.9582 + return function(attributeValue) { 1.9583 + return mapping[String(attributeValue).charAt(0)]; 1.9584 + }; 1.9585 + })() 1.9586 + }; 1.9587 + 1.9588 + // checks if element is possibly visible 1.9589 + var typeCeckMethods = { 1.9590 + has_visible_contet: (function() { 1.9591 + var txt, 1.9592 + isVisible = false, 1.9593 + visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript', 1.9594 + 'style', 'table', 'iframe', 'object', 'embed', 'audio', 1.9595 + 'svg', 'input', 'button', 'select','textarea', 'canvas']; 1.9596 + 1.9597 + return function(el) { 1.9598 + 1.9599 + // has visible innertext. so is visible 1.9600 + txt = (el.innerText || el.textContent).replace(/\s/g, ''); 1.9601 + if (txt && txt.length > 0) { 1.9602 + return true; 1.9603 + } 1.9604 + 1.9605 + // matches list of visible dimensioned elements 1.9606 + for (var i = visibleElements.length; i--;) { 1.9607 + if (el.querySelector(visibleElements[i])) { 1.9608 + return true; 1.9609 + } 1.9610 + } 1.9611 + 1.9612 + // try to measure dimesions in last resort. (can find only of elements in dom) 1.9613 + if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) { 1.9614 + return true; 1.9615 + } 1.9616 + 1.9617 + return false; 1.9618 + }; 1.9619 + })() 1.9620 + }; 1.9621 + 1.9622 + var elementHandlingMethods = { 1.9623 + unwrap: function (element) { 1.9624 + wysihtml.dom.unwrap(element); 1.9625 + }, 1.9626 + 1.9627 + remove: function (element) { 1.9628 + element.parentNode.removeChild(element); 1.9629 + } 1.9630 + }; 1.9631 + 1.9632 + return parse(elementOrHtml_current, config_current); 1.9633 +}; 1.9634 + 1.9635 +// does a selector query on element or array of elements 1.9636 +wysihtml.dom.query = function(elements, query) { 1.9637 + var ret = [], 1.9638 + q; 1.9639 + 1.9640 + if (elements.nodeType) { 1.9641 + elements = [elements]; 1.9642 + } 1.9643 + 1.9644 + for (var e = 0, len = elements.length; e < len; e++) { 1.9645 + q = elements[e].querySelectorAll(query); 1.9646 + if (q) { 1.9647 + for(var i = q.length; i--; ret.unshift(q[i])); 1.9648 + } 1.9649 + } 1.9650 + return ret; 1.9651 +}; 1.9652 + 1.9653 +/** 1.9654 + * Checks for empty text node childs and removes them 1.9655 + * 1.9656 + * @param {Element} node The element in which to cleanup 1.9657 + * @example 1.9658 + * wysihtml.dom.removeEmptyTextNodes(element); 1.9659 + */ 1.9660 +wysihtml.dom.removeEmptyTextNodes = function(node) { 1.9661 + var childNode, 1.9662 + childNodes = wysihtml.lang.array(node.childNodes).get(), 1.9663 + childNodesLength = childNodes.length, 1.9664 + i = 0; 1.9665 + 1.9666 + for (; i<childNodesLength; i++) { 1.9667 + childNode = childNodes[i]; 1.9668 + if (childNode.nodeType === wysihtml.TEXT_NODE && (/^[\n\r]*$/).test(childNode.data)) { 1.9669 + childNode.parentNode.removeChild(childNode); 1.9670 + } 1.9671 + } 1.9672 +}; 1.9673 + 1.9674 +wysihtml.dom.removeInvisibleSpaces = function(node) { 1.9675 + var textNodes = wysihtml.dom.getTextNodes(node); 1.9676 + for (var n = textNodes.length; n--;) { 1.9677 + textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); 1.9678 + } 1.9679 +}; 1.9680 + 1.9681 +/** 1.9682 + * Renames an element (eg. a <div> to a <p>) and keeps its childs 1.9683 + * 1.9684 + * @param {Element} element The list element which should be renamed 1.9685 + * @param {Element} newNodeName The desired tag name 1.9686 + * 1.9687 + * @example 1.9688 + * <!-- Assume the following dom: --> 1.9689 + * <ul id="list"> 1.9690 + * <li>eminem</li> 1.9691 + * <li>dr. dre</li> 1.9692 + * <li>50 Cent</li> 1.9693 + * </ul> 1.9694 + * 1.9695 + * <script> 1.9696 + * wysihtml.dom.renameElement(document.getElementById("list"), "ol"); 1.9697 + * </script> 1.9698 + * 1.9699 + * <!-- Will result in: --> 1.9700 + * <ol> 1.9701 + * <li>eminem</li> 1.9702 + * <li>dr. dre</li> 1.9703 + * <li>50 Cent</li> 1.9704 + * </ol> 1.9705 + */ 1.9706 +wysihtml.dom.renameElement = function(element, newNodeName) { 1.9707 + var newElement = element.ownerDocument.createElement(newNodeName), 1.9708 + firstChild; 1.9709 + while (firstChild = element.firstChild) { 1.9710 + newElement.appendChild(firstChild); 1.9711 + } 1.9712 + wysihtml.dom.copyAttributes(["align", "className"]).from(element).to(newElement); 1.9713 + 1.9714 + if (element.parentNode) { 1.9715 + element.parentNode.replaceChild(newElement, element); 1.9716 + } 1.9717 + 1.9718 + return newElement; 1.9719 +}; 1.9720 + 1.9721 +/** 1.9722 + * Takes an element, removes it and replaces it with it's childs 1.9723 + * 1.9724 + * @param {Object} node The node which to replace with it's child nodes 1.9725 + * @example 1.9726 + * <div id="foo"> 1.9727 + * <span>hello</span> 1.9728 + * </div> 1.9729 + * <script> 1.9730 + * // Remove #foo and replace with it's children 1.9731 + * wysihtml.dom.replaceWithChildNodes(document.getElementById("foo")); 1.9732 + * </script> 1.9733 + */ 1.9734 +wysihtml.dom.replaceWithChildNodes = function(node) { 1.9735 + if (!node.parentNode) { 1.9736 + return; 1.9737 + } 1.9738 + 1.9739 + while (node.firstChild) { 1.9740 + node.parentNode.insertBefore(node.firstChild, node); 1.9741 + } 1.9742 + node.parentNode.removeChild(node); 1.9743 +}; 1.9744 + 1.9745 +/** 1.9746 + * Unwraps an unordered/ordered list 1.9747 + * 1.9748 + * @param {Element} element The list element which should be unwrapped 1.9749 + * 1.9750 + * @example 1.9751 + * <!-- Assume the following dom: --> 1.9752 + * <ul id="list"> 1.9753 + * <li>eminem</li> 1.9754 + * <li>dr. dre</li> 1.9755 + * <li>50 Cent</li> 1.9756 + * </ul> 1.9757 + * 1.9758 + * <script> 1.9759 + * wysihtml.dom.resolveList(document.getElementById("list")); 1.9760 + * </script> 1.9761 + * 1.9762 + * <!-- Will result in: --> 1.9763 + * eminem<br> 1.9764 + * dr. dre<br> 1.9765 + * 50 Cent<br> 1.9766 + */ 1.9767 +(function(dom) { 1.9768 + function _isBlockElement(node) { 1.9769 + return dom.getStyle("display").from(node) === "block"; 1.9770 + } 1.9771 + 1.9772 + function _isLineBreak(node) { 1.9773 + return node.nodeName === "BR"; 1.9774 + } 1.9775 + 1.9776 + function _appendLineBreak(element) { 1.9777 + var lineBreak = element.ownerDocument.createElement("br"); 1.9778 + element.appendChild(lineBreak); 1.9779 + } 1.9780 + 1.9781 + function resolveList(list, useLineBreaks) { 1.9782 + if (!list.nodeName.match(/^(MENU|UL|OL)$/)) { 1.9783 + return; 1.9784 + } 1.9785 + 1.9786 + var doc = list.ownerDocument, 1.9787 + fragment = doc.createDocumentFragment(), 1.9788 + previousSibling = wysihtml.dom.domNode(list).prev({ignoreBlankTexts: true}), 1.9789 + nextSibling = wysihtml.dom.domNode(list).next({ignoreBlankTexts: true}), 1.9790 + firstChild, 1.9791 + lastChild, 1.9792 + isLastChild, 1.9793 + shouldAppendLineBreak, 1.9794 + paragraph, 1.9795 + listItem, 1.9796 + lastListItem = list.lastElementChild || list.lastChild, 1.9797 + isLastItem; 1.9798 + 1.9799 + if (useLineBreaks) { 1.9800 + // Insert line break if list is after a non-block element 1.9801 + if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) { 1.9802 + _appendLineBreak(fragment); 1.9803 + } 1.9804 + 1.9805 + while (listItem = (list.firstElementChild || list.firstChild)) { 1.9806 + lastChild = listItem.lastChild; 1.9807 + isLastItem = listItem === lastListItem; 1.9808 + while (firstChild = listItem.firstChild) { 1.9809 + isLastChild = firstChild === lastChild; 1.9810 + // This needs to be done before appending it to the fragment, as it otherwise will lose style information 1.9811 + shouldAppendLineBreak = (!isLastItem || (nextSibling && !_isBlockElement(nextSibling))) && isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); 1.9812 + fragment.appendChild(firstChild); 1.9813 + if (shouldAppendLineBreak) { 1.9814 + _appendLineBreak(fragment); 1.9815 + } 1.9816 + } 1.9817 + 1.9818 + listItem.parentNode.removeChild(listItem); 1.9819 + } 1.9820 + } else { 1.9821 + while (listItem = (list.firstElementChild || list.firstChild)) { 1.9822 + if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) { 1.9823 + while (firstChild = listItem.firstChild) { 1.9824 + fragment.appendChild(firstChild); 1.9825 + } 1.9826 + } else { 1.9827 + paragraph = doc.createElement("p"); 1.9828 + while (firstChild = listItem.firstChild) { 1.9829 + paragraph.appendChild(firstChild); 1.9830 + } 1.9831 + fragment.appendChild(paragraph); 1.9832 + } 1.9833 + listItem.parentNode.removeChild(listItem); 1.9834 + } 1.9835 + } 1.9836 + 1.9837 + list.parentNode.replaceChild(fragment, list); 1.9838 + } 1.9839 + 1.9840 + dom.resolveList = resolveList; 1.9841 +})(wysihtml.dom); 1.9842 + 1.9843 +/** 1.9844 + * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way 1.9845 + * 1.9846 + * Browser Compatibility: 1.9847 + * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" 1.9848 + * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) 1.9849 + * 1.9850 + * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons: 1.9851 + * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'") 1.9852 + * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...) 1.9853 + * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire 1.9854 + * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe 1.9855 + * can do anything as if the sandbox attribute wasn't set 1.9856 + * 1.9857 + * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready 1.9858 + * @param {Object} [config] Optional parameters 1.9859 + * 1.9860 + * @example 1.9861 + * new wysihtml.dom.Sandbox(function(sandbox) { 1.9862 + * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">'; 1.9863 + * }); 1.9864 + */ 1.9865 +(function(wysihtml) { 1.9866 + var /** 1.9867 + * Default configuration 1.9868 + */ 1.9869 + doc = document, 1.9870 + /** 1.9871 + * Properties to unset/protect on the window object 1.9872 + */ 1.9873 + windowProperties = [ 1.9874 + "parent", "top", "opener", "frameElement", "frames", 1.9875 + "localStorage", "globalStorage", "sessionStorage", "indexedDB" 1.9876 + ], 1.9877 + /** 1.9878 + * Properties on the window object which are set to an empty function 1.9879 + */ 1.9880 + windowProperties2 = [ 1.9881 + "open", "close", "openDialog", "showModalDialog", 1.9882 + "alert", "confirm", "prompt", 1.9883 + "openDatabase", "postMessage", 1.9884 + "XMLHttpRequest", "XDomainRequest" 1.9885 + ], 1.9886 + /** 1.9887 + * Properties to unset/protect on the document object 1.9888 + */ 1.9889 + documentProperties = [ 1.9890 + "referrer", 1.9891 + "write", "open", "close" 1.9892 + ]; 1.9893 + 1.9894 + wysihtml.dom.Sandbox = Base.extend( 1.9895 + /** @scope wysihtml.dom.Sandbox.prototype */ { 1.9896 + 1.9897 + constructor: function(readyCallback, config) { 1.9898 + this.callback = readyCallback || wysihtml.EMPTY_FUNCTION; 1.9899 + this.config = wysihtml.lang.object({}).merge(config).get(); 1.9900 + if (!this.config.className) { 1.9901 + this.config.className = "wysihtml-sandbox"; 1.9902 + } 1.9903 + this.editableArea = this._createIframe(); 1.9904 + }, 1.9905 + 1.9906 + insertInto: function(element) { 1.9907 + if (typeof(element) === "string") { 1.9908 + element = doc.getElementById(element); 1.9909 + } 1.9910 + 1.9911 + element.appendChild(this.editableArea); 1.9912 + }, 1.9913 + 1.9914 + getIframe: function() { 1.9915 + return this.editableArea; 1.9916 + }, 1.9917 + 1.9918 + getWindow: function() { 1.9919 + this._readyError(); 1.9920 + }, 1.9921 + 1.9922 + getDocument: function() { 1.9923 + this._readyError(); 1.9924 + }, 1.9925 + 1.9926 + destroy: function() { 1.9927 + var iframe = this.getIframe(); 1.9928 + iframe.parentNode.removeChild(iframe); 1.9929 + }, 1.9930 + 1.9931 + _readyError: function() { 1.9932 + throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet"); 1.9933 + }, 1.9934 + 1.9935 + /** 1.9936 + * Creates the sandbox iframe 1.9937 + * 1.9938 + * Some important notes: 1.9939 + * - We can't use HTML5 sandbox for now: 1.9940 + * setting it causes that the iframe's dom can't be accessed from the outside 1.9941 + * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom 1.9942 + * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. 1.9943 + * In order to make this happen we need to set the "allow-scripts" flag. 1.9944 + * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. 1.9945 + * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) 1.9946 + * - IE needs to have the security="restricted" attribute set before the iframe is 1.9947 + * inserted into the dom tree 1.9948 + * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even 1.9949 + * though it supports it 1.9950 + * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore 1.9951 + * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely 1.9952 + * on the onreadystatechange event 1.9953 + */ 1.9954 + _createIframe: function() { 1.9955 + var that = this, 1.9956 + iframe = doc.createElement("iframe"); 1.9957 + iframe.className = this.config.className; 1.9958 + wysihtml.dom.setAttributes({ 1.9959 + "security": "restricted", 1.9960 + "allowtransparency": "true", 1.9961 + "frameborder": 0, 1.9962 + "width": 0, 1.9963 + "height": 0, 1.9964 + "marginwidth": 0, 1.9965 + "marginheight": 0 1.9966 + }).on(iframe); 1.9967 + 1.9968 + // Setting the src like this prevents ssl warnings in IE6 1.9969 + if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { 1.9970 + iframe.src = "javascript:'<html></html>'"; 1.9971 + } 1.9972 + 1.9973 + iframe.onload = function() { 1.9974 + iframe.onreadystatechange = iframe.onload = null; 1.9975 + that._onLoadIframe(iframe); 1.9976 + }; 1.9977 + 1.9978 + iframe.onreadystatechange = function() { 1.9979 + if (/loaded|complete/.test(iframe.readyState)) { 1.9980 + iframe.onreadystatechange = iframe.onload = null; 1.9981 + that._onLoadIframe(iframe); 1.9982 + } 1.9983 + }; 1.9984 + 1.9985 + return iframe; 1.9986 + }, 1.9987 + 1.9988 + /** 1.9989 + * Callback for when the iframe has finished loading 1.9990 + */ 1.9991 + _onLoadIframe: function(iframe) { 1.9992 + // don't resume when the iframe got unloaded (eg. by removing it from the dom) 1.9993 + if (!wysihtml.dom.contains(doc.documentElement, iframe)) { 1.9994 + return; 1.9995 + } 1.9996 + 1.9997 + var that = this, 1.9998 + iframeWindow = iframe.contentWindow, 1.9999 + iframeDocument = iframe.contentWindow.document, 1.10000 + charset = doc.characterSet || doc.charset || "utf-8", 1.10001 + sandboxHtml = this._getHtml({ 1.10002 + charset: charset, 1.10003 + stylesheets: this.config.stylesheets 1.10004 + }); 1.10005 + 1.10006 + // Create the basic dom tree including proper DOCTYPE and charset 1.10007 + iframeDocument.open("text/html", "replace"); 1.10008 + iframeDocument.write(sandboxHtml); 1.10009 + iframeDocument.close(); 1.10010 + 1.10011 + this.getWindow = function() { return iframe.contentWindow; }; 1.10012 + this.getDocument = function() { return iframe.contentWindow.document; }; 1.10013 + 1.10014 + // Catch js errors and pass them to the parent's onerror event 1.10015 + // addEventListener("error") doesn't work properly in some browsers 1.10016 + // TODO: apparently this doesn't work in IE9! 1.10017 + iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { 1.10018 + throw new Error("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber); 1.10019 + }; 1.10020 + 1.10021 + if (!wysihtml.browser.supportsSandboxedIframes()) { 1.10022 + // Unset a bunch of sensitive variables 1.10023 + // Please note: This isn't hack safe! 1.10024 + // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information 1.10025 + // IE is secure though, which is the most important thing, since IE is the only browser, who 1.10026 + // takes over scripts & styles into contentEditable elements when copied from external websites 1.10027 + // or applications (Microsoft Word, ...) 1.10028 + var i, length; 1.10029 + for (i=0, length=windowProperties.length; i<length; i++) { 1.10030 + this._unset(iframeWindow, windowProperties[i]); 1.10031 + } 1.10032 + for (i=0, length=windowProperties2.length; i<length; i++) { 1.10033 + this._unset(iframeWindow, windowProperties2[i], wysihtml.EMPTY_FUNCTION); 1.10034 + } 1.10035 + for (i=0, length=documentProperties.length; i<length; i++) { 1.10036 + this._unset(iframeDocument, documentProperties[i]); 1.10037 + } 1.10038 + // This doesn't work in Safari 5 1.10039 + // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit 1.10040 + this._unset(iframeDocument, "cookie", "", true); 1.10041 + } 1.10042 + 1.10043 + if (wysihtml.polyfills) { 1.10044 + wysihtml.polyfills(iframeWindow, iframeDocument).apply(); 1.10045 + } 1.10046 + 1.10047 + this.loaded = true; 1.10048 + 1.10049 + // Trigger the callback 1.10050 + setTimeout(function() { that.callback(that); }, 0); 1.10051 + }, 1.10052 + 1.10053 + _getHtml: function(templateVars) { 1.10054 + var stylesheets = templateVars.stylesheets, 1.10055 + html = "", 1.10056 + i = 0, 1.10057 + length; 1.10058 + stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets; 1.10059 + if (stylesheets) { 1.10060 + length = stylesheets.length; 1.10061 + for (; i<length; i++) { 1.10062 + html += '<link rel="stylesheet" href="' + stylesheets[i] + '">'; 1.10063 + } 1.10064 + } 1.10065 + templateVars.stylesheets = html; 1.10066 + 1.10067 + return wysihtml.lang.string( 1.10068 + '<!DOCTYPE html><html><head>' 1.10069 + + '<meta charset="#{charset}">#{stylesheets}</head>' 1.10070 + + '<body></body></html>' 1.10071 + ).interpolate(templateVars); 1.10072 + }, 1.10073 + 1.10074 + /** 1.10075 + * Method to unset/override existing variables 1.10076 + * @example 1.10077 + * // Make cookie unreadable and unwritable 1.10078 + * this._unset(document, "cookie", "", true); 1.10079 + */ 1.10080 + _unset: function(object, property, value, setter) { 1.10081 + try { object[property] = value; } catch(e) {} 1.10082 + 1.10083 + try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} 1.10084 + if (setter) { 1.10085 + try { object.__defineSetter__(property, function() {}); } catch(e) {} 1.10086 + } 1.10087 + 1.10088 + if (!wysihtml.browser.crashesWhenDefineProperty(property)) { 1.10089 + try { 1.10090 + var config = { 1.10091 + get: function() { return value; } 1.10092 + }; 1.10093 + if (setter) { 1.10094 + config.set = function() {}; 1.10095 + } 1.10096 + Object.defineProperty(object, property, config); 1.10097 + } catch(e) {} 1.10098 + } 1.10099 + } 1.10100 + }); 1.10101 +})(wysihtml); 1.10102 + 1.10103 +(function() { 1.10104 + var mapping = { 1.10105 + "className": "class" 1.10106 + }; 1.10107 + wysihtml.dom.setAttributes = function(attributes) { 1.10108 + return { 1.10109 + on: function(element) { 1.10110 + for (var i in attributes) { 1.10111 + element.setAttribute(mapping[i] || i, attributes[i]); 1.10112 + } 1.10113 + } 1.10114 + }; 1.10115 + }; 1.10116 +})(); 1.10117 + 1.10118 +wysihtml.dom.setStyles = function(styles) { 1.10119 + return { 1.10120 + on: function(element) { 1.10121 + var style = element.style; 1.10122 + if (typeof(styles) === "string") { 1.10123 + style.cssText += ";" + styles; 1.10124 + return; 1.10125 + } 1.10126 + for (var i in styles) { 1.10127 + if (i === "float") { 1.10128 + style.cssFloat = styles[i]; 1.10129 + style.styleFloat = styles[i]; 1.10130 + } else { 1.10131 + style[i] = styles[i]; 1.10132 + } 1.10133 + } 1.10134 + } 1.10135 + }; 1.10136 +}; 1.10137 + 1.10138 +/** 1.10139 + * Simulate HTML5 placeholder attribute 1.10140 + * 1.10141 + * Needed since 1.10142 + * - div[contentEditable] elements don't support it 1.10143 + * - older browsers (such as IE8 and Firefox 3.6) don't support it at all 1.10144 + * 1.10145 + * @param {Object} parent Instance of main wysihtml.Editor class 1.10146 + * @param {Element} view Instance of wysihtml.views.* class 1.10147 + * @param {String} placeholderText 1.10148 + * 1.10149 + * @example 1.10150 + * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); 1.10151 + */ 1.10152 +(function(dom) { 1.10153 + dom.simulatePlaceholder = function(editor, view, placeholderText, placeholderClassName) { 1.10154 + var CLASS_NAME = placeholderClassName || "wysihtml-placeholder", 1.10155 + unset = function() { 1.10156 + var composerIsVisible = view.element.offsetWidth > 0 && view.element.offsetHeight > 0; 1.10157 + if (view.hasPlaceholderSet()) { 1.10158 + view.clear(); 1.10159 + view.element.focus(); 1.10160 + if (composerIsVisible ) { 1.10161 + setTimeout(function() { 1.10162 + var sel = view.selection.getSelection(); 1.10163 + if (!sel.focusNode || !sel.anchorNode) { 1.10164 + view.selection.selectNode(view.element.firstChild || view.element); 1.10165 + } 1.10166 + }, 0); 1.10167 + } 1.10168 + } 1.10169 + view.placeholderSet = false; 1.10170 + dom.removeClass(view.element, CLASS_NAME); 1.10171 + }, 1.10172 + set = function() { 1.10173 + if (view.isEmpty() && !view.placeholderSet) { 1.10174 + view.placeholderSet = true; 1.10175 + view.setValue(placeholderText, false); 1.10176 + dom.addClass(view.element, CLASS_NAME); 1.10177 + } 1.10178 + }; 1.10179 + 1.10180 + editor 1.10181 + .on("set_placeholder", set) 1.10182 + .on("unset_placeholder", unset) 1.10183 + .on("focus:composer", unset) 1.10184 + .on("paste:composer", unset) 1.10185 + .on("blur:composer", set); 1.10186 + 1.10187 + set(); 1.10188 + }; 1.10189 +})(wysihtml.dom); 1.10190 + 1.10191 +(function(dom) { 1.10192 + var documentElement = document.documentElement; 1.10193 + if ("textContent" in documentElement) { 1.10194 + dom.setTextContent = function(element, text) { 1.10195 + element.textContent = text; 1.10196 + }; 1.10197 + 1.10198 + dom.getTextContent = function(element) { 1.10199 + return element.textContent; 1.10200 + }; 1.10201 + } else if ("innerText" in documentElement) { 1.10202 + dom.setTextContent = function(element, text) { 1.10203 + element.innerText = text; 1.10204 + }; 1.10205 + 1.10206 + dom.getTextContent = function(element) { 1.10207 + return element.innerText; 1.10208 + }; 1.10209 + } else { 1.10210 + dom.setTextContent = function(element, text) { 1.10211 + element.nodeValue = text; 1.10212 + }; 1.10213 + 1.10214 + dom.getTextContent = function(element) { 1.10215 + return element.nodeValue; 1.10216 + }; 1.10217 + } 1.10218 +})(wysihtml.dom); 1.10219 + 1.10220 +/* Unwraps element and returns list of childNodes that the node contained. 1.10221 + * 1.10222 + * Example: 1.10223 + * var childnodes = wysihtml.dom.unwrap(document.querySelector('.unwrap-me')); 1.10224 +*/ 1.10225 + 1.10226 +wysihtml.dom.unwrap = function(node) { 1.10227 + var children = []; 1.10228 + if (node.parentNode) { 1.10229 + while (node.lastChild) { 1.10230 + children.unshift(node.lastChild); 1.10231 + wysihtml.dom.insert(node.lastChild).after(node); 1.10232 + } 1.10233 + node.parentNode.removeChild(node); 1.10234 + } 1.10235 + return children; 1.10236 +}; 1.10237 + 1.10238 +/** 1.10239 + * Fix most common html formatting misbehaviors of browsers implementation when inserting 1.10240 + * content via copy & paste contentEditable 1.10241 + * 1.10242 + * @author Christopher Blum 1.10243 + */ 1.10244 +wysihtml.quirks.cleanPastedHTML = (function() { 1.10245 + 1.10246 + var styleToRegex = function (styleStr) { 1.10247 + var trimmedStr = wysihtml.lang.string(styleStr).trim(), 1.10248 + escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 1.10249 + 1.10250 + return new RegExp("^((?!^" + escapedStr + "$).)*$", "i"); 1.10251 + }; 1.10252 + 1.10253 + var extendRulesWithStyleExceptions = function (rules, exceptStyles) { 1.10254 + var newRules = wysihtml.lang.object(rules).clone(true), 1.10255 + tag, style; 1.10256 + 1.10257 + for (tag in newRules.tags) { 1.10258 + 1.10259 + if (newRules.tags.hasOwnProperty(tag)) { 1.10260 + if (newRules.tags[tag].keep_styles) { 1.10261 + for (style in newRules.tags[tag].keep_styles) { 1.10262 + if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) { 1.10263 + if (exceptStyles[style]) { 1.10264 + newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]); 1.10265 + } 1.10266 + } 1.10267 + } 1.10268 + } 1.10269 + } 1.10270 + } 1.10271 + 1.10272 + return newRules; 1.10273 + }; 1.10274 + 1.10275 + var pickRuleset = function(ruleset, html) { 1.10276 + var pickedSet, defaultSet; 1.10277 + 1.10278 + if (!ruleset) { 1.10279 + return null; 1.10280 + } 1.10281 + 1.10282 + for (var i = 0, max = ruleset.length; i < max; i++) { 1.10283 + if (!ruleset[i].condition) { 1.10284 + defaultSet = ruleset[i].set; 1.10285 + } 1.10286 + if (ruleset[i].condition && ruleset[i].condition.test(html)) { 1.10287 + return ruleset[i].set; 1.10288 + } 1.10289 + } 1.10290 + 1.10291 + return defaultSet; 1.10292 + }; 1.10293 + 1.10294 + return function(html, options) { 1.10295 + var exceptStyles = { 1.10296 + 'color': wysihtml.dom.getStyle("color").from(options.referenceNode), 1.10297 + 'fontSize': wysihtml.dom.getStyle("font-size").from(options.referenceNode) 1.10298 + }, 1.10299 + rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles), 1.10300 + newHtml; 1.10301 + 1.10302 + newHtml = wysihtml.dom.parse(html, { 1.10303 + "rules": rules, 1.10304 + "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content 1.10305 + "context": options.referenceNode.ownerDocument, 1.10306 + "uneditableClass": options.uneditableClass, 1.10307 + "clearInternals" : true, // don't paste temprorary selection and other markings 1.10308 + "unjoinNbsps" : true 1.10309 + }); 1.10310 + 1.10311 + return newHtml; 1.10312 + }; 1.10313 + 1.10314 +})(); 1.10315 + 1.10316 +/** 1.10317 + * IE and Opera leave an empty paragraph in the contentEditable element after clearing it 1.10318 + * 1.10319 + * @param {Object} contentEditableElement The contentEditable element to observe for clearing events 1.10320 + * @exaple 1.10321 + * wysihtml.quirks.ensureProperClearing(myContentEditableElement); 1.10322 + */ 1.10323 +wysihtml.quirks.ensureProperClearing = (function() { 1.10324 + var clearIfNecessary = function() { 1.10325 + var element = this; 1.10326 + setTimeout(function() { 1.10327 + var innerHTML = element.innerHTML.toLowerCase(); 1.10328 + if (innerHTML == "<p> </p>" || 1.10329 + innerHTML == "<p> </p><p> </p>") { 1.10330 + element.innerHTML = ""; 1.10331 + } 1.10332 + }, 0); 1.10333 + }; 1.10334 + 1.10335 + return function(composer) { 1.10336 + wysihtml.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); 1.10337 + }; 1.10338 +})(); 1.10339 + 1.10340 +// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 1.10341 +// 1.10342 +// In Firefox this: 1.10343 +// var d = document.createElement("div"); 1.10344 +// d.innerHTML ='<a href="~"></a>'; 1.10345 +// d.innerHTML; 1.10346 +// will result in: 1.10347 +// <a href="%7E"></a> 1.10348 +// which is wrong 1.10349 +(function(wysihtml) { 1.10350 + var TILDE_ESCAPED = "%7E"; 1.10351 + wysihtml.quirks.getCorrectInnerHTML = function(element) { 1.10352 + var innerHTML = element.innerHTML; 1.10353 + if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { 1.10354 + return innerHTML; 1.10355 + } 1.10356 + 1.10357 + var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), 1.10358 + url, 1.10359 + urlToSearch, 1.10360 + length, 1.10361 + i; 1.10362 + for (i=0, length=elementsWithTilde.length; i<length; i++) { 1.10363 + url = elementsWithTilde[i].href || elementsWithTilde[i].src; 1.10364 + urlToSearch = wysihtml.lang.string(url).replace("~").by(TILDE_ESCAPED); 1.10365 + innerHTML = wysihtml.lang.string(innerHTML).replace(urlToSearch).by(url); 1.10366 + } 1.10367 + return innerHTML; 1.10368 + }; 1.10369 +})(wysihtml); 1.10370 + 1.10371 +/** 1.10372 + * Force rerendering of a given element 1.10373 + * Needed to fix display misbehaviors of IE 1.10374 + * 1.10375 + * @param {Element} element The element object which needs to be rerendered 1.10376 + * @example 1.10377 + * wysihtml.quirks.redraw(document.body); 1.10378 + */ 1.10379 +(function(wysihtml) { 1.10380 + var CLASS_NAME = "wysihtml-quirks-redraw"; 1.10381 + 1.10382 + wysihtml.quirks.redraw = function(element) { 1.10383 + wysihtml.dom.addClass(element, CLASS_NAME); 1.10384 + wysihtml.dom.removeClass(element, CLASS_NAME); 1.10385 + 1.10386 + // Following hack is needed for firefox to make sure that image resize handles are properly removed 1.10387 + try { 1.10388 + var doc = element.ownerDocument; 1.10389 + doc.execCommand("italic", false, null); 1.10390 + doc.execCommand("italic", false, null); 1.10391 + } catch(e) {} 1.10392 + }; 1.10393 +})(wysihtml); 1.10394 + 1.10395 +(function(wysihtml) { 1.10396 + 1.10397 + // List of supported color format parsing methods 1.10398 + // If radix is not defined 10 is expected as default 1.10399 + var colorParseMethods = { 1.10400 + rgba : { 1.10401 + regex: /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i, 1.10402 + name: "rgba" 1.10403 + }, 1.10404 + rgb : { 1.10405 + regex: /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i, 1.10406 + name: "rgb" 1.10407 + }, 1.10408 + hex6 : { 1.10409 + regex: /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i, 1.10410 + name: "hex", 1.10411 + radix: 16 1.10412 + }, 1.10413 + hex3 : { 1.10414 + regex: /^#([0-9a-f])([0-9a-f])([0-9a-f])/i, 1.10415 + name: "hex", 1.10416 + radix: 16 1.10417 + } 1.10418 + }, 1.10419 + // 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 1.10420 + makeParamRegExp = function (p) { 1.10421 + return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+", "gi"); 1.10422 + }; 1.10423 + 1.10424 + // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns suitable parsing method for it 1.10425 + function getColorParseMethod (colorStr) { 1.10426 + var prop, colorTypeConf; 1.10427 + 1.10428 + for (prop in colorParseMethods) { 1.10429 + if (!colorParseMethods.hasOwnProperty(prop)) { continue; } 1.10430 + 1.10431 + colorTypeConf = colorParseMethods[prop]; 1.10432 + 1.10433 + if (colorTypeConf.regex.test(colorStr)) { 1.10434 + return colorTypeConf; 1.10435 + } 1.10436 + } 1.10437 + } 1.10438 + 1.10439 + // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns the type of that color format "hex", "rgb", "rgba". 1.10440 + function getColorFormat (colorStr) { 1.10441 + var type = getColorParseMethod(colorStr); 1.10442 + 1.10443 + return type ? type.name : undefined; 1.10444 + } 1.10445 + 1.10446 + // Public API functions for styleParser 1.10447 + wysihtml.quirks.styleParser = { 1.10448 + 1.10449 + // Takes color string value as an argument and returns suitable parsing method for it 1.10450 + getColorParseMethod : getColorParseMethod, 1.10451 + 1.10452 + // Takes color string value as an argument and returns the type of that color format "hex", "rgb", "rgba". 1.10453 + getColorFormat : getColorFormat, 1.10454 + 1.10455 + /* Parses a color string to and array of [red, green, blue, alpha]. 1.10456 + * paramName: optional argument to parse color value directly from style string parameter 1.10457 + * 1.10458 + * Examples: 1.10459 + * var colorArray = wysihtml.quirks.styleParser.parseColor("#ABC"); // [170, 187, 204, 1] 1.10460 + * var colorArray = wysihtml.quirks.styleParser.parseColor("#AABBCC"); // [170, 187, 204, 1] 1.10461 + * var colorArray = wysihtml.quirks.styleParser.parseColor("rgb(1,2,3)"); // [1, 2, 3, 1] 1.10462 + * var colorArray = wysihtml.quirks.styleParser.parseColor("rgba(1,2,3,0.5)"); // [1, 2, 3, 0.5] 1.10463 + * 1.10464 + * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1] 1.10465 + * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color"); // [0, 0, 0, 1] 1.10466 + */ 1.10467 + parseColor : function (stylesStr, paramName) { 1.10468 + var paramsRegex, params, colorType, colorMatch, radix, 1.10469 + colorStr = stylesStr; 1.10470 + 1.10471 + if (paramName) { 1.10472 + paramsRegex = makeParamRegExp(paramName); 1.10473 + 1.10474 + if (!(params = stylesStr.match(paramsRegex))) { return false; } 1.10475 + 1.10476 + params = params.pop().split(":")[1]; 1.10477 + colorStr = wysihtml.lang.string(params).trim(); 1.10478 + } 1.10479 + 1.10480 + if (!(colorType = getColorParseMethod(colorStr))) { return false; } 1.10481 + if (!(colorMatch = colorStr.match(colorType.regex))) { return false; } 1.10482 + 1.10483 + radix = colorType.radix || 10; 1.10484 + 1.10485 + if (colorType === colorParseMethods.hex3) { 1.10486 + colorMatch.shift(); 1.10487 + colorMatch.push(1); 1.10488 + return wysihtml.lang.array(colorMatch).map(function(d, idx) { 1.10489 + return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d); 1.10490 + }); 1.10491 + } 1.10492 + 1.10493 + colorMatch.shift(); 1.10494 + 1.10495 + if (!colorMatch[3]) { 1.10496 + colorMatch.push(1); 1.10497 + } 1.10498 + 1.10499 + return wysihtml.lang.array(colorMatch).map(function(d, idx) { 1.10500 + return (idx < 3) ? parseInt(d, radix): parseFloat(d); 1.10501 + }); 1.10502 + }, 1.10503 + 1.10504 + /* Takes rgba color array [r,g,b,a] as a value and formats it to color string with given format type 1.10505 + * If no format is given, rgba/rgb is returned based on alpha value 1.10506 + * 1.10507 + * Example: 1.10508 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash"); // "#AABBCC" 1.10509 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex"); // "AABBCC" 1.10510 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv"); // "170, 187, 204, 1" 1.10511 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba"); // "rgba(170,187,204,1)" 1.10512 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgb"); // "rgb(170,187,204)" 1.10513 + * 1.10514 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 0.5]); // "rgba(170,187,204,0.5)" 1.10515 + * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1]); // "rgb(170,187,204)" 1.10516 + */ 1.10517 + unparseColor: function(val, colorFormat) { 1.10518 + var hexRadix = 16; 1.10519 + 1.10520 + if (colorFormat === "hex") { 1.10521 + return (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase(); 1.10522 + } else if (colorFormat === "hash") { 1.10523 + return "#" + (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase(); 1.10524 + } else if (colorFormat === "rgb") { 1.10525 + return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")"; 1.10526 + } else if (colorFormat === "rgba") { 1.10527 + return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")"; 1.10528 + } else if (colorFormat === "csv") { 1.10529 + return val[0] + "," + val[1] + "," + val[2] + "," + val[3]; 1.10530 + } 1.10531 + 1.10532 + if (val[3] && val[3] !== 1) { 1.10533 + return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")"; 1.10534 + } else { 1.10535 + return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")"; 1.10536 + } 1.10537 + }, 1.10538 + 1.10539 + // Parses font size value from style string 1.10540 + parseFontSize: function(stylesStr) { 1.10541 + var params = stylesStr.match(makeParamRegExp("font-size")); 1.10542 + if (params) { 1.10543 + return wysihtml.lang.string(params[params.length - 1].split(":")[1]).trim(); 1.10544 + } 1.10545 + return false; 1.10546 + } 1.10547 + }; 1.10548 + 1.10549 +})(wysihtml); 1.10550 + 1.10551 +/** 1.10552 + * Selection API 1.10553 + * 1.10554 + * @example 1.10555 + * var selection = new wysihtml.Selection(editor); 1.10556 + */ 1.10557 +(function(wysihtml) { 1.10558 + var dom = wysihtml.dom; 1.10559 + 1.10560 + function _getCumulativeOffsetTop(element) { 1.10561 + var top = 0; 1.10562 + if (element.parentNode) { 1.10563 + do { 1.10564 + top += element.offsetTop || 0; 1.10565 + element = element.offsetParent; 1.10566 + } while (element); 1.10567 + } 1.10568 + return top; 1.10569 + } 1.10570 + 1.10571 + // Provides the depth of ``descendant`` relative to ``ancestor`` 1.10572 + function getDepth(ancestor, descendant) { 1.10573 + var ret = 0; 1.10574 + while (descendant !== ancestor) { 1.10575 + ret++; 1.10576 + descendant = descendant.parentNode; 1.10577 + if (!descendant) 1.10578 + throw new Error("not a descendant of ancestor!"); 1.10579 + } 1.10580 + return ret; 1.10581 + } 1.10582 + 1.10583 + function getRangeNode(node, offset) { 1.10584 + if (node.nodeType === 3) { 1.10585 + return node; 1.10586 + } else { 1.10587 + return node.childNodes[offset] || node; 1.10588 + } 1.10589 + } 1.10590 + 1.10591 + function getWebkitSelectionFixNode(container) { 1.10592 + var blankNode = document.createElement('span'); 1.10593 + 1.10594 + var placeholderRemover = function(event) { 1.10595 + // Self-destructs the caret and keeps the text inserted into it by user 1.10596 + var lastChild; 1.10597 + 1.10598 + container.removeEventListener('mouseup', placeholderRemover); 1.10599 + container.removeEventListener('keydown', placeholderRemover); 1.10600 + container.removeEventListener('touchstart', placeholderRemover); 1.10601 + container.removeEventListener('focus', placeholderRemover); 1.10602 + container.removeEventListener('blur', placeholderRemover); 1.10603 + container.removeEventListener('paste', delayedPlaceholderRemover); 1.10604 + container.removeEventListener('drop', delayedPlaceholderRemover); 1.10605 + container.removeEventListener('beforepaste', delayedPlaceholderRemover); 1.10606 + 1.10607 + if (blankNode && blankNode.parentNode) { 1.10608 + blankNode.parentNode.removeChild(blankNode); 1.10609 + } 1.10610 + }, 1.10611 + delayedPlaceholderRemover = function (event) { 1.10612 + if (blankNode && blankNode.parentNode) { 1.10613 + setTimeout(placeholderRemover, 0); 1.10614 + } 1.10615 + }; 1.10616 + 1.10617 + blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml.INVISIBLE_SPACE)); 1.10618 + blankNode.className = '_wysihtml-temp-caret-fix'; 1.10619 + blankNode.style.display = 'block'; 1.10620 + blankNode.style.minWidth = '1px'; 1.10621 + blankNode.style.height = '0px'; 1.10622 + 1.10623 + container.addEventListener('mouseup', placeholderRemover); 1.10624 + container.addEventListener('keydown', placeholderRemover); 1.10625 + container.addEventListener('touchstart', placeholderRemover); 1.10626 + container.addEventListener('focus', placeholderRemover); 1.10627 + container.addEventListener('blur', placeholderRemover); 1.10628 + container.addEventListener('paste', delayedPlaceholderRemover); 1.10629 + container.addEventListener('drop', delayedPlaceholderRemover); 1.10630 + container.addEventListener('beforepaste', delayedPlaceholderRemover); 1.10631 + 1.10632 + return blankNode; 1.10633 + } 1.10634 + 1.10635 + // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon 1.10636 + // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection 1.10637 + function expandRangeToSurround(range) { 1.10638 + if (range.canSurroundContents()) return; 1.10639 + 1.10640 + var common = range.commonAncestorContainer, 1.10641 + start_depth = getDepth(common, range.startContainer), 1.10642 + end_depth = getDepth(common, range.endContainer); 1.10643 + 1.10644 + while(!range.canSurroundContents()) { 1.10645 + // 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. 1.10646 + if (start_depth > end_depth) { 1.10647 + range.setStartBefore(range.startContainer); 1.10648 + start_depth = getDepth(common, range.startContainer); 1.10649 + } 1.10650 + else { 1.10651 + range.setEndAfter(range.endContainer); 1.10652 + end_depth = getDepth(common, range.endContainer); 1.10653 + } 1.10654 + } 1.10655 + } 1.10656 + 1.10657 + wysihtml.Selection = Base.extend( 1.10658 + /** @scope wysihtml.Selection.prototype */ { 1.10659 + constructor: function(editor, contain, unselectableClass) { 1.10660 + // Make sure that our external range library is initialized 1.10661 + rangy.init(); 1.10662 + 1.10663 + this.editor = editor; 1.10664 + this.composer = editor.composer; 1.10665 + this.doc = this.composer.doc; 1.10666 + this.win = this.composer.win; 1.10667 + this.contain = contain; 1.10668 + this.unselectableClass = unselectableClass || false; 1.10669 + }, 1.10670 + 1.10671 + /** 1.10672 + * Get the current selection as a bookmark to be able to later restore it 1.10673 + * 1.10674 + * @return {Object} An object that represents the current selection 1.10675 + */ 1.10676 + getBookmark: function() { 1.10677 + var range = this.getRange(); 1.10678 + return range && range.cloneRange(); 1.10679 + }, 1.10680 + 1.10681 + /** 1.10682 + * Restore a selection retrieved via wysihtml.Selection.prototype.getBookmark 1.10683 + * 1.10684 + * @param {Object} bookmark An object that represents the current selection 1.10685 + */ 1.10686 + setBookmark: function(bookmark) { 1.10687 + if (!bookmark) { 1.10688 + return; 1.10689 + } 1.10690 + 1.10691 + this.setSelection(bookmark); 1.10692 + }, 1.10693 + 1.10694 + /** 1.10695 + * Set the caret in front of the given node 1.10696 + * 1.10697 + * @param {Object} node The element or text node where to position the caret in front of 1.10698 + * @example 1.10699 + * selection.setBefore(myElement); 1.10700 + */ 1.10701 + setBefore: function(node) { 1.10702 + var range = rangy.createRange(this.doc); 1.10703 + range.setStartBefore(node); 1.10704 + range.setEndBefore(node); 1.10705 + return this.setSelection(range); 1.10706 + }, 1.10707 + 1.10708 + // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail. 1.10709 + // Webkit has an issue with placing caret into places where there are no textnodes near by. 1.10710 + createTemporaryCaretSpaceAfter: function (node) { 1.10711 + var caretPlaceholder = this.doc.createElement('span'), 1.10712 + caretPlaceholderText = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE), 1.10713 + placeholderRemover = (function(event) { 1.10714 + // Self-destructs the caret and keeps the text inserted into it by user 1.10715 + var lastChild; 1.10716 + 1.10717 + this.contain.removeEventListener('mouseup', placeholderRemover); 1.10718 + this.contain.removeEventListener('keydown', keyDownHandler); 1.10719 + this.contain.removeEventListener('touchstart', placeholderRemover); 1.10720 + this.contain.removeEventListener('focus', placeholderRemover); 1.10721 + this.contain.removeEventListener('blur', placeholderRemover); 1.10722 + this.contain.removeEventListener('paste', delayedPlaceholderRemover); 1.10723 + this.contain.removeEventListener('drop', delayedPlaceholderRemover); 1.10724 + this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover); 1.10725 + 1.10726 + // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack 1.10727 + // Otherwise the wrapper can just be removed 1.10728 + if (caretPlaceholder && caretPlaceholder.parentNode) { 1.10729 + caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); 1.10730 + if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) { 1.10731 + lastChild = caretPlaceholder.lastChild; 1.10732 + wysihtml.dom.unwrap(caretPlaceholder); 1.10733 + this.setAfter(lastChild); 1.10734 + } else { 1.10735 + caretPlaceholder.parentNode.removeChild(caretPlaceholder); 1.10736 + } 1.10737 + 1.10738 + } 1.10739 + }).bind(this), 1.10740 + delayedPlaceholderRemover = function (event) { 1.10741 + if (caretPlaceholder && caretPlaceholder.parentNode) { 1.10742 + setTimeout(placeholderRemover, 0); 1.10743 + } 1.10744 + }, 1.10745 + keyDownHandler = function(event) { 1.10746 + if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) { 1.10747 + placeholderRemover(); 1.10748 + } 1.10749 + }; 1.10750 + 1.10751 + caretPlaceholder.className = '_wysihtml-temp-caret-fix'; 1.10752 + caretPlaceholder.style.position = 'absolute'; 1.10753 + caretPlaceholder.style.display = 'block'; 1.10754 + caretPlaceholder.style.minWidth = '1px'; 1.10755 + caretPlaceholder.style.zIndex = '99999'; 1.10756 + caretPlaceholder.appendChild(caretPlaceholderText); 1.10757 + 1.10758 + node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); 1.10759 + this.setBefore(caretPlaceholderText); 1.10760 + 1.10761 + // Remove the caret fix on any of the following events (some are delayed as content change happens after event) 1.10762 + this.contain.addEventListener('mouseup', placeholderRemover); 1.10763 + this.contain.addEventListener('keydown', keyDownHandler); 1.10764 + this.contain.addEventListener('touchstart', placeholderRemover); 1.10765 + this.contain.addEventListener('focus', placeholderRemover); 1.10766 + this.contain.addEventListener('blur', placeholderRemover); 1.10767 + this.contain.addEventListener('paste', delayedPlaceholderRemover); 1.10768 + this.contain.addEventListener('drop', delayedPlaceholderRemover); 1.10769 + this.contain.addEventListener('beforepaste', delayedPlaceholderRemover); 1.10770 + 1.10771 + return caretPlaceholder; 1.10772 + }, 1.10773 + 1.10774 + /** 1.10775 + * Set the caret after the given node 1.10776 + * 1.10777 + * @param {Object} node The element or text node where to position the caret in front of 1.10778 + * @example 1.10779 + * selection.setBefore(myElement); 1.10780 + * callback is an optional parameter accepting a function to execute when selection ahs been set 1.10781 + */ 1.10782 + setAfter: function(node, notVisual, callback) { 1.10783 + var win = this.win, 1.10784 + range = rangy.createRange(this.doc), 1.10785 + fixWebkitSelection = function() { 1.10786 + // Webkit fails to add selection if there are no textnodes in that region 1.10787 + // (like an uneditable container at the end of content). 1.10788 + var parent = node.parentNode, 1.10789 + lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null; 1.10790 + 1.10791 + if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) { 1.10792 + if (notVisual) { 1.10793 + // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation 1.10794 + // and remove itself in call stack end instead on user interaction 1.10795 + var caretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE); 1.10796 + node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); 1.10797 + this.selectNode(caretPlaceholder); 1.10798 + setTimeout(function() { 1.10799 + if (caretPlaceholder && caretPlaceholder.parentNode) { 1.10800 + caretPlaceholder.parentNode.removeChild(caretPlaceholder); 1.10801 + } 1.10802 + }, 0); 1.10803 + } else { 1.10804 + this.createTemporaryCaretSpaceAfter(node); 1.10805 + } 1.10806 + } 1.10807 + }.bind(this), 1.10808 + sel; 1.10809 + 1.10810 + range.setStartAfter(node); 1.10811 + range.setEndAfter(node); 1.10812 + 1.10813 + // In IE contenteditable must be focused before we can set selection 1.10814 + // thus setting the focus if activeElement is not this composer 1.10815 + if (!document.activeElement || document.activeElement !== this.composer.element) { 1.10816 + var scrollPos = this.composer.getScrollPos(); 1.10817 + this.composer.element.focus(); 1.10818 + this.composer.setScrollPos(scrollPos); 1.10819 + setTimeout(function() { 1.10820 + sel = this.setSelection(range); 1.10821 + fixWebkitSelection(); 1.10822 + if (callback) { 1.10823 + callback(sel); 1.10824 + } 1.10825 + }.bind(this), 0); 1.10826 + } else { 1.10827 + sel = this.setSelection(range); 1.10828 + fixWebkitSelection(); 1.10829 + if (callback) { 1.10830 + callback(sel); 1.10831 + } 1.10832 + } 1.10833 + }, 1.10834 + 1.10835 + /** 1.10836 + * Ability to select/mark nodes 1.10837 + * 1.10838 + * @param {Element} node The node/element to select 1.10839 + * @example 1.10840 + * selection.selectNode(document.getElementById("my-image")); 1.10841 + */ 1.10842 + selectNode: function(node, avoidInvisibleSpace) { 1.10843 + var range = rangy.createRange(this.doc), 1.10844 + isElement = node.nodeType === wysihtml.ELEMENT_NODE, 1.10845 + canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), 1.10846 + content = isElement ? node.innerHTML : node.data, 1.10847 + isEmpty = (content === "" || content === wysihtml.INVISIBLE_SPACE), 1.10848 + displayStyle = dom.getStyle("display").from(node), 1.10849 + isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); 1.10850 + 1.10851 + if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) { 1.10852 + // Make sure that caret is visible in node by inserting a zero width no breaking space 1.10853 + try { node.innerHTML = wysihtml.INVISIBLE_SPACE; } catch(e) {} 1.10854 + } 1.10855 + if (canHaveHTML) { 1.10856 + range.selectNodeContents(node); 1.10857 + } else { 1.10858 + range.selectNode(node); 1.10859 + } 1.10860 + 1.10861 + if (canHaveHTML && isEmpty && isElement) { 1.10862 + range.collapse(isBlockElement); 1.10863 + } else if (canHaveHTML && isEmpty) { 1.10864 + range.setStartAfter(node); 1.10865 + range.setEndAfter(node); 1.10866 + } 1.10867 + 1.10868 + this.setSelection(range); 1.10869 + }, 1.10870 + 1.10871 + /** 1.10872 + * Get the node which contains the selection 1.10873 + * 1.10874 + * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" 1.10875 + * @return {Object} The node that contains the caret 1.10876 + * @example 1.10877 + * var nodeThatContainsCaret = selection.getSelectedNode(); 1.10878 + */ 1.10879 + getSelectedNode: function(controlRange) { 1.10880 + var selection, 1.10881 + range; 1.10882 + 1.10883 + if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { 1.10884 + range = this.doc.selection.createRange(); 1.10885 + if (range && range.length) { 1.10886 + return range.item(0); 1.10887 + } 1.10888 + } 1.10889 + 1.10890 + selection = this.getSelection(this.doc); 1.10891 + if (selection.focusNode === selection.anchorNode) { 1.10892 + return selection.focusNode; 1.10893 + } else { 1.10894 + range = this.getRange(this.doc); 1.10895 + return range ? range.commonAncestorContainer : this.doc.body; 1.10896 + } 1.10897 + }, 1.10898 + 1.10899 + fixSelBorders: function() { 1.10900 + var range = this.getRange(); 1.10901 + expandRangeToSurround(range); 1.10902 + this.setSelection(range); 1.10903 + }, 1.10904 + 1.10905 + getSelectedOwnNodes: function(controlRange) { 1.10906 + var selection, 1.10907 + ranges = this.getOwnRanges(), 1.10908 + ownNodes = []; 1.10909 + 1.10910 + for (var i = 0, maxi = ranges.length; i < maxi; i++) { 1.10911 + ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body); 1.10912 + } 1.10913 + return ownNodes; 1.10914 + }, 1.10915 + 1.10916 + findNodesInSelection: function(nodeTypes) { 1.10917 + var ranges = this.getOwnRanges(), 1.10918 + nodes = [], curNodes; 1.10919 + for (var i = 0, maxi = ranges.length; i < maxi; i++) { 1.10920 + curNodes = ranges[i].getNodes([1], function(node) { 1.10921 + return wysihtml.lang.array(nodeTypes).contains(node.nodeName); 1.10922 + }); 1.10923 + nodes = nodes.concat(curNodes); 1.10924 + } 1.10925 + return nodes; 1.10926 + }, 1.10927 + 1.10928 + filterElements: function(filter) { 1.10929 + var ranges = this.getOwnRanges(), 1.10930 + nodes = [], curNodes; 1.10931 + 1.10932 + for (var i = 0, maxi = ranges.length; i < maxi; i++) { 1.10933 + curNodes = ranges[i].getNodes([1], function(element){ 1.10934 + return filter(element, ranges[i]); 1.10935 + }); 1.10936 + nodes = nodes.concat(curNodes); 1.10937 + } 1.10938 + return nodes; 1.10939 + }, 1.10940 + 1.10941 + containsUneditable: function() { 1.10942 + var uneditables = this.getOwnUneditables(), 1.10943 + selection = this.getSelection(); 1.10944 + 1.10945 + for (var i = 0, maxi = uneditables.length; i < maxi; i++) { 1.10946 + if (selection.containsNode(uneditables[i])) { 1.10947 + return true; 1.10948 + } 1.10949 + } 1.10950 + 1.10951 + return false; 1.10952 + }, 1.10953 + 1.10954 + // Deletes selection contents making sure uneditables/unselectables are not partially deleted 1.10955 + // Triggers wysihtml:uneditable:delete custom event on all deleted uneditables if customevents suppoorted 1.10956 + deleteContents: function() { 1.10957 + var range = this.getRange(); 1.10958 + this.deleteRangeContents(range); 1.10959 + this.setSelection(range); 1.10960 + }, 1.10961 + 1.10962 + // Makes sure all uneditable sare notified before deleting contents 1.10963 + deleteRangeContents: function (range) { 1.10964 + var startParent, endParent, uneditables, ev; 1.10965 + 1.10966 + if (this.unselectableClass) { 1.10967 + if ((startParent = wysihtml.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) { 1.10968 + range.setStartBefore(startParent); 1.10969 + } 1.10970 + if ((endParent = wysihtml.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) { 1.10971 + range.setEndAfter(endParent); 1.10972 + } 1.10973 + 1.10974 + // If customevents present notify uneditable elements of being deleted 1.10975 + uneditables = range.getNodes([1], (function (node) { 1.10976 + return wysihtml.dom.hasClass(node, this.unselectableClass); 1.10977 + }).bind(this)); 1.10978 + for (var i = uneditables.length; i--;) { 1.10979 + try { 1.10980 + ev = new CustomEvent("wysihtml:uneditable:delete"); 1.10981 + uneditables[i].dispatchEvent(ev); 1.10982 + } catch (err) {} 1.10983 + } 1.10984 + } 1.10985 + range.deleteContents(); 1.10986 + }, 1.10987 + 1.10988 + getCaretNode: function () { 1.10989 + var selection = this.getSelection(); 1.10990 + return (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null; 1.10991 + }, 1.10992 + 1.10993 + getPreviousNode: function(node, ignoreEmpty) { 1.10994 + var displayStyle; 1.10995 + if (!node) { 1.10996 + var selection = this.getSelection(); 1.10997 + node = (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null; 1.10998 + } 1.10999 + 1.11000 + if (node === this.contain) { 1.11001 + return false; 1.11002 + } 1.11003 + 1.11004 + var ret = node.previousSibling, 1.11005 + parent; 1.11006 + 1.11007 + if (ret === this.contain) { 1.11008 + return false; 1.11009 + } 1.11010 + 1.11011 + if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) { 1.11012 + // do not count comments and other node types 1.11013 + ret = this.getPreviousNode(ret, ignoreEmpty); 1.11014 + } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) { 1.11015 + // do not count empty textnodes as previous nodes 1.11016 + ret = this.getPreviousNode(ret, ignoreEmpty); 1.11017 + } else if (ignoreEmpty && ret && ret.nodeType === 1) { 1.11018 + // Do not count empty nodes if param set. 1.11019 + // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like 1.11020 + displayStyle = wysihtml.dom.getStyle("display").from(ret); 1.11021 + if ( 1.11022 + !wysihtml.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) && 1.11023 + !wysihtml.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) && 1.11024 + (/^[\s]*$/).test(ret.innerHTML) 1.11025 + ) { 1.11026 + ret = this.getPreviousNode(ret, ignoreEmpty); 1.11027 + } 1.11028 + } else if (!ret && node !== this.contain) { 1.11029 + parent = node.parentNode; 1.11030 + if (parent !== this.contain) { 1.11031 + ret = this.getPreviousNode(parent, ignoreEmpty); 1.11032 + } 1.11033 + } 1.11034 + 1.11035 + return (ret !== this.contain) ? ret : false; 1.11036 + }, 1.11037 + 1.11038 + // Gather info about caret location (caret node, previous and next node) 1.11039 + getNodesNearCaret: function() { 1.11040 + if (!this.isCollapsed()) { 1.11041 + throw "Selection must be caret when using selection.getNodesNearCaret()"; 1.11042 + } 1.11043 + 1.11044 + var r = this.getOwnRanges(), 1.11045 + caretNode, prevNode, nextNode, offset; 1.11046 + 1.11047 + if (r && r.length > 0) { 1.11048 + if (r[0].startContainer.nodeType === 1) { 1.11049 + caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1]; 1.11050 + if (!caretNode && r[0].startOffset === 0) { 1.11051 + // Is first position before all nodes 1.11052 + nextNode = r[0].startContainer.childNodes[0]; 1.11053 + } else if (caretNode) { 1.11054 + prevNode = caretNode.previousSibling; 1.11055 + nextNode = caretNode.nextSibling; 1.11056 + } 1.11057 + } else { 1.11058 + if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) { 1.11059 + caretNode = r[0].startContainer.previousSibling; 1.11060 + if (caretNode.nodeType === 3) { 1.11061 + offset = caretNode.data.length; 1.11062 + } 1.11063 + } else { 1.11064 + caretNode = r[0].startContainer; 1.11065 + offset = r[0].startOffset; 1.11066 + } 1.11067 + prevNode = caretNode.previousSibling; 1.11068 + nextNode = caretNode.nextSibling; 1.11069 + } 1.11070 + 1.11071 + return { 1.11072 + "caretNode": caretNode, 1.11073 + "prevNode": prevNode, 1.11074 + "nextNode": nextNode, 1.11075 + "textOffset": offset 1.11076 + }; 1.11077 + } 1.11078 + 1.11079 + return null; 1.11080 + }, 1.11081 + 1.11082 + getSelectionParentsByTag: function(tagName) { 1.11083 + var nodes = this.getSelectedOwnNodes(), 1.11084 + curEl, parents = []; 1.11085 + 1.11086 + for (var i = 0, maxi = nodes.length; i < maxi; i++) { 1.11087 + curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain); 1.11088 + if (curEl) { 1.11089 + parents.push(curEl); 1.11090 + } 1.11091 + } 1.11092 + return (parents.length) ? parents : null; 1.11093 + }, 1.11094 + 1.11095 + getRangeToNodeEnd: function() { 1.11096 + if (this.isCollapsed()) { 1.11097 + var range = this.getRange(), 1.11098 + sNode, pos, lastR; 1.11099 + if (range) { 1.11100 + sNode = range.startContainer; 1.11101 + pos = range.startOffset; 1.11102 + lastR = rangy.createRange(this.doc); 1.11103 + 1.11104 + lastR.selectNodeContents(sNode); 1.11105 + lastR.setStart(sNode, pos); 1.11106 + return lastR; 1.11107 + } 1.11108 + } 1.11109 + }, 1.11110 + 1.11111 + getRangeToNodeBeginning: function() { 1.11112 + if (this.isCollapsed()) { 1.11113 + var range = this.getRange(), 1.11114 + sNode = range.startContainer, 1.11115 + pos = range.startOffset, 1.11116 + lastR = rangy.createRange(this.doc); 1.11117 + 1.11118 + lastR.selectNodeContents(sNode); 1.11119 + lastR.setEnd(sNode, pos); 1.11120 + return lastR; 1.11121 + } 1.11122 + }, 1.11123 + 1.11124 + // This function returns if caret is last in a node (no textual visible content follows) 1.11125 + caretIsInTheEndOfNode: function(ignoreIfSpaceIsBeforeCaret) { 1.11126 + var r = rangy.createRange(this.doc), 1.11127 + s = this.getSelection(), 1.11128 + rangeToNodeEnd = this.getRangeToNodeEnd(), 1.11129 + endc, endtxt, beginc, begintxt; 1.11130 + 1.11131 + if (rangeToNodeEnd) { 1.11132 + endc = rangeToNodeEnd.cloneContents(); 1.11133 + endtxt = endc.textContent; 1.11134 + 1.11135 + if ((/^\s*$/).test(endtxt)) { 1.11136 + if (ignoreIfSpaceIsBeforeCaret) { 1.11137 + beginc = this.getRangeToNodeBeginning().cloneContents(); 1.11138 + begintxt = beginc.textContent; 1.11139 + return !(/[\u00A0 ][\s\uFEFF]*$/).test(begintxt); 1.11140 + } else { 1.11141 + return true; 1.11142 + } 1.11143 + } else { 1.11144 + return false; 1.11145 + } 1.11146 + } else { 1.11147 + return false; 1.11148 + } 1.11149 + }, 1.11150 + 1.11151 + caretIsFirstInSelection: function(includeLineBreaks) { 1.11152 + var r = rangy.createRange(this.doc), 1.11153 + s = this.getSelection(), 1.11154 + range = this.getRange(), 1.11155 + startNode = getRangeNode(range.startContainer, range.startOffset); 1.11156 + 1.11157 + if (startNode) { 1.11158 + if (startNode.nodeType === wysihtml.TEXT_NODE) { 1.11159 + if (!startNode.parentNode) { 1.11160 + return false; 1.11161 + } 1.11162 + if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml.dom.domNode(startNode.previousSibling).is.block())) { 1.11163 + return false; 1.11164 + } 1.11165 + var ws = this.win.getComputedStyle(startNode.parentNode).whiteSpace; 1.11166 + return (ws === "pre" || ws === "pre-wrap") ? range.startOffset === 0 : (/^\s*$/).test(startNode.data.substr(0,range.startOffset)); 1.11167 + } else if (includeLineBreaks && wysihtml.dom.domNode(startNode).is.lineBreak()) { 1.11168 + return true; 1.11169 + } else { 1.11170 + r.selectNodeContents(this.getRange().commonAncestorContainer); 1.11171 + r.collapse(true); 1.11172 + return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset); 1.11173 + } 1.11174 + } 1.11175 + }, 1.11176 + 1.11177 + caretIsInTheBeginnig: function(ofNode) { 1.11178 + var selection = this.getSelection(), 1.11179 + node = selection.anchorNode, 1.11180 + offset = selection.anchorOffset; 1.11181 + if (ofNode && node) { 1.11182 + return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml.dom.getParentElement(node.parentNode, { query: ofNode }, 1))); 1.11183 + } else if (node) { 1.11184 + return (offset === 0 && !this.getPreviousNode(node, true)); 1.11185 + } 1.11186 + }, 1.11187 + 1.11188 + // Returns object describing node/text before selection 1.11189 + // If includePrevLeaves is true returns also previous last leaf child if selection is in the beginning of current node 1.11190 + getBeforeSelection: function(includePrevLeaves) { 1.11191 + var sel = this.getSelection(), 1.11192 + startNode = (sel.isBackwards()) ? sel.focusNode : sel.anchorNode, 1.11193 + startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset, 1.11194 + rng = this.createRange(), endNode, inTmpCaret; 1.11195 + 1.11196 + // If start is textnode and all is whitespace before caret. Set start offset to 0 1.11197 + if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) { 1.11198 + startOffset = 0; 1.11199 + } 1.11200 + 1.11201 + // Escape temproray helper nodes if selection in them 1.11202 + inTmpCaret = wysihtml.dom.getParentElement(startNode, { query: '._wysihtml-temp-caret-fix' }, 1); 1.11203 + if (inTmpCaret) { 1.11204 + startNode = inTmpCaret.parentNode; 1.11205 + startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret); 1.11206 + } 1.11207 + 1.11208 + if (startNode) { 1.11209 + if (startOffset > 0) { 1.11210 + if (startNode.nodeType === 3) { 1.11211 + rng.setStart(startNode, 0); 1.11212 + rng.setEnd(startNode, startOffset); 1.11213 + return { 1.11214 + type: "text", 1.11215 + range: rng, 1.11216 + offset : startOffset, 1.11217 + node: startNode 1.11218 + }; 1.11219 + } else { 1.11220 + rng.setStartBefore(startNode.childNodes[0]); 1.11221 + endNode = startNode.childNodes[startOffset - 1]; 1.11222 + rng.setEndAfter(endNode); 1.11223 + return { 1.11224 + type: "element", 1.11225 + range: rng, 1.11226 + offset : startOffset, 1.11227 + node: endNode 1.11228 + }; 1.11229 + } 1.11230 + } else { 1.11231 + rng.setStartAndEnd(startNode, 0); 1.11232 + 1.11233 + if (includePrevLeaves) { 1.11234 + var prevNode = this.getPreviousNode(startNode, true), 1.11235 + prevLeaf = null; 1.11236 + 1.11237 + if(prevNode) { 1.11238 + if (prevNode.nodeType === 1 && wysihtml.dom.hasClass(prevNode, this.unselectableClass)) { 1.11239 + prevLeaf = prevNode; 1.11240 + } else { 1.11241 + prevLeaf = wysihtml.dom.domNode(prevNode).lastLeafNode(); 1.11242 + } 1.11243 + } 1.11244 + 1.11245 + if (prevLeaf) { 1.11246 + return { 1.11247 + type: "leafnode", 1.11248 + range: rng, 1.11249 + offset : startOffset, 1.11250 + node: prevLeaf 1.11251 + }; 1.11252 + } 1.11253 + } 1.11254 + 1.11255 + return { 1.11256 + type: "none", 1.11257 + range: rng, 1.11258 + offset : startOffset, 1.11259 + node: startNode 1.11260 + }; 1.11261 + } 1.11262 + } 1.11263 + return null; 1.11264 + }, 1.11265 + 1.11266 + // TODO: Figure out a method from following 2 that would work universally 1.11267 + executeAndRestoreRangy: function(method, restoreScrollPosition) { 1.11268 + var sel = rangy.saveSelection(this.win); 1.11269 + if (!sel) { 1.11270 + method(); 1.11271 + } else { 1.11272 + try { 1.11273 + method(); 1.11274 + } catch(e) { 1.11275 + setTimeout(function() { throw e; }, 0); 1.11276 + } 1.11277 + } 1.11278 + rangy.restoreSelection(sel); 1.11279 + }, 1.11280 + 1.11281 + // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween 1.11282 + executeAndRestore: function(method, restoreScrollPosition) { 1.11283 + var body = this.doc.body, 1.11284 + oldScrollTop = restoreScrollPosition && body.scrollTop, 1.11285 + oldScrollLeft = restoreScrollPosition && body.scrollLeft, 1.11286 + className = "_wysihtml-temp-placeholder", 1.11287 + placeholderHtml = '<span class="' + className + '">' + wysihtml.INVISIBLE_SPACE + '</span>', 1.11288 + range = this.getRange(true), 1.11289 + caretPlaceholder, 1.11290 + newCaretPlaceholder, 1.11291 + nextSibling, prevSibling, 1.11292 + node, node2, range2, 1.11293 + newRange; 1.11294 + 1.11295 + // Nothing selected, execute and say goodbye 1.11296 + if (!range) { 1.11297 + method(body, body); 1.11298 + return; 1.11299 + } 1.11300 + 1.11301 + if (!range.collapsed) { 1.11302 + range2 = range.cloneRange(); 1.11303 + node2 = range2.createContextualFragment(placeholderHtml); 1.11304 + range2.collapse(false); 1.11305 + range2.insertNode(node2); 1.11306 + range2.detach(); 1.11307 + } 1.11308 + 1.11309 + node = range.createContextualFragment(placeholderHtml); 1.11310 + range.insertNode(node); 1.11311 + 1.11312 + if (node2) { 1.11313 + caretPlaceholder = this.contain.querySelectorAll("." + className); 1.11314 + range.setStartBefore(caretPlaceholder[0]); 1.11315 + range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]); 1.11316 + } 1.11317 + this.setSelection(range); 1.11318 + 1.11319 + // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder 1.11320 + try { 1.11321 + method(range.startContainer, range.endContainer); 1.11322 + } catch(e) { 1.11323 + setTimeout(function() { throw e; }, 0); 1.11324 + } 1.11325 + caretPlaceholder = this.contain.querySelectorAll("." + className); 1.11326 + if (caretPlaceholder && caretPlaceholder.length) { 1.11327 + newRange = rangy.createRange(this.doc); 1.11328 + nextSibling = caretPlaceholder[0].nextSibling; 1.11329 + if (caretPlaceholder.length > 1) { 1.11330 + prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling; 1.11331 + } 1.11332 + if (prevSibling && nextSibling) { 1.11333 + newRange.setStartBefore(nextSibling); 1.11334 + newRange.setEndAfter(prevSibling); 1.11335 + } else { 1.11336 + newCaretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE); 1.11337 + dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]); 1.11338 + newRange.setStartBefore(newCaretPlaceholder); 1.11339 + newRange.setEndAfter(newCaretPlaceholder); 1.11340 + } 1.11341 + this.setSelection(newRange); 1.11342 + for (var i = caretPlaceholder.length; i--;) { 1.11343 + caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]); 1.11344 + } 1.11345 + 1.11346 + } else { 1.11347 + // fallback for when all hell breaks loose 1.11348 + this.contain.focus(); 1.11349 + } 1.11350 + 1.11351 + if (restoreScrollPosition) { 1.11352 + body.scrollTop = oldScrollTop; 1.11353 + body.scrollLeft = oldScrollLeft; 1.11354 + } 1.11355 + 1.11356 + // Remove it again, just to make sure that the placeholder is definitely out of the dom tree 1.11357 + try { 1.11358 + caretPlaceholder.parentNode.removeChild(caretPlaceholder); 1.11359 + } catch(e2) {} 1.11360 + }, 1.11361 + 1.11362 + set: function(node, offset) { 1.11363 + var newRange = rangy.createRange(this.doc); 1.11364 + newRange.setStart(node, offset || 0); 1.11365 + this.setSelection(newRange); 1.11366 + }, 1.11367 + 1.11368 + /** 1.11369 + * Insert html at the caret or selection position and move the cursor after the inserted html 1.11370 + * Replaces selection content if present 1.11371 + * 1.11372 + * @param {String} html HTML string to insert 1.11373 + * @example 1.11374 + * selection.insertHTML("<p>foobar</p>"); 1.11375 + */ 1.11376 + insertHTML: function(html) { 1.11377 + var range = this.getRange(), 1.11378 + node = this.doc.createElement('DIV'), 1.11379 + fragment = this.doc.createDocumentFragment(), 1.11380 + lastChild, lastEditorElement; 1.11381 + 1.11382 + if (range) { 1.11383 + range.deleteContents(); 1.11384 + node.innerHTML = html; 1.11385 + lastChild = node.lastChild; 1.11386 + 1.11387 + while (node.firstChild) { 1.11388 + fragment.appendChild(node.firstChild); 1.11389 + } 1.11390 + range.insertNode(fragment); 1.11391 + 1.11392 + lastEditorElement = this.contain.lastChild; 1.11393 + while (lastEditorElement && lastEditorElement.nodeType === 3 && lastEditorElement.previousSibling && (/^\s*$/).test(lastEditorElement.data)) { 1.11394 + lastEditorElement = lastEditorElement.previousSibling; 1.11395 + } 1.11396 + 1.11397 + if (lastChild) { 1.11398 + // fixes some pad cases mostly on webkit where last nr is needed 1.11399 + if (lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === 1) { 1.11400 + this.contain.appendChild(this.doc.createElement('br')); 1.11401 + } 1.11402 + this.setAfter(lastChild); 1.11403 + } 1.11404 + } 1.11405 + }, 1.11406 + 1.11407 + /** 1.11408 + * Insert a node at the caret position and move the cursor behind it 1.11409 + * 1.11410 + * @param {Object} node HTML string to insert 1.11411 + * @example 1.11412 + * selection.insertNode(document.createTextNode("foobar")); 1.11413 + */ 1.11414 + insertNode: function(node) { 1.11415 + var range = this.getRange(); 1.11416 + if (range) { 1.11417 + range.deleteContents(); 1.11418 + range.insertNode(node); 1.11419 + } 1.11420 + }, 1.11421 + 1.11422 + canAppendChild: function (node) { 1.11423 + var anchorNode, anchorNodeTagNameLower, 1.11424 + voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"], 1.11425 + range = this.getRange(); 1.11426 + 1.11427 + anchorNode = node || range.startContainer; 1.11428 + 1.11429 + if (anchorNode) { 1.11430 + anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase(); 1.11431 + } 1.11432 + 1.11433 + return voidElements.indexOf(anchorNodeTagNameLower) === -1; 1.11434 + }, 1.11435 + 1.11436 + splitElementAtCaret: function (element, insertNode) { 1.11437 + var sel = this.getSelection(), 1.11438 + range, contentAfterRangeStart, 1.11439 + firstChild, lastChild, childNodes; 1.11440 + 1.11441 + if (sel.rangeCount > 0) { 1.11442 + range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with 1.11443 + 1.11444 + range.setEndAfter(element); // Place the end of the range after the element 1.11445 + contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment 1.11446 + 1.11447 + childNodes = contentAfterRangeStart.childNodes; 1.11448 + 1.11449 + // Empty elements are cleaned up from extracted content 1.11450 + for (var i = childNodes.length; i --;) { 1.11451 + if (!wysihtml.dom.domNode(childNodes[i]).is.visible()) { 1.11452 + contentAfterRangeStart.removeChild(childNodes[i]); 1.11453 + } 1.11454 + } 1.11455 + 1.11456 + element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling); 1.11457 + 1.11458 + if (insertNode) { 1.11459 + firstChild = insertNode.firstChild || insertNode; 1.11460 + lastChild = insertNode.lastChild || insertNode; 1.11461 + 1.11462 + element.parentNode.insertBefore(insertNode, element.nextSibling); 1.11463 + 1.11464 + // Select inserted node contents 1.11465 + if (firstChild && lastChild) { 1.11466 + range.setStartBefore(firstChild); 1.11467 + range.setEndAfter(lastChild); 1.11468 + this.setSelection(range); 1.11469 + } 1.11470 + } else { 1.11471 + range.setStartAfter(element); 1.11472 + range.setEndAfter(element); 1.11473 + } 1.11474 + 1.11475 + if (!wysihtml.dom.domNode(element).is.visible()) { 1.11476 + if (wysihtml.dom.getTextContent(element) === '') { 1.11477 + element.parentNode.removeChild(element); 1.11478 + } else { 1.11479 + element.parentNode.replaceChild(this.doc.createTextNode(" "), element); 1.11480 + } 1.11481 + } 1.11482 + 1.11483 + 1.11484 + } 1.11485 + }, 1.11486 + 1.11487 + /** 1.11488 + * Wraps current selection with the given node 1.11489 + * 1.11490 + * @param {Object} node The node to surround the selected elements with 1.11491 + */ 1.11492 + surround: function(nodeOptions) { 1.11493 + var ranges = this.getOwnRanges(), 1.11494 + node, nodes = []; 1.11495 + if (ranges.length == 0) { 1.11496 + return nodes; 1.11497 + } 1.11498 + 1.11499 + for (var i = ranges.length; i--;) { 1.11500 + node = this.doc.createElement(nodeOptions.nodeName); 1.11501 + nodes.push(node); 1.11502 + if (nodeOptions.className) { 1.11503 + node.className = nodeOptions.className; 1.11504 + } 1.11505 + if (nodeOptions.cssStyle) { 1.11506 + node.setAttribute('style', nodeOptions.cssStyle); 1.11507 + } 1.11508 + try { 1.11509 + // This only works when the range boundaries are not overlapping other elements 1.11510 + ranges[i].surroundContents(node); 1.11511 + this.selectNode(node); 1.11512 + } catch(e) { 1.11513 + // fallback 1.11514 + node.appendChild(ranges[i].extractContents()); 1.11515 + ranges[i].insertNode(node); 1.11516 + } 1.11517 + } 1.11518 + return nodes; 1.11519 + }, 1.11520 + 1.11521 + /** 1.11522 + * Scroll the current caret position into the view 1.11523 + * FIXME: This is a bit hacky, there might be a smarter way of doing this 1.11524 + * 1.11525 + * @example 1.11526 + * selection.scrollIntoView(); 1.11527 + */ 1.11528 + scrollIntoView: function() { 1.11529 + var doc = this.doc, 1.11530 + tolerance = 5, // px 1.11531 + hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, 1.11532 + tempElement = doc._wysihtmlScrollIntoViewElement = doc._wysihtmlScrollIntoViewElement || (function() { 1.11533 + var element = doc.createElement("span"); 1.11534 + // The element needs content in order to be able to calculate it's position properly 1.11535 + element.innerHTML = wysihtml.INVISIBLE_SPACE; 1.11536 + return element; 1.11537 + })(), 1.11538 + offsetTop; 1.11539 + 1.11540 + if (hasScrollBars) { 1.11541 + this.insertNode(tempElement); 1.11542 + offsetTop = _getCumulativeOffsetTop(tempElement); 1.11543 + tempElement.parentNode.removeChild(tempElement); 1.11544 + if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) { 1.11545 + doc.body.scrollTop = offsetTop; 1.11546 + } 1.11547 + } 1.11548 + }, 1.11549 + 1.11550 + /** 1.11551 + * Select line where the caret is in 1.11552 + */ 1.11553 + selectLine: function() { 1.11554 + var r = rangy.createRange(); 1.11555 + if (wysihtml.browser.supportsSelectionModify()) { 1.11556 + this._selectLine_W3C(); 1.11557 + } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) { 1.11558 + // For IE Edge as it ditched the old api and did not fully implement the new one (as expected) 1.11559 + this._selectLineUniversal(); 1.11560 + } 1.11561 + }, 1.11562 + 1.11563 + includeRangyRangeHelpers: function() { 1.11564 + var s = this.getSelection(), 1.11565 + r = s.getRangeAt(0), 1.11566 + isHelperNode = function(node) { 1.11567 + return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary')); 1.11568 + }, 1.11569 + getNodeLength = function (node) { 1.11570 + if (node.nodeType === 1) { 1.11571 + return node.childNodes && node.childNodes.length || 0; 1.11572 + } else { 1.11573 + return node.data && node.data.length || 0; 1.11574 + } 1.11575 + }, 1.11576 + anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, 1.11577 + fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode; 1.11578 + 1.11579 + if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) { 1.11580 + r.setEndAfter(fnode.nextSibling); 1.11581 + } 1.11582 + if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) { 1.11583 + r.setStartBefore(anode.previousSibling); 1.11584 + } 1.11585 + r.select(); 1.11586 + }, 1.11587 + 1.11588 + /** 1.11589 + * See https://developer.mozilla.org/en/DOM/Selection/modify 1.11590 + */ 1.11591 + _selectLine_W3C: function() { 1.11592 + var selection = this.win.getSelection(), 1.11593 + initialBoundry = [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset]; 1.11594 + 1.11595 + selection.modify("move", "left", "lineboundary"); 1.11596 + selection.modify("extend", "right", "lineboundary"); 1.11597 + 1.11598 + // IF lineboundary extending did not change selection try universal fallback (FF fails sometimes without a reason) 1.11599 + if (selection.anchorNode === initialBoundry[0] && 1.11600 + selection.anchorOffset === initialBoundry[1] && 1.11601 + selection.focusNode === initialBoundry[2] && 1.11602 + selection.focusOffset === initialBoundry[3] 1.11603 + ) { 1.11604 + this._selectLineUniversal(); 1.11605 + } else { 1.11606 + this.includeRangyRangeHelpers(); 1.11607 + } 1.11608 + }, 1.11609 + 1.11610 + // collapses selection to current line beginning or end 1.11611 + toLineBoundary: function (location, collapse) { 1.11612 + collapse = (typeof collapse === 'undefined') ? false : collapse; 1.11613 + if (wysihtml.browser.supportsSelectionModify()) { 1.11614 + var selection = this.win.getSelection(); 1.11615 + 1.11616 + selection.modify("extend", location, "lineboundary"); 1.11617 + if (collapse) { 1.11618 + if (location === "left") { 1.11619 + selection.collapseToStart(); 1.11620 + } else if (location === "right") { 1.11621 + selection.collapseToEnd(); 1.11622 + } 1.11623 + } 1.11624 + } 1.11625 + }, 1.11626 + 1.11627 + getRangeRect: function(r) { 1.11628 + var textNode = this.doc.createTextNode("i"), 1.11629 + testNode = this.doc.createTextNode("i"), 1.11630 + rect, cr; 1.11631 + 1.11632 + /*testNode.style.visibility = "hidden"; 1.11633 + testNode.style.width = "0px"; 1.11634 + testNode.style.display = "inline-block"; 1.11635 + testNode.style.overflow = "hidden"; 1.11636 + testNode.appendChild(textNode);*/ 1.11637 + 1.11638 + if (r.collapsed) { 1.11639 + r.insertNode(testNode); 1.11640 + r.selectNode(testNode); 1.11641 + rect = r.nativeRange.getBoundingClientRect(); 1.11642 + r.deleteContents(); 1.11643 + 1.11644 + } else { 1.11645 + rect = r.nativeRange.getBoundingClientRect(); 1.11646 + } 1.11647 + 1.11648 + return rect; 1.11649 + 1.11650 + }, 1.11651 + 1.11652 + _selectLineUniversal: function() { 1.11653 + var s = this.getSelection(), 1.11654 + r = s.getRangeAt(0), 1.11655 + rect, 1.11656 + startRange, endRange, testRange, 1.11657 + count = 0, 1.11658 + amount, testRect, found, 1.11659 + that = this, 1.11660 + isLineBreakingElement = function(el) { 1.11661 + return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml.lang.array(['BR', 'HR']).contains(el.nodeName)); 1.11662 + }, 1.11663 + prevNode = function(node) { 1.11664 + var pnode = node; 1.11665 + if (pnode) { 1.11666 + while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) { 1.11667 + pnode = pnode.previousSibling; 1.11668 + } 1.11669 + } 1.11670 + return pnode; 1.11671 + }; 1.11672 + 1.11673 + startRange = r.cloneRange(); 1.11674 + endRange = r.cloneRange(); 1.11675 + 1.11676 + if (r.collapsed) { 1.11677 + // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary 1.11678 + // TODO: figure out a shorter and more readable way 1.11679 + if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) { 1.11680 + r.moveEnd('character', 1); 1.11681 + } 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) { 1.11682 + r.moveEnd('character', 1); 1.11683 + } else if ( 1.11684 + r.startOffset > 0 && 1.11685 + ( 1.11686 + r.startContainer.nodeType === 3 || 1.11687 + ( 1.11688 + r.startContainer.nodeType === 1 && 1.11689 + !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1])) 1.11690 + ) 1.11691 + ) 1.11692 + ) { 1.11693 + r.moveStart('character', -1); 1.11694 + } 1.11695 + } 1.11696 + if (!r.collapsed) { 1.11697 + r.insertNode(this.doc.createTextNode(wysihtml.INVISIBLE_SPACE)); 1.11698 + } 1.11699 + 1.11700 + // Is probably just empty line as can not be expanded 1.11701 + rect = r.nativeRange.getBoundingClientRect(); 1.11702 + // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes; 1.11703 + do { 1.11704 + amount = r.moveStart('character', -1); 1.11705 + testRect = r.nativeRange.getBoundingClientRect(); 1.11706 + 1.11707 + if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) { 1.11708 + r.moveStart('character', 1); 1.11709 + found = true; 1.11710 + } 1.11711 + count++; 1.11712 + } while (amount !== 0 && !found && count < 2000); 1.11713 + count = 0; 1.11714 + found = false; 1.11715 + rect = r.nativeRange.getBoundingClientRect(); 1.11716 + 1.11717 + if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) { 1.11718 + do { 1.11719 + amount = r.moveEnd('character', 1); 1.11720 + testRect = r.nativeRange.getBoundingClientRect(); 1.11721 + if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) { 1.11722 + r.moveEnd('character', -1); 1.11723 + 1.11724 + // Fix a IE line end marked by linebreak element although caret is before it 1.11725 + // If causes problems should be changed to be applied only to IE 1.11726 + 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) { 1.11727 + if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) { 1.11728 + r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length); 1.11729 + } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) { 1.11730 + r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length); 1.11731 + } 1.11732 + } 1.11733 + found = true; 1.11734 + } 1.11735 + count++; 1.11736 + } while (amount !== 0 && !found && count < 2000); 1.11737 + } 1.11738 + r.select(); 1.11739 + this.includeRangyRangeHelpers(); 1.11740 + }, 1.11741 + 1.11742 + getText: function() { 1.11743 + var selection = this.getSelection(); 1.11744 + return selection ? selection.toString() : ""; 1.11745 + }, 1.11746 + 1.11747 + getNodes: function(nodeType, filter) { 1.11748 + var range = this.getRange(); 1.11749 + if (range) { 1.11750 + return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter); 1.11751 + } else { 1.11752 + return []; 1.11753 + } 1.11754 + }, 1.11755 + 1.11756 + // Gets all the elements in selection with nodeType 1.11757 + // Ignores the elements not belonging to current editable area 1.11758 + // If filter is defined nodes must pass the filter function with true to be included in list 1.11759 + getOwnNodes: function(nodeType, filter, splitBounds) { 1.11760 + var ranges = this.getOwnRanges(), 1.11761 + nodes = []; 1.11762 + for (var r = 0, rmax = ranges.length; r < rmax; r++) { 1.11763 + if (ranges[r]) { 1.11764 + if (splitBounds) { 1.11765 + ranges[r].splitBoundaries(); 1.11766 + } 1.11767 + nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter)); 1.11768 + } 1.11769 + } 1.11770 + 1.11771 + return nodes; 1.11772 + }, 1.11773 + 1.11774 + fixRangeOverflow: function(range) { 1.11775 + if (this.contain && this.contain.firstChild && range) { 1.11776 + var containment = range.compareNode(this.contain); 1.11777 + if (containment !== 2) { 1.11778 + if (containment === 1) { 1.11779 + range.setStartBefore(this.contain.firstChild); 1.11780 + } 1.11781 + if (containment === 0) { 1.11782 + range.setEndAfter(this.contain.lastChild); 1.11783 + } 1.11784 + if (containment === 3) { 1.11785 + range.setStartBefore(this.contain.firstChild); 1.11786 + range.setEndAfter(this.contain.lastChild); 1.11787 + } 1.11788 + } else if (this._detectInlineRangeProblems(range)) { 1.11789 + var previousElementSibling = range.endContainer.previousElementSibling; 1.11790 + if (previousElementSibling) { 1.11791 + range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling)); 1.11792 + } 1.11793 + } 1.11794 + } 1.11795 + }, 1.11796 + 1.11797 + _endOffsetForNode: function(node) { 1.11798 + var range = document.createRange(); 1.11799 + range.selectNodeContents(node); 1.11800 + return range.endOffset; 1.11801 + }, 1.11802 + 1.11803 + _detectInlineRangeProblems: function(range) { 1.11804 + var position = dom.compareDocumentPosition(range.startContainer, range.endContainer); 1.11805 + return ( 1.11806 + range.endOffset == 0 && 1.11807 + position & 4 //Node.DOCUMENT_POSITION_FOLLOWING 1.11808 + ); 1.11809 + }, 1.11810 + 1.11811 + getRange: function(dontFix) { 1.11812 + var selection = this.getSelection(), 1.11813 + range = selection && selection.rangeCount && selection.getRangeAt(0); 1.11814 + 1.11815 + if (dontFix !== true) { 1.11816 + this.fixRangeOverflow(range); 1.11817 + } 1.11818 + 1.11819 + return range; 1.11820 + }, 1.11821 + 1.11822 + getOwnUneditables: function() { 1.11823 + var allUneditables = dom.query(this.contain, '.' + this.unselectableClass), 1.11824 + deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass); 1.11825 + 1.11826 + return wysihtml.lang.array(allUneditables).without(deepUneditables); 1.11827 + }, 1.11828 + 1.11829 + // Returns an array of ranges that belong only to this editable 1.11830 + // Needed as uneditable block in contenteditabel can split range into pieces 1.11831 + // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges 1.11832 + getOwnRanges: function() { 1.11833 + var ranges = [], 1.11834 + r = this.getRange(), 1.11835 + tmpRanges; 1.11836 + 1.11837 + if (r) { ranges.push(r); } 1.11838 + 1.11839 + if (this.unselectableClass && this.contain && r) { 1.11840 + var uneditables = this.getOwnUneditables(), 1.11841 + tmpRange; 1.11842 + if (uneditables.length > 0) { 1.11843 + for (var i = 0, imax = uneditables.length; i < imax; i++) { 1.11844 + tmpRanges = []; 1.11845 + for (var j = 0, jmax = ranges.length; j < jmax; j++) { 1.11846 + if (ranges[j]) { 1.11847 + switch (ranges[j].compareNode(uneditables[i])) { 1.11848 + case 2: 1.11849 + // all selection inside uneditable. remove 1.11850 + break; 1.11851 + case 3: 1.11852 + //section begins before and ends after uneditable. spilt 1.11853 + tmpRange = ranges[j].cloneRange(); 1.11854 + tmpRange.setEndBefore(uneditables[i]); 1.11855 + tmpRanges.push(tmpRange); 1.11856 + 1.11857 + tmpRange = ranges[j].cloneRange(); 1.11858 + tmpRange.setStartAfter(uneditables[i]); 1.11859 + tmpRanges.push(tmpRange); 1.11860 + break; 1.11861 + default: 1.11862 + // in all other cases uneditable does not touch selection. dont modify 1.11863 + tmpRanges.push(ranges[j]); 1.11864 + } 1.11865 + } 1.11866 + ranges = tmpRanges; 1.11867 + } 1.11868 + } 1.11869 + } 1.11870 + } 1.11871 + return ranges; 1.11872 + }, 1.11873 + 1.11874 + getSelection: function() { 1.11875 + return rangy.getSelection(this.win); 1.11876 + }, 1.11877 + 1.11878 + // Sets selection in document to a given range 1.11879 + // Set selection method detects if it fails to set any selection in document and returns null on fail 1.11880 + // (especially needed in webkit where some ranges just can not create selection for no reason) 1.11881 + setSelection: function(range) { 1.11882 + var selection = rangy.getSelection(this.win); 1.11883 + selection.setSingleRange(range); 1.11884 + return (selection && selection.anchorNode && selection.focusNode) ? selection : null; 1.11885 + }, 1.11886 + 1.11887 + 1.11888 + 1.11889 + // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area 1.11890 + selectAll: function() { 1.11891 + var range = this.createRange(), 1.11892 + composer = this.composer, 1.11893 + that = this, 1.11894 + blankEndNode = getWebkitSelectionFixNode(this.composer.element), 1.11895 + blankStartNode = getWebkitSelectionFixNode(this.composer.element), 1.11896 + s; 1.11897 + 1.11898 + var doSelect = function() { 1.11899 + range.setStart(composer.element, 0); 1.11900 + range.setEnd(composer.element, composer.element.childNodes.length); 1.11901 + s = that.setSelection(range); 1.11902 + }; 1.11903 + 1.11904 + var notSelected = function() { 1.11905 + return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None")); 1.11906 + } 1.11907 + 1.11908 + wysihtml.dom.removeInvisibleSpaces(this.composer.element); 1.11909 + doSelect(); 1.11910 + 1.11911 + if (this.composer.element.firstChild && notSelected()) { 1.11912 + // Try fixing end 1.11913 + this.composer.element.appendChild(blankEndNode); 1.11914 + doSelect(); 1.11915 + 1.11916 + if (notSelected()) { 1.11917 + // Remove end fix 1.11918 + blankEndNode.parentNode.removeChild(blankEndNode); 1.11919 + 1.11920 + // Try fixing beginning 1.11921 + this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild); 1.11922 + doSelect(); 1.11923 + 1.11924 + if (notSelected()) { 1.11925 + // Try fixing both 1.11926 + this.composer.element.appendChild(blankEndNode); 1.11927 + doSelect(); 1.11928 + } 1.11929 + } 1.11930 + } 1.11931 + }, 1.11932 + 1.11933 + createRange: function() { 1.11934 + return rangy.createRange(this.doc); 1.11935 + }, 1.11936 + 1.11937 + isCollapsed: function() { 1.11938 + return this.getSelection().isCollapsed; 1.11939 + }, 1.11940 + 1.11941 + getHtml: function() { 1.11942 + return this.getSelection().toHtml(); 1.11943 + }, 1.11944 + 1.11945 + getPlainText: function () { 1.11946 + return this.getSelection().toString(); 1.11947 + }, 1.11948 + 1.11949 + isEndToEndInNode: function(nodeNames) { 1.11950 + var range = this.getRange(), 1.11951 + parentElement = range.commonAncestorContainer, 1.11952 + startNode = range.startContainer, 1.11953 + endNode = range.endContainer; 1.11954 + 1.11955 + 1.11956 + if (parentElement.nodeType === wysihtml.TEXT_NODE) { 1.11957 + parentElement = parentElement.parentNode; 1.11958 + } 1.11959 + 1.11960 + if (startNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) { 1.11961 + return false; 1.11962 + } 1.11963 + 1.11964 + if (endNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) { 1.11965 + return false; 1.11966 + } 1.11967 + 1.11968 + while (startNode && startNode !== parentElement) { 1.11969 + if (startNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, startNode)) { 1.11970 + return false; 1.11971 + } 1.11972 + if (wysihtml.dom.domNode(startNode).prev({ignoreBlankTexts: true})) { 1.11973 + return false; 1.11974 + } 1.11975 + startNode = startNode.parentNode; 1.11976 + } 1.11977 + 1.11978 + while (endNode && endNode !== parentElement) { 1.11979 + if (endNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, endNode)) { 1.11980 + return false; 1.11981 + } 1.11982 + if (wysihtml.dom.domNode(endNode).next({ignoreBlankTexts: true})) { 1.11983 + return false; 1.11984 + } 1.11985 + endNode = endNode.parentNode; 1.11986 + } 1.11987 + 1.11988 + return (wysihtml.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false; 1.11989 + }, 1.11990 + 1.11991 + isInThisEditable: function() { 1.11992 + var sel = this.getSelection(), 1.11993 + fnode = sel.focusNode, 1.11994 + anode = sel.anchorNode; 1.11995 + 1.11996 + // In IE node contains will not work for textnodes, thus taking parentNode 1.11997 + if (fnode && fnode.nodeType !== 1) { 1.11998 + fnode = fnode.parentNode; 1.11999 + } 1.12000 + 1.12001 + if (anode && anode.nodeType !== 1) { 1.12002 + anode = anode.parentNode; 1.12003 + } 1.12004 + 1.12005 + return anode && fnode && 1.12006 + (wysihtml.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) && 1.12007 + (wysihtml.dom.contains(this.composer.element, anode) || this.composer.element === anode); 1.12008 + }, 1.12009 + 1.12010 + deselect: function() { 1.12011 + var sel = this.getSelection(); 1.12012 + sel && sel.removeAllRanges(); 1.12013 + } 1.12014 + }); 1.12015 + 1.12016 +})(wysihtml); 1.12017 + 1.12018 +/** 1.12019 + * Rich Text Query/Formatting Commands 1.12020 + * 1.12021 + * @example 1.12022 + * var commands = new wysihtml.Commands(editor); 1.12023 + */ 1.12024 +wysihtml.Commands = Base.extend( 1.12025 + /** @scope wysihtml.Commands.prototype */ { 1.12026 + constructor: function(editor) { 1.12027 + this.editor = editor; 1.12028 + this.composer = editor.composer; 1.12029 + this.doc = this.composer.doc; 1.12030 + }, 1.12031 + 1.12032 + /** 1.12033 + * Check whether the browser supports the given command 1.12034 + * 1.12035 + * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") 1.12036 + * @example 1.12037 + * commands.supports("createLink"); 1.12038 + */ 1.12039 + support: function(command) { 1.12040 + return wysihtml.browser.supportsCommand(this.doc, command); 1.12041 + }, 1.12042 + 1.12043 + /** 1.12044 + * Check whether the browser supports the given command 1.12045 + * 1.12046 + * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") 1.12047 + * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) 1.12048 + * @example 1.12049 + * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg"); 1.12050 + */ 1.12051 + exec: function(command, value) { 1.12052 + var obj = wysihtml.commands[command], 1.12053 + args = wysihtml.lang.array(arguments).get(), 1.12054 + method = obj && obj.exec, 1.12055 + result = null; 1.12056 + 1.12057 + // If composer ahs placeholder unset it before command 1.12058 + // Do not apply on commands that are behavioral 1.12059 + if (this.composer.hasPlaceholderSet() && !wysihtml.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) { 1.12060 + this.composer.element.innerHTML = ""; 1.12061 + this.composer.selection.selectNode(this.composer.element); 1.12062 + } 1.12063 + 1.12064 + this.editor.fire("beforecommand:composer"); 1.12065 + 1.12066 + if (method) { 1.12067 + args.unshift(this.composer); 1.12068 + result = method.apply(obj, args); 1.12069 + } else { 1.12070 + try { 1.12071 + // try/catch for buggy firefox 1.12072 + result = this.doc.execCommand(command, false, value); 1.12073 + } catch(e) {} 1.12074 + } 1.12075 + 1.12076 + this.editor.fire("aftercommand:composer"); 1.12077 + return result; 1.12078 + }, 1.12079 + 1.12080 + remove: function(command, commandValue) { 1.12081 + var obj = wysihtml.commands[command], 1.12082 + args = wysihtml.lang.array(arguments).get(), 1.12083 + method = obj && obj.remove; 1.12084 + if (method) { 1.12085 + args.unshift(this.composer); 1.12086 + return method.apply(obj, args); 1.12087 + } 1.12088 + }, 1.12089 + 1.12090 + /** 1.12091 + * Check whether the current command is active 1.12092 + * If the caret is within a bold text, then calling this with command "bold" should return true 1.12093 + * 1.12094 + * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") 1.12095 + * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) 1.12096 + * @return {Boolean} Whether the command is active 1.12097 + * @example 1.12098 + * var isCurrentSelectionBold = commands.state("bold"); 1.12099 + */ 1.12100 + state: function(command, commandValue) { 1.12101 + var obj = wysihtml.commands[command], 1.12102 + args = wysihtml.lang.array(arguments).get(), 1.12103 + method = obj && obj.state; 1.12104 + if (method) { 1.12105 + args.unshift(this.composer); 1.12106 + return method.apply(obj, args); 1.12107 + } else { 1.12108 + try { 1.12109 + // try/catch for buggy firefox 1.12110 + return this.doc.queryCommandState(command); 1.12111 + } catch(e) { 1.12112 + return false; 1.12113 + } 1.12114 + } 1.12115 + }, 1.12116 + 1.12117 + /* Get command state parsed value if command has stateValue parsing function */ 1.12118 + stateValue: function(command) { 1.12119 + var obj = wysihtml.commands[command], 1.12120 + args = wysihtml.lang.array(arguments).get(), 1.12121 + method = obj && obj.stateValue; 1.12122 + if (method) { 1.12123 + args.unshift(this.composer); 1.12124 + return method.apply(obj, args); 1.12125 + } else { 1.12126 + return false; 1.12127 + } 1.12128 + } 1.12129 +}); 1.12130 + 1.12131 +(function(wysihtml) { 1.12132 + 1.12133 + var nodeOptions = { 1.12134 + nodeName: "A", 1.12135 + toggle: false 1.12136 + }; 1.12137 + 1.12138 + function getOptions(value) { 1.12139 + var options = typeof value === 'object' ? value : {'href': value}; 1.12140 + return wysihtml.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get(); 1.12141 + } 1.12142 + 1.12143 + wysihtml.commands.createLink = { 1.12144 + exec: function(composer, command, value) { 1.12145 + var opts = getOptions(value); 1.12146 + 1.12147 + if (composer.selection.isCollapsed() && !this.state(composer, command)) { 1.12148 + var textNode = composer.doc.createTextNode(opts.attribute.href); 1.12149 + composer.selection.insertNode(textNode); 1.12150 + composer.selection.selectNode(textNode); 1.12151 + } 1.12152 + wysihtml.commands.formatInline.exec(composer, command, opts); 1.12153 + }, 1.12154 + 1.12155 + state: function(composer, command) { 1.12156 + return wysihtml.commands.formatInline.state(composer, command, nodeOptions); 1.12157 + } 1.12158 + }; 1.12159 + 1.12160 +})(wysihtml); 1.12161 + 1.12162 +/* Formatblock 1.12163 + * Is used to insert block level elements 1.12164 + * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...) 1.12165 + * 1.12166 +*/ 1.12167 +(function(wysihtml) { 1.12168 + 1.12169 + var dom = wysihtml.dom, 1.12170 + // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 1.12171 + // instead of creating a H4 within a H1 which would result in semantically invalid html 1.12172 + UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre", 1.12173 + BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote", 1.12174 + 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"; 1.12175 + 1.12176 + function correctOptionsForSimilarityCheck(options) { 1.12177 + return { 1.12178 + nodeName: options.nodeName || null, 1.12179 + className: (!options.classRegExp) ? options.className || null : null, 1.12180 + classRegExp: options.classRegExp || null, 1.12181 + styleProperty: options.styleProperty || null 1.12182 + }; 1.12183 + } 1.12184 + 1.12185 + function getRangeNode(node, offset) { 1.12186 + if (node.nodeType === 3) { 1.12187 + return node; 1.12188 + } else { 1.12189 + return node.childNodes[offset] || node; 1.12190 + } 1.12191 + } 1.12192 + 1.12193 + // Returns if node is a line break 1.12194 + function isBr(n) { 1.12195 + return n && n.nodeType === 1 && n.nodeName === "BR"; 1.12196 + } 1.12197 + 1.12198 + // Is block level element 1.12199 + function isBlock(n, composer) { 1.12200 + return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block"; 1.12201 + } 1.12202 + 1.12203 + // 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) 1.12204 + function isBookmark(n) { 1.12205 + return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary'); 1.12206 + } 1.12207 + 1.12208 + // Is line breaking node 1.12209 + function isLineBreaking(n, composer) { 1.12210 + return isBr(n) || isBlock(n, composer); 1.12211 + } 1.12212 + 1.12213 + // Removes empty block level elements 1.12214 + function cleanup(composer, newBlockElements) { 1.12215 + wysihtml.dom.removeInvisibleSpaces(composer.element); 1.12216 + var container = composer.element, 1.12217 + allElements = container.querySelectorAll(BLOCK_ELEMENTS), 1.12218 + noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '), 1.12219 + uneditables = container.querySelectorAll(noEditQuery), 1.12220 + elements = wysihtml.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents 1.12221 + nbIdx; 1.12222 + 1.12223 + for (var i = elements.length; i--;) { 1.12224 + if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) { 1.12225 + // If cleanup removes some new block elements. remove them from newblocks array too 1.12226 + nbIdx = wysihtml.lang.array(newBlockElements).indexOf(elements[i]); 1.12227 + if (nbIdx > -1) { 1.12228 + newBlockElements.splice(nbIdx, 1); 1.12229 + } 1.12230 + elements[i].parentNode.removeChild(elements[i]); 1.12231 + } 1.12232 + } 1.12233 + 1.12234 + return newBlockElements; 1.12235 + } 1.12236 + 1.12237 + function defaultNodeName(composer) { 1.12238 + return composer.config.useLineBreaks ? "DIV" : "P"; 1.12239 + } 1.12240 + 1.12241 + // The outermost un-nestable block element parent of from node 1.12242 + function findOuterBlock(node, container, allBlocks) { 1.12243 + var n = node, 1.12244 + block = null; 1.12245 + 1.12246 + while (n && container && n !== container) { 1.12247 + if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) { 1.12248 + block = n; 1.12249 + } 1.12250 + n = n.parentNode; 1.12251 + } 1.12252 + 1.12253 + return block; 1.12254 + } 1.12255 + 1.12256 + // Clone for splitting the inner inline element out of its parent inline elements context 1.12257 + // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return 1.12258 + function cloneOuterInlines(node, container) { 1.12259 + var n = node, 1.12260 + innerNode, 1.12261 + parentNode, 1.12262 + el = null, 1.12263 + el2; 1.12264 + 1.12265 + while (n && container && n !== container) { 1.12266 + if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) { 1.12267 + parentNode = n; 1.12268 + if (el === null) { 1.12269 + el = n.cloneNode(false); 1.12270 + innerNode = el; 1.12271 + } else { 1.12272 + el2 = n.cloneNode(false); 1.12273 + el2.appendChild(el); 1.12274 + el = el2; 1.12275 + } 1.12276 + } 1.12277 + n = n.parentNode; 1.12278 + } 1.12279 + 1.12280 + return { 1.12281 + parent: parentNode, 1.12282 + outerNode: el, 1.12283 + innerNode: innerNode 1.12284 + }; 1.12285 + } 1.12286 + 1.12287 + // Formats an element according to options nodeName, className, styleProperty, styleValue 1.12288 + // If element is not defined, creates new element 1.12289 + // if opotions is null, remove format instead 1.12290 + function applyOptionsToElement(element, options, composer) { 1.12291 + 1.12292 + if (!element) { 1.12293 + element = composer.doc.createElement(options.nodeName || defaultNodeName(composer)); 1.12294 + // Add invisible space as otherwise webkit cannot set selection or range to it correctly 1.12295 + element.appendChild(composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE)); 1.12296 + } 1.12297 + 1.12298 + if (options.nodeName && element.nodeName !== options.nodeName) { 1.12299 + element = dom.renameElement(element, options.nodeName); 1.12300 + } 1.12301 + 1.12302 + // Remove similar classes before applying className 1.12303 + if (options.classRegExp) { 1.12304 + element.className = element.className.replace(options.classRegExp, ""); 1.12305 + } 1.12306 + if (options.className) { 1.12307 + element.classList.add(options.className); 1.12308 + } 1.12309 + 1.12310 + if (options.styleProperty && typeof options.styleValue !== "undefined") { 1.12311 + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue; 1.12312 + } 1.12313 + 1.12314 + return element; 1.12315 + } 1.12316 + 1.12317 + // Unsets element properties by options 1.12318 + // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes) 1.12319 + function removeOptionsFromElement(element, options, composer) { 1.12320 + var style, classes, 1.12321 + prevNode = element.previousSibling, 1.12322 + nextNode = element.nextSibling, 1.12323 + unwrapped = false; 1.12324 + 1.12325 + if (options.styleProperty) { 1.12326 + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = ''; 1.12327 + } 1.12328 + if (options.className) { 1.12329 + element.classList.remove(options.className); 1.12330 + } 1.12331 + 1.12332 + if (options.classRegExp) { 1.12333 + element.className = element.className.replace(options.classRegExp, ""); 1.12334 + } 1.12335 + 1.12336 + // Clean up blank class attribute 1.12337 + if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") { 1.12338 + element.removeAttribute('class'); 1.12339 + } 1.12340 + 1.12341 + if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) { 1.12342 + style = element.getAttribute('style'); 1.12343 + if (!style || style.trim() === '') { 1.12344 + dom.unwrap(element); 1.12345 + unwrapped = true; 1.12346 + } else { 1.12347 + element = dom.renameElement(element, defaultNodeName(composer)); 1.12348 + } 1.12349 + } 1.12350 + 1.12351 + // Clean up blank style attribute 1.12352 + if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") { 1.12353 + element.removeAttribute('style'); 1.12354 + } 1.12355 + 1.12356 + if (unwrapped) { 1.12357 + applySurroundingLineBreaks(prevNode, nextNode, composer); 1.12358 + } 1.12359 + } 1.12360 + 1.12361 + // Unwraps block level elements from inside content 1.12362 + // Useful as not all block level elements can contain other block-levels 1.12363 + function unwrapBlocksFromContent(element) { 1.12364 + var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents 1.12365 + nextEl, prevEl; 1.12366 + 1.12367 + for (var i = blocks.length; i--;) { 1.12368 + nextEl = wysihtml.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}), 1.12369 + prevEl = wysihtml.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.12370 + 1.12371 + if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') { 1.12372 + if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') { 1.12373 + blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl); 1.12374 + } 1.12375 + } 1.12376 + if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') { 1.12377 + if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') { 1.12378 + blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl); 1.12379 + } 1.12380 + } 1.12381 + wysihtml.dom.unwrap(blocks[i]); 1.12382 + } 1.12383 + } 1.12384 + 1.12385 + // Fix ranges that visually cover whole block element to actually cover the block 1.12386 + function fixRangeCoverage(range, composer) { 1.12387 + var node, 1.12388 + start = range.startContainer, 1.12389 + end = range.endContainer; 1.12390 + 1.12391 + // If range has only one childNode and it is end to end the range, extend the range to contain the container element too 1.12392 + // This ensures the wrapper node is modified and optios added to it 1.12393 + if (start && start.nodeType === 1 && start === end) { 1.12394 + if (start.firstChild === start.lastChild && range.endOffset === 1) { 1.12395 + if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') { 1.12396 + range.setStartBefore(start); 1.12397 + range.setEndAfter(end); 1.12398 + } 1.12399 + } 1.12400 + return; 1.12401 + } 1.12402 + 1.12403 + // 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 1.12404 + if (start && start.nodeType === 1 && end.nodeType === 3) { 1.12405 + if (start.firstChild === end && range.endOffset === end.data.length) { 1.12406 + if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') { 1.12407 + range.setEndAfter(start); 1.12408 + } 1.12409 + } 1.12410 + return; 1.12411 + } 1.12412 + 1.12413 + // 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 1.12414 + if (end && end.nodeType === 1 && start.nodeType === 3) { 1.12415 + if (end.firstChild === start && range.startOffset === 0) { 1.12416 + if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') { 1.12417 + range.setStartBefore(end); 1.12418 + } 1.12419 + } 1.12420 + return; 1.12421 + } 1.12422 + 1.12423 + // If range covers a whole textnode and the textnode is the only child of node, extend range to node 1.12424 + if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) { 1.12425 + if (range.endOffset == end.data.length && range.startOffset === 0) { 1.12426 + node = start.parentNode; 1.12427 + if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') { 1.12428 + range.setStartBefore(node); 1.12429 + range.setEndAfter(node); 1.12430 + } 1.12431 + } 1.12432 + return; 1.12433 + } 1.12434 + } 1.12435 + 1.12436 + // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges 1.12437 + // Some places do not allow block level elements inbetween (inside ul and outside li) 1.12438 + // TODO: might need extending for other nodes besides li (maybe dd,dl,dt) 1.12439 + function fixNotPermittedInsertionPoints(ranges) { 1.12440 + var newRanges = [], 1.12441 + lis, j, maxj, tmpRange, rangePos, closestLI; 1.12442 + 1.12443 + for (var i = 0, maxi = ranges.length; i < maxi; i++) { 1.12444 + 1.12445 + // Fixes range start and end positions if inside UL or OL element (outside of LI) 1.12446 + if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) { 1.12447 + ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0); 1.12448 + } 1.12449 + if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) { 1.12450 + closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)]; 1.12451 + if (closestLI.childNodes) { 1.12452 + ranges[i].setEnd(closestLI, closestLI.childNodes.length); 1.12453 + } 1.12454 + } 1.12455 + 1.12456 + // Get all LI eleemnts in selection (fully or partially covered) 1.12457 + // And make sure ranges are either inside LI or outside UL/OL 1.12458 + // Split and add new ranges as needed to cover same range content 1.12459 + // TODO: Needs improvement to accept DL, DD, DT 1.12460 + lis = ranges[i].getNodes([1], function(node) { 1.12461 + return node.nodeName === "LI"; 1.12462 + }); 1.12463 + if (lis.length > 0) { 1.12464 + 1.12465 + for (j = 0, maxj = lis.length; j < maxj; j++) { 1.12466 + rangePos = ranges[i].compareNode(lis[j]); 1.12467 + 1.12468 + // Fixes start of range that crosses LI border 1.12469 + if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) { 1.12470 + // Range starts before and ends inside the node 1.12471 + 1.12472 + tmpRange = ranges[i].cloneRange(); 1.12473 + closestLI = wysihtml.dom.domNode(lis[j]).prev({nodeTypes: [1]}); 1.12474 + 1.12475 + if (closestLI) { 1.12476 + tmpRange.setEnd(closestLI, closestLI.childNodes.length); 1.12477 + } else if (lis[j].closest('ul, ol')) { 1.12478 + tmpRange.setEndBefore(lis[j].closest('ul, ol')); 1.12479 + } else { 1.12480 + tmpRange.setEndBefore(lis[j]); 1.12481 + } 1.12482 + newRanges.push(tmpRange); 1.12483 + ranges[i].setStart(lis[j], 0); 1.12484 + } 1.12485 + 1.12486 + // Fixes end of range that crosses li border 1.12487 + if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) { 1.12488 + // Range starts inside the node and ends after node 1.12489 + 1.12490 + tmpRange = ranges[i].cloneRange(); 1.12491 + tmpRange.setEnd(lis[j], lis[j].childNodes.length); 1.12492 + newRanges.push(tmpRange); 1.12493 + 1.12494 + // Find next LI in list and if present set range to it, else 1.12495 + closestLI = wysihtml.dom.domNode(lis[j]).next({nodeTypes: [1]}); 1.12496 + if (closestLI) { 1.12497 + ranges[i].setStart(closestLI, 0); 1.12498 + } else if (lis[j].closest('ul, ol')) { 1.12499 + ranges[i].setStartAfter(lis[j].closest('ul, ol')); 1.12500 + } else { 1.12501 + ranges[i].setStartAfter(lis[j]); 1.12502 + } 1.12503 + } 1.12504 + } 1.12505 + newRanges.push(ranges[i]); 1.12506 + } else { 1.12507 + newRanges.push(ranges[i]); 1.12508 + } 1.12509 + } 1.12510 + return newRanges; 1.12511 + } 1.12512 + 1.12513 + // Return options object with nodeName set if original did not have any 1.12514 + // Node name is set to local or global default 1.12515 + function getOptionsWithNodename(options, defaultName, composer) { 1.12516 + var correctedOptions = (options) ? wysihtml.lang.object(options).clone(true) : null; 1.12517 + if (correctedOptions) { 1.12518 + correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer); 1.12519 + } 1.12520 + return correctedOptions; 1.12521 + } 1.12522 + 1.12523 + // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted 1.12524 + // Also wraps empty clones of split parent tags around fragment to keep formatting 1.12525 + // 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) 1.12526 + function injectFragmentToRange(fragment, range, composer, firstOuterBlock) { 1.12527 + var rangeStartContainer = range.startContainer, 1.12528 + firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true), 1.12529 + outerInlines, first, last, prev, next; 1.12530 + 1.12531 + if (firstOuterBlock) { 1.12532 + // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between 1.12533 + first = fragment.firstChild; 1.12534 + last = fragment.lastChild; 1.12535 + 1.12536 + composer.selection.splitElementAtCaret(firstOuterBlock, fragment); 1.12537 + 1.12538 + next = wysihtml.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.12539 + prev = wysihtml.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.12540 + 1.12541 + if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) { 1.12542 + first.parentNode.insertBefore(composer.doc.createElement('br'), first); 1.12543 + } 1.12544 + 1.12545 + if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) { 1.12546 + next.parentNode.insertBefore(composer.doc.createElement('br'), next); 1.12547 + } 1.12548 + 1.12549 + } else { 1.12550 + // Ensure node does not get inserted into an inline where it is not allowed 1.12551 + outerInlines = cloneOuterInlines(rangeStartContainer, composer.element); 1.12552 + if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) { 1.12553 + if (fragment.childNodes.length === 1) { 1.12554 + while(fragment.firstChild.firstChild) { 1.12555 + outerInlines.innerNode.appendChild(fragment.firstChild.firstChild); 1.12556 + } 1.12557 + fragment.firstChild.appendChild(outerInlines.outerNode); 1.12558 + } 1.12559 + composer.selection.splitElementAtCaret(outerInlines.parent, fragment); 1.12560 + } else { 1.12561 + var fc = fragment.firstChild, 1.12562 + lc = fragment.lastChild; 1.12563 + 1.12564 + range.insertNode(fragment); 1.12565 + // restore range position as it might get lost in webkit sometimes 1.12566 + range.setStartBefore(fc); 1.12567 + range.setEndAfter(lc); 1.12568 + } 1.12569 + } 1.12570 + } 1.12571 + 1.12572 + // Removes all block formatting from range 1.12573 + function clearRangeBlockFromating(range, closestBlockName, composer) { 1.12574 + var r = range.cloneRange(), 1.12575 + prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling, 1.12576 + nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling, 1.12577 + content = r.extractContents(), 1.12578 + fragment = composer.doc.createDocumentFragment(), 1.12579 + children, blocks, 1.12580 + first = true; 1.12581 + 1.12582 + while(content.firstChild) { 1.12583 + // Iterate over all selection content first level childNodes 1.12584 + if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) { 1.12585 + // If node is a block element 1.12586 + // Split block formating and add new block to wrap caret 1.12587 + 1.12588 + unwrapBlocksFromContent(content.firstChild); 1.12589 + children = wysihtml.dom.unwrap(content.firstChild); 1.12590 + 1.12591 + // Add line break before if needed 1.12592 + if (children.length > 0) { 1.12593 + if ( 1.12594 + (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) || 1.12595 + (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer))) 1.12596 + ){ 1.12597 + fragment.appendChild(composer.doc.createElement('BR')); 1.12598 + } 1.12599 + } 1.12600 + 1.12601 + for (var c = 0, cmax = children.length; c < cmax; c++) { 1.12602 + fragment.appendChild(children[c]); 1.12603 + } 1.12604 + 1.12605 + // Add line break after if needed 1.12606 + if (children.length > 0) { 1.12607 + if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) { 1.12608 + if (nextNode || fragment.lastChild !== content.lastChild) { 1.12609 + fragment.appendChild(composer.doc.createElement('BR')); 1.12610 + } 1.12611 + } 1.12612 + } 1.12613 + 1.12614 + } else { 1.12615 + fragment.appendChild(content.firstChild); 1.12616 + } 1.12617 + 1.12618 + first = false; 1.12619 + } 1.12620 + blocks = wysihtml.lang.array(fragment.childNodes).get(); 1.12621 + injectFragmentToRange(fragment, r, composer); 1.12622 + return blocks; 1.12623 + } 1.12624 + 1.12625 + // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself) 1.12626 + function removeSurroundingLineBreaks(prevNode, nextNode, composer) { 1.12627 + var prevPrev = prevNode && wysihtml.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.12628 + if (isBr(nextNode)) { 1.12629 + nextNode.parentNode.removeChild(nextNode); 1.12630 + } 1.12631 + if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) { 1.12632 + prevNode.parentNode.removeChild(prevNode); 1.12633 + } 1.12634 + } 1.12635 + 1.12636 + function applySurroundingLineBreaks(prevNode, nextNode, composer) { 1.12637 + var prevPrev; 1.12638 + 1.12639 + if (prevNode && isBookmark(prevNode)) { 1.12640 + prevNode = prevNode.previousSibling; 1.12641 + } 1.12642 + if (nextNode && isBookmark(nextNode)) { 1.12643 + nextNode = nextNode.nextSibling; 1.12644 + } 1.12645 + 1.12646 + prevPrev = prevNode && prevNode.previousSibling; 1.12647 + 1.12648 + if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) { 1.12649 + prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling); 1.12650 + } 1.12651 + 1.12652 + if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) { 1.12653 + nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode); 1.12654 + } 1.12655 + } 1.12656 + 1.12657 + var isWhitespaceBefore = function (textNode, offset) { 1.12658 + var str = textNode.data ? textNode.data.slice(0, offset) : ""; 1.12659 + return (/^\s*$/).test(str); 1.12660 + } 1.12661 + 1.12662 + var isWhitespaceAfter = function (textNode, offset) { 1.12663 + var str = textNode.data ? textNode.data.slice(offset) : ""; 1.12664 + return (/^\s*$/).test(str); 1.12665 + } 1.12666 + 1.12667 + var trimBlankTextsAndBreaks = function(fragment) { 1.12668 + if (fragment) { 1.12669 + while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) { 1.12670 + fragment.removeChild(fragment.firstChild); 1.12671 + } 1.12672 + 1.12673 + while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) { 1.12674 + fragment.removeChild(fragment.lastChild); 1.12675 + } 1.12676 + 1.12677 + if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) { 1.12678 + fragment.removeChild(fragment.firstChild); 1.12679 + } 1.12680 + 1.12681 + if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) { 1.12682 + fragment.removeChild(fragment.lastChild); 1.12683 + } 1.12684 + } 1.12685 + } 1.12686 + 1.12687 + // Wrap the range with a block level element 1.12688 + // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur 1.12689 + function wrapRangeWithElement(range, options, closestBlockName, composer) { 1.12690 + var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null, 1.12691 + r = range.cloneRange(), 1.12692 + rangeStartContainer = r.startContainer, 1.12693 + startNode = getRangeNode(r.startContainer, r.startOffset), 1.12694 + endNode = getRangeNode(r.endContainer, r.endOffset), 1.12695 + prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}), 1.12696 + nextNode = ( 1.12697 + ( 1.12698 + r.endContainer.nodeType === 1 && 1.12699 + r.endContainer.childNodes[r.endOffset] === endNode && 1.12700 + ( 1.12701 + endNode.nodeType === 1 || 1.12702 + !isWhitespaceAfter(endNode, r.endOffset) && 1.12703 + !wysihtml.dom.domNode(endNode).is.rangyBookmark() 1.12704 + ) 1.12705 + ) || ( 1.12706 + r.endContainer === endNode && 1.12707 + endNode.nodeType === 3 && 1.12708 + !isWhitespaceAfter(endNode, r.endOffset) 1.12709 + ) 1.12710 + ) ? endNode : wysihtml.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}), 1.12711 + content = r.extractContents(), 1.12712 + fragment = composer.doc.createDocumentFragment(), 1.12713 + similarOuterBlock = similarOptions ? wysihtml.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null, 1.12714 + splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"), 1.12715 + firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start 1.12716 + wrapper, blocks, children, 1.12717 + firstc, lastC; 1.12718 + 1.12719 + if (wysihtml.dom.domNode(nextNode).is.rangyBookmark()) { 1.12720 + endNode = nextNode; 1.12721 + nextNode = endNode.nextSibling; 1.12722 + } 1.12723 + 1.12724 + trimBlankTextsAndBreaks(content); 1.12725 + 1.12726 + if (options && options.nodeName === "BLOCKQUOTE") { 1.12727 + 1.12728 + // If blockquote is to be inserted no quessing just add it as outermost block on line or selection 1.12729 + var tmpEl = applyOptionsToElement(null, options, composer); 1.12730 + tmpEl.appendChild(content); 1.12731 + fragment.appendChild(tmpEl); 1.12732 + blocks = [tmpEl]; 1.12733 + 1.12734 + } else { 1.12735 + 1.12736 + if (!content.firstChild) { 1.12737 + // IF selection is caret (can happen if line is empty) add format around tag 1.12738 + fragment.appendChild(applyOptionsToElement(null, options, composer)); 1.12739 + } else { 1.12740 + 1.12741 + while(content.firstChild) { 1.12742 + // Iterate over all selection content first level childNodes 1.12743 + 1.12744 + if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) { 1.12745 + 1.12746 + // If node is a block element 1.12747 + // Escape(split) block formatting at caret 1.12748 + applyOptionsToElement(content.firstChild, options, composer); 1.12749 + if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) { 1.12750 + unwrapBlocksFromContent(content.firstChild); 1.12751 + } 1.12752 + fragment.appendChild(content.firstChild); 1.12753 + 1.12754 + } else { 1.12755 + 1.12756 + // Wrap subsequent non-block nodes inside new block element 1.12757 + wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer); 1.12758 + while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) { 1.12759 + if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) { 1.12760 + unwrapBlocksFromContent(content.firstChild); 1.12761 + } 1.12762 + wrapper.appendChild(content.firstChild); 1.12763 + } 1.12764 + fragment.appendChild(wrapper); 1.12765 + } 1.12766 + } 1.12767 + } 1.12768 + 1.12769 + blocks = wysihtml.lang.array(fragment.childNodes).get(); 1.12770 + } 1.12771 + injectFragmentToRange(fragment, r, composer, firstOuterBlock); 1.12772 + removeSurroundingLineBreaks(prevNode, nextNode, composer); 1.12773 + 1.12774 + // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block 1.12775 + // (if it contains rangy bookmark, so selection can be restored later correctly) 1.12776 + if (blocks.length > 0 && 1.12777 + ( 1.12778 + typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark() 1.12779 + ) 1.12780 + ) { 1.12781 + blocks[blocks.length - 1].appendChild(composer.doc.createElement('br')); 1.12782 + } 1.12783 + return blocks; 1.12784 + } 1.12785 + 1.12786 + // Find closest block level element 1.12787 + function getParentBlockNodeName(element, composer) { 1.12788 + var parentNode = wysihtml.dom.getParentElement(element, { 1.12789 + query: BLOCK_ELEMENTS 1.12790 + }, null, composer.element); 1.12791 + 1.12792 + return (parentNode) ? parentNode.nodeName : null; 1.12793 + } 1.12794 + 1.12795 + // Expands caret to cover the closest block that: 1.12796 + // * cannot contain other block level elements (h1-6,p, etc) 1.12797 + // * Has the same nodeName that is to be inserted 1.12798 + // * has insertingNodeName 1.12799 + // * is DIV if insertingNodeName is not present 1.12800 + // 1.12801 + // If nothing found selects the current line 1.12802 + function expandCaretToBlock(composer, insertingNodeName) { 1.12803 + var parent = wysihtml.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, { 1.12804 + query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'), 1.12805 + }, null, composer.element), 1.12806 + range; 1.12807 + 1.12808 + if (parent) { 1.12809 + range = composer.selection.createRange(); 1.12810 + range.selectNode(parent); 1.12811 + composer.selection.setSelection(range); 1.12812 + } else if (!composer.isEmpty()) { 1.12813 + composer.selection.selectLine(); 1.12814 + } 1.12815 + } 1.12816 + 1.12817 + // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element 1.12818 + // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway 1.12819 + function selectElements(newBlockElements, composer) { 1.12820 + var range = composer.selection.createRange(), 1.12821 + lastEl = newBlockElements[newBlockElements.length - 1], 1.12822 + lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0; 1.12823 + 1.12824 + range.setStart(newBlockElements[0], 0); 1.12825 + range.setEnd(lastEl, lastOffset); 1.12826 + range.select(); 1.12827 + } 1.12828 + 1.12829 + // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each 1.12830 + // Return created/modified block level elements 1.12831 + // Method can be either "apply" or "remove" 1.12832 + function formatSelection(method, composer, options) { 1.12833 + var ranges = composer.selection.getOwnRanges(), 1.12834 + newBlockElements = [], 1.12835 + closestBlockName; 1.12836 + 1.12837 + // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th) 1.12838 + ranges = fixNotPermittedInsertionPoints(ranges); 1.12839 + 1.12840 + for (var i = ranges.length; i--;) { 1.12841 + fixRangeCoverage(ranges[i], composer); 1.12842 + closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer); 1.12843 + if (method === "remove") { 1.12844 + newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer)); 1.12845 + } else { 1.12846 + newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer)); 1.12847 + } 1.12848 + } 1.12849 + return newBlockElements; 1.12850 + } 1.12851 + 1.12852 + // If properties is passed as a string, look for tag with that tagName/query 1.12853 + function parseOptions(options) { 1.12854 + if (typeof options === "string") { 1.12855 + options = { 1.12856 + nodeName: options.toUpperCase() 1.12857 + }; 1.12858 + } 1.12859 + return options; 1.12860 + } 1.12861 + 1.12862 + function caretIsOnEmptyLine(composer) { 1.12863 + var caretInfo; 1.12864 + if (composer.selection.isCollapsed()) { 1.12865 + caretInfo = composer.selection.getNodesNearCaret(); 1.12866 + if (caretInfo && caretInfo.caretNode) { 1.12867 + if ( 1.12868 + // caret is allready breaknode 1.12869 + wysihtml.dom.domNode(caretInfo.caretNode).is.lineBreak() || 1.12870 + // caret is textnode 1.12871 + (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak())) || 1.12872 + // Caret is temprorary rangy selection marker 1.12873 + (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') && 1.12874 + (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.prevNode).is.block()) && 1.12875 + (!caretInfo.nextNode || wysihtml.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.nextNode).is.block()) 1.12876 + ) 1.12877 + ) { 1.12878 + return true; 1.12879 + } 1.12880 + } 1.12881 + } 1.12882 + return false; 1.12883 + } 1.12884 + 1.12885 + wysihtml.commands.formatBlock = { 1.12886 + exec: function(composer, command, options) { 1.12887 + options = parseOptions(options); 1.12888 + var newBlockElements = [], 1.12889 + ranges, range, bookmark, state, closestBlockName; 1.12890 + 1.12891 + // Find if current format state is active if options.toggle is set as true 1.12892 + // In toggle case active state elemets are formatted instead of working directly on selection 1.12893 + if (options && options.toggle) { 1.12894 + state = this.state(composer, command, options); 1.12895 + } 1.12896 + if (state) { 1.12897 + // Remove format from state nodes if toggle set and state on and selection is collapsed 1.12898 + bookmark = rangy.saveSelection(composer.win); 1.12899 + for (var j = 0, jmax = state.length; j < jmax; j++) { 1.12900 + removeOptionsFromElement(state[j], options, composer); 1.12901 + } 1.12902 + 1.12903 + } else { 1.12904 + // If selection is caret expand it to cover nearest suitable block element or row if none found 1.12905 + if (composer.selection.isCollapsed()) { 1.12906 + bookmark = rangy.saveSelection(composer.win); 1.12907 + if (caretIsOnEmptyLine(composer)) { 1.12908 + composer.selection.selectLine(); 1.12909 + } else { 1.12910 + expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined); 1.12911 + } 1.12912 + } 1.12913 + if (options) { 1.12914 + newBlockElements = formatSelection("apply", composer, options); 1.12915 + } else { 1.12916 + // Options == null means block formatting should be removed from selection 1.12917 + newBlockElements = formatSelection("remove", composer); 1.12918 + } 1.12919 + 1.12920 + } 1.12921 + 1.12922 + // Remove empty block elements that may be left behind 1.12923 + // Also remove them from new blocks list 1.12924 + newBlockElements = cleanup(composer, newBlockElements); 1.12925 + 1.12926 + // Restore selection 1.12927 + if (bookmark) { 1.12928 + rangy.restoreSelection(bookmark); 1.12929 + } else { 1.12930 + selectElements(newBlockElements, composer); 1.12931 + } 1.12932 + }, 1.12933 + 1.12934 + // Removes all block formatting from selection 1.12935 + remove: function(composer, command, options) { 1.12936 + options = parseOptions(options); 1.12937 + var newBlockElements, bookmark; 1.12938 + 1.12939 + // If selection is caret expand it to cover nearest suitable block element or row if none found 1.12940 + if (composer.selection.isCollapsed()) { 1.12941 + bookmark = rangy.saveSelection(composer.win); 1.12942 + expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined); 1.12943 + } 1.12944 + 1.12945 + newBlockElements = formatSelection("remove", composer); 1.12946 + newBlockElements = cleanup(composer, newBlockElements); 1.12947 + 1.12948 + // Restore selection 1.12949 + if (bookmark) { 1.12950 + rangy.restoreSelection(bookmark); 1.12951 + } else { 1.12952 + selectElements(newBlockElements, composer); 1.12953 + } 1.12954 + }, 1.12955 + 1.12956 + // If options as null is passed returns status describing all block level elements 1.12957 + state: function(composer, command, options) { 1.12958 + options = parseOptions(options); 1.12959 + 1.12960 + var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection 1.12961 + return wysihtml.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS }); 1.12962 + }).bind(this)), 1.12963 + parentNodes = composer.selection.getSelectedOwnNodes(), 1.12964 + parent; 1.12965 + 1.12966 + // Finds matching elements that are parents of selection and adds to nodes list 1.12967 + for (var i = 0, maxi = parentNodes.length; i < maxi; i++) { 1.12968 + parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element); 1.12969 + if (parent && nodes.indexOf(parent) === -1) { 1.12970 + nodes.push(parent); 1.12971 + } 1.12972 + } 1.12973 + 1.12974 + return (nodes.length === 0) ? false : nodes; 1.12975 + } 1.12976 + 1.12977 + }; 1.12978 +})(wysihtml); 1.12979 + 1.12980 +/** 1.12981 + * Unifies all inline tags additions and removals 1.12982 + * See https://github.com/Voog/wysihtml/pull/169 for specification of action 1.12983 + */ 1.12984 + 1.12985 +(function(wysihtml) { 1.12986 + 1.12987 + var defaultTag = "SPAN", 1.12988 + 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", 1.12989 + queryAliasMap = { 1.12990 + "b": "b, strong", 1.12991 + "strong": "b, strong", 1.12992 + "em": "em, i", 1.12993 + "i": "em, i" 1.12994 + }; 1.12995 + 1.12996 + function hasNoClass(element) { 1.12997 + return (/^\s*$/).test(element.className); 1.12998 + } 1.12999 + 1.13000 + function hasNoStyle(element) { 1.13001 + return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style')); 1.13002 + } 1.13003 + 1.13004 + // Associative arrays in javascript are really objects and do not have length defined 1.13005 + // Thus have to check emptyness in a different way 1.13006 + function hasNoAttributes(element) { 1.13007 + var attr = wysihtml.dom.getAttributes(element); 1.13008 + return wysihtml.lang.object(attr).isEmpty(); 1.13009 + } 1.13010 + 1.13011 + // compares two nodes if they are semantically the same 1.13012 + // Used in cleanup to find consequent semantically similar elements for merge 1.13013 + function isSameNode(element1, element2) { 1.13014 + var classes1, classes2, 1.13015 + attr1, attr2; 1.13016 + 1.13017 + if (element1.nodeType !== 1 || element2.nodeType !== 1) { 1.13018 + return false; 1.13019 + } 1.13020 + 1.13021 + if (element1.nodeName !== element2.nodeName) { 1.13022 + return false; 1.13023 + } 1.13024 + 1.13025 + classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' '); 1.13026 + classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' '); 1.13027 + if (wysihtml.lang.array(classes1).without(classes2).length > 0) { 1.13028 + return false; 1.13029 + } 1.13030 + 1.13031 + attr1 = wysihtml.dom.getAttributes(element1); 1.13032 + attr2 = wysihtml.dom.getAttributes(element2); 1.13033 + 1.13034 + if (attr1.length !== attr2.length || !wysihtml.lang.object(wysihtml.lang.object(attr1).difference(attr2)).isEmpty()) { 1.13035 + return false; 1.13036 + } 1.13037 + 1.13038 + return true; 1.13039 + } 1.13040 + 1.13041 + function createWrapNode(textNode, options) { 1.13042 + var nodeName = options && options.nodeName || defaultTag, 1.13043 + element = textNode.ownerDocument.createElement(nodeName); 1.13044 + 1.13045 + // Remove similar classes before applying className 1.13046 + if (options.classRegExp) { 1.13047 + element.className = element.className.replace(options.classRegExp, ""); 1.13048 + } 1.13049 + 1.13050 + if (options.className) { 1.13051 + element.classList.add(options.className); 1.13052 + } 1.13053 + 1.13054 + if (options.styleProperty && typeof options.styleValue !== "undefined") { 1.13055 + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue; 1.13056 + } 1.13057 + 1.13058 + if (options.attribute) { 1.13059 + if (typeof options.attribute === "object") { 1.13060 + for (var a in options.attribute) { 1.13061 + if (options.attribute.hasOwnProperty(a)) { 1.13062 + element.setAttribute(a, options.attribute[a]); 1.13063 + } 1.13064 + } 1.13065 + } else if (typeof options.attributeValue !== "undefined") { 1.13066 + element.setAttribute(options.attribute, options.attributeValue); 1.13067 + } 1.13068 + } 1.13069 + 1.13070 + return element; 1.13071 + } 1.13072 + 1.13073 + // Tests if attr2 list contains all attributes present in attr1 1.13074 + // Note: attr 1 can have more attributes than attr2 1.13075 + function containsSameAttributes(attr1, attr2) { 1.13076 + for (var a in attr1) { 1.13077 + if (attr1.hasOwnProperty(a)) { 1.13078 + if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) { 1.13079 + return false; 1.13080 + } 1.13081 + } 1.13082 + } 1.13083 + return true; 1.13084 + } 1.13085 + 1.13086 + // If attrbutes and values are the same > remove 1.13087 + // if attributes or values 1.13088 + function updateElementAttributes(element, newAttributes, toggle) { 1.13089 + var attr = wysihtml.dom.getAttributes(element), 1.13090 + fullContain = containsSameAttributes(newAttributes, attr), 1.13091 + attrDifference = wysihtml.lang.object(attr).difference(newAttributes), 1.13092 + a, b; 1.13093 + 1.13094 + if (fullContain && toggle !== false) { 1.13095 + for (a in newAttributes) { 1.13096 + if (newAttributes.hasOwnProperty(a)) { 1.13097 + element.removeAttribute(a); 1.13098 + } 1.13099 + } 1.13100 + } else { 1.13101 + 1.13102 + /*if (!wysihtml.lang.object(attrDifference).isEmpty()) { 1.13103 + for (b in attrDifference) { 1.13104 + if (attrDifference.hasOwnProperty(b)) { 1.13105 + element.removeAttribute(b); 1.13106 + } 1.13107 + } 1.13108 + }*/ 1.13109 + 1.13110 + for (a in newAttributes) { 1.13111 + if (newAttributes.hasOwnProperty(a)) { 1.13112 + element.setAttribute(a, newAttributes[a]); 1.13113 + } 1.13114 + } 1.13115 + } 1.13116 + } 1.13117 + 1.13118 + function updateFormatOfElement(element, options) { 1.13119 + var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch; 1.13120 + 1.13121 + if (options.className) { 1.13122 + if (options.toggle !== false && element.classList.contains(options.className)) { 1.13123 + element.classList.remove(options.className); 1.13124 + } else { 1.13125 + if (options.classRegExp) { 1.13126 + element.className = element.className.replace(options.classRegExp, ''); 1.13127 + } 1.13128 + element.classList.add(options.className); 1.13129 + } 1.13130 + if (hasNoClass(element)) { 1.13131 + element.removeAttribute('class'); 1.13132 + } 1.13133 + } 1.13134 + 1.13135 + // change/remove style 1.13136 + if (options.styleProperty) { 1.13137 + if (options.toggle !== false && element.style[wysihtml.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) { 1.13138 + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = ''; 1.13139 + } else { 1.13140 + element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue; 1.13141 + } 1.13142 + } 1.13143 + if (hasNoStyle(element)) { 1.13144 + element.removeAttribute('style'); 1.13145 + } 1.13146 + 1.13147 + if (options.attribute) { 1.13148 + if (typeof options.attribute === "object") { 1.13149 + newAttributes = options.attribute; 1.13150 + } else { 1.13151 + newAttributes = {}; 1.13152 + newAttributes[options.attribute] = options.attributeValue || ''; 1.13153 + } 1.13154 + updateElementAttributes(element, newAttributes, options.toggle); 1.13155 + } 1.13156 + 1.13157 + 1.13158 + // Handle similar semantically same elements (queryAliasMap) 1.13159 + nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null; 1.13160 + nodeQueryMatch = nodeNameQuery ? wysihtml.dom.domNode(element).test({ query: nodeNameQuery }) : false; 1.13161 + 1.13162 + // Unwrap element if no attributes present and node name given 1.13163 + // or no attributes and if no nodename set but node is the default 1.13164 + if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) { 1.13165 + if ( 1.13166 + ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) && 1.13167 + hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element) 1.13168 + ) { 1.13169 + wysihtml.dom.unwrap(element); 1.13170 + } 1.13171 + 1.13172 + } 1.13173 + } 1.13174 + 1.13175 + // Fetch all textnodes in selection 1.13176 + // Empty textnodes are ignored except the one containing text caret 1.13177 + function getSelectedTextNodes(selection, splitBounds) { 1.13178 + var textNodes = []; 1.13179 + 1.13180 + if (!selection.isCollapsed()) { 1.13181 + textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) { 1.13182 + // Exclude empty nodes except caret node 1.13183 + return (!wysihtml.dom.domNode(node).is.emptyTextNode()); 1.13184 + }, splitBounds)); 1.13185 + } 1.13186 + 1.13187 + return textNodes; 1.13188 + } 1.13189 + 1.13190 + function findSimilarTextNodeWrapper(textNode, options, container, exact) { 1.13191 + var node = textNode, 1.13192 + similarOptions = exact ? options : correctOptionsForSimilarityCheck(options); 1.13193 + 1.13194 + do { 1.13195 + if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) { 1.13196 + return node; 1.13197 + } 1.13198 + node = node.parentNode; 1.13199 + } while (node && node !== container); 1.13200 + 1.13201 + return null; 1.13202 + } 1.13203 + 1.13204 + function correctOptionsForSimilarityCheck(options) { 1.13205 + return { 1.13206 + nodeName: options.nodeName || null, 1.13207 + className: (!options.classRegExp) ? options.className || null : null, 1.13208 + classRegExp: options.classRegExp || null, 1.13209 + styleProperty: options.styleProperty || null 1.13210 + }; 1.13211 + } 1.13212 + 1.13213 + // Finds inline node with similar nodeName/style/className 1.13214 + // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes 1.13215 + function isSimilarNode(node, options) { 1.13216 + var o; 1.13217 + if (options.nodeName) { 1.13218 + var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase(); 1.13219 + return wysihtml.dom.domNode(node).test({ query: query }); 1.13220 + } else { 1.13221 + o = wysihtml.lang.object(options).clone(); 1.13222 + o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted 1.13223 + return wysihtml.dom.domNode(node).test(o); 1.13224 + } 1.13225 + } 1.13226 + 1.13227 + function selectRange(composer, range) { 1.13228 + var d = document.documentElement || document.body, 1.13229 + oldScrollTop = d.scrollTop, 1.13230 + oldScrollLeft = d.scrollLeft, 1.13231 + selection = rangy.getSelection(composer.win); 1.13232 + 1.13233 + rangy.getSelection(composer.win).removeAllRanges(); 1.13234 + 1.13235 + // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again 1.13236 + try { 1.13237 + rangy.getSelection(composer.win).addRange(range); 1.13238 + } catch (e) {} 1.13239 + if (!composer.doc.activeElement || !wysihtml.dom.contains(composer.element, composer.doc.activeElement)) { 1.13240 + composer.element.focus(); 1.13241 + d.scrollTop = oldScrollTop; 1.13242 + d.scrollLeft = oldScrollLeft; 1.13243 + rangy.getSelection(composer.win).addRange(range); 1.13244 + } 1.13245 + } 1.13246 + 1.13247 + function selectTextNodes(textNodes, composer) { 1.13248 + var range = rangy.createRange(composer.doc), 1.13249 + lastText = textNodes[textNodes.length - 1]; 1.13250 + 1.13251 + if (textNodes[0] && lastText) { 1.13252 + range.setStart(textNodes[0], 0); 1.13253 + range.setEnd(lastText, lastText.length); 1.13254 + selectRange(composer, range); 1.13255 + } 1.13256 + 1.13257 + } 1.13258 + 1.13259 + function selectTextNode(composer, node, start, end) { 1.13260 + var range = rangy.createRange(composer.doc); 1.13261 + if (node) { 1.13262 + range.setStart(node, start); 1.13263 + range.setEnd(node, typeof end !== 'undefined' ? end : start); 1.13264 + selectRange(composer, range); 1.13265 + } 1.13266 + } 1.13267 + 1.13268 + function getState(composer, options, exact) { 1.13269 + var searchNodes = getSelectedTextNodes(composer.selection), 1.13270 + nodes = [], 1.13271 + partial = false, 1.13272 + node, range, caretNode; 1.13273 + 1.13274 + if (composer.selection.isInThisEditable()) { 1.13275 + 1.13276 + if (searchNodes.length === 0 && composer.selection.isCollapsed()) { 1.13277 + caretNode = composer.selection.getSelection().anchorNode; 1.13278 + if (!caretNode) { 1.13279 + // selection not in editor 1.13280 + return { 1.13281 + nodes: [], 1.13282 + partial: false 1.13283 + }; 1.13284 + } 1.13285 + if (caretNode.nodeType === 3) { 1.13286 + searchNodes = [caretNode]; 1.13287 + } 1.13288 + } 1.13289 + 1.13290 + // Handle collapsed selection caret 1.13291 + if (!searchNodes.length) { 1.13292 + range = composer.selection.getOwnRanges()[0]; 1.13293 + if (range) { 1.13294 + searchNodes = [range.endContainer]; 1.13295 + } 1.13296 + } 1.13297 + 1.13298 + for (var i = 0, maxi = searchNodes.length; i < maxi; i++) { 1.13299 + node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact); 1.13300 + if (node) { 1.13301 + nodes.push(node); 1.13302 + } else { 1.13303 + partial = true; 1.13304 + } 1.13305 + } 1.13306 + 1.13307 + } 1.13308 + 1.13309 + return { 1.13310 + nodes: nodes, 1.13311 + partial: partial 1.13312 + }; 1.13313 + } 1.13314 + 1.13315 + // Returns if caret is inside a word in textnode (not on boundary) 1.13316 + // If selection anchornode is not text node, returns false 1.13317 + function caretIsInsideWord(selection) { 1.13318 + var anchor, offset, beforeChar, afterChar; 1.13319 + if (selection) { 1.13320 + anchor = selection.anchorNode; 1.13321 + offset = selection.anchorOffset; 1.13322 + if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) { 1.13323 + beforeChar = anchor.data[offset - 1]; 1.13324 + afterChar = anchor.data[offset]; 1.13325 + return (/\w/).test(beforeChar) && (/\w/).test(afterChar); 1.13326 + } 1.13327 + } 1.13328 + return false; 1.13329 + } 1.13330 + 1.13331 + // Returns a range and textnode containing object from caret position covering a whole word 1.13332 + // wordOffsety describes the original position of caret in the new textNode 1.13333 + // Caret has to be inside a textNode. 1.13334 + function getRangeForWord(selection) { 1.13335 + var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar, 1.13336 + txtNodes = []; 1.13337 + if (selection) { 1.13338 + anchor = selection.anchorNode; 1.13339 + offset = offsetStart = offsetEnd = selection.anchorOffset; 1.13340 + doc = anchor.ownerDocument; 1.13341 + range = rangy.createRange(doc); 1.13342 + 1.13343 + if (anchor && anchor.nodeType === 3) { 1.13344 + 1.13345 + while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) { 1.13346 + offsetStart--; 1.13347 + } 1.13348 + 1.13349 + while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) { 1.13350 + offsetEnd++; 1.13351 + } 1.13352 + 1.13353 + range.setStartAndEnd(anchor, offsetStart, offsetEnd); 1.13354 + range.splitBoundaries(); 1.13355 + txtNodes = range.getNodes([3], function(node) { 1.13356 + return (!wysihtml.dom.domNode(node).is.emptyTextNode()); 1.13357 + }); 1.13358 + 1.13359 + return { 1.13360 + wordOffset: offset - offsetStart, 1.13361 + range: range, 1.13362 + textNode: txtNodes[0] 1.13363 + }; 1.13364 + 1.13365 + } 1.13366 + } 1.13367 + return false; 1.13368 + } 1.13369 + 1.13370 + // Contents of 2 elements are merged to fitst element. second element is removed as consequence 1.13371 + function mergeContents(element1, element2) { 1.13372 + while (element2.firstChild) { 1.13373 + element1.appendChild(element2.firstChild); 1.13374 + } 1.13375 + element2.parentNode.removeChild(element2); 1.13376 + } 1.13377 + 1.13378 + function mergeConsequentSimilarElements(elements) { 1.13379 + for (var i = elements.length; i--;) { 1.13380 + 1.13381 + if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup 1.13382 + 1.13383 + if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) { 1.13384 + mergeContents(elements[i], elements[i].nextSibling); 1.13385 + } 1.13386 + 1.13387 + if (elements[i].previousSibling && isSameNode(elements[i] , elements[i].previousSibling)) { 1.13388 + mergeContents(elements[i].previousSibling, elements[i]); 1.13389 + } 1.13390 + 1.13391 + } 1.13392 + } 1.13393 + } 1.13394 + 1.13395 + function cleanupAndSetSelection(composer, textNodes, options) { 1.13396 + if (textNodes.length > 0) { 1.13397 + selectTextNodes(textNodes, composer); 1.13398 + } 1.13399 + mergeConsequentSimilarElements(getState(composer, options).nodes); 1.13400 + if (textNodes.length > 0) { 1.13401 + selectTextNodes(textNodes, composer); 1.13402 + } 1.13403 + } 1.13404 + 1.13405 + function cleanupAndSetCaret(composer, textNode, offset, options) { 1.13406 + selectTextNode(composer, textNode, offset); 1.13407 + mergeConsequentSimilarElements(getState(composer, options).nodes); 1.13408 + selectTextNode(composer, textNode, offset); 1.13409 + } 1.13410 + 1.13411 + // Formats a textnode with given options 1.13412 + function formatTextNode(textNode, options) { 1.13413 + var wrapNode = createWrapNode(textNode, options); 1.13414 + 1.13415 + textNode.parentNode.insertBefore(wrapNode, textNode); 1.13416 + wrapNode.appendChild(textNode); 1.13417 + } 1.13418 + 1.13419 + // Changes/toggles format of a textnode 1.13420 + function unformatTextNode(textNode, composer, options) { 1.13421 + var container = composer.element, 1.13422 + wrapNode = findSimilarTextNodeWrapper(textNode, options, container), 1.13423 + newWrapNode; 1.13424 + 1.13425 + if (wrapNode) { 1.13426 + newWrapNode = wrapNode.cloneNode(false); 1.13427 + 1.13428 + wysihtml.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode); 1.13429 + updateFormatOfElement(newWrapNode, options); 1.13430 + } 1.13431 + } 1.13432 + 1.13433 + // Removes the format around textnode 1.13434 + function removeFormatFromTextNode(textNode, composer, options) { 1.13435 + var container = composer.element, 1.13436 + wrapNode = findSimilarTextNodeWrapper(textNode, options, container); 1.13437 + 1.13438 + if (wrapNode) { 1.13439 + wysihtml.dom.domNode(textNode).escapeParent(wrapNode); 1.13440 + } 1.13441 + } 1.13442 + 1.13443 + // Creates node around caret formated with options 1.13444 + function formatTextRange(range, composer, options) { 1.13445 + var wrapNode = createWrapNode(range.endContainer, options); 1.13446 + 1.13447 + range.surroundContents(wrapNode); 1.13448 + composer.selection.selectNode(wrapNode); 1.13449 + } 1.13450 + 1.13451 + // Changes/toggles format of whole selection 1.13452 + function updateFormat(composer, textNodes, state, options) { 1.13453 + var exactState = getState(composer, options, true), 1.13454 + selection = composer.selection.getSelection(), 1.13455 + wordObj, textNode, newNode, i; 1.13456 + 1.13457 + if (!textNodes.length) { 1.13458 + // Selection is caret 1.13459 + 1.13460 + 1.13461 + if (options.toggle !== false) { 1.13462 + if (caretIsInsideWord(selection)) { 1.13463 + 1.13464 + // Unformat whole word 1.13465 + wordObj = getRangeForWord(selection); 1.13466 + textNode = wordObj.textNode; 1.13467 + unformatTextNode(wordObj.textNode, composer, options); 1.13468 + cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options); 1.13469 + 1.13470 + } else { 1.13471 + 1.13472 + // Escape caret out of format 1.13473 + textNode = composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE); 1.13474 + newNode = state.nodes[0].cloneNode(false); 1.13475 + newNode.appendChild(textNode); 1.13476 + composer.selection.splitElementAtCaret(state.nodes[0], newNode); 1.13477 + updateFormatOfElement(newNode, options); 1.13478 + cleanupAndSetSelection(composer, [textNode], options); 1.13479 + var s = composer.selection.getSelection(); 1.13480 + if (s.anchorNode && s.focusNode) { 1.13481 + // Has an error in IE when collapsing selection. probably from rangy 1.13482 + try { 1.13483 + s.collapseToEnd(); 1.13484 + } catch (e) {} 1.13485 + } 1.13486 + } 1.13487 + } else { 1.13488 + // In non-toggle mode the closest state element has to be found and the state updated differently 1.13489 + for (i = state.nodes.length; i--;) { 1.13490 + updateFormatOfElement(state.nodes[i], options); 1.13491 + } 1.13492 + } 1.13493 + 1.13494 + } else { 1.13495 + 1.13496 + if (!exactState.partial && options.toggle !== false) { 1.13497 + 1.13498 + // If whole selection (all textnodes) are in the applied format 1.13499 + // remove the format from selection 1.13500 + // Non-toggle mode never removes. Remove has to be called explicitly 1.13501 + for (i = textNodes.length; i--;) { 1.13502 + unformatTextNode(textNodes[i], composer, options); 1.13503 + } 1.13504 + 1.13505 + } else { 1.13506 + 1.13507 + // Selection is partially in format 1.13508 + // change it to new if format if textnode allreafy in similar state 1.13509 + // else just apply 1.13510 + 1.13511 + for (i = textNodes.length; i--;) { 1.13512 + 1.13513 + if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) { 1.13514 + unformatTextNode(textNodes[i], composer, options); 1.13515 + } 1.13516 + 1.13517 + if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) { 1.13518 + formatTextNode(textNodes[i], options); 1.13519 + } 1.13520 + } 1.13521 + 1.13522 + } 1.13523 + 1.13524 + cleanupAndSetSelection(composer, textNodes, options); 1.13525 + } 1.13526 + } 1.13527 + 1.13528 + // Removes format from selection 1.13529 + function removeFormat(composer, textNodes, state, options) { 1.13530 + var textNode, textOffset, newNode, i, 1.13531 + selection = composer.selection.getSelection(); 1.13532 + 1.13533 + if (!textNodes.length) { 1.13534 + textNode = selection.anchorNode; 1.13535 + textOffset = selection.anchorOffset; 1.13536 + 1.13537 + for (i = state.nodes.length; i--;) { 1.13538 + wysihtml.dom.unwrap(state.nodes[i]); 1.13539 + } 1.13540 + 1.13541 + cleanupAndSetCaret(composer, textNode, textOffset, options); 1.13542 + } else { 1.13543 + for (i = textNodes.length; i--;) { 1.13544 + removeFormatFromTextNode(textNodes[i], composer, options); 1.13545 + } 1.13546 + cleanupAndSetSelection(composer, textNodes, options); 1.13547 + } 1.13548 + } 1.13549 + 1.13550 + // Adds format to selection 1.13551 + function applyFormat(composer, textNodes, options) { 1.13552 + var wordObj, i, 1.13553 + selection = composer.selection.getSelection(); 1.13554 + 1.13555 + if (!textNodes.length) { 1.13556 + // Handle collapsed selection caret and return 1.13557 + if (caretIsInsideWord(selection)) { 1.13558 + 1.13559 + wordObj = getRangeForWord(selection); 1.13560 + formatTextNode(wordObj.textNode, options); 1.13561 + cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options); 1.13562 + 1.13563 + } else { 1.13564 + var r = composer.selection.getOwnRanges()[0]; 1.13565 + if (r) { 1.13566 + formatTextRange(r, composer, options); 1.13567 + } 1.13568 + } 1.13569 + 1.13570 + } else { 1.13571 + // Handle textnodes in selection and apply format 1.13572 + for (i = textNodes.length; i--;) { 1.13573 + formatTextNode(textNodes[i], options); 1.13574 + } 1.13575 + cleanupAndSetSelection(composer, textNodes, options); 1.13576 + } 1.13577 + } 1.13578 + 1.13579 + // If properties is passed as a string, correct options with that nodeName 1.13580 + function fixOptions(options) { 1.13581 + options = (typeof options === "string") ? { nodeName: options } : options; 1.13582 + if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); } 1.13583 + return options; 1.13584 + } 1.13585 + 1.13586 + wysihtml.commands.formatInline = { 1.13587 + 1.13588 + // Basics: 1.13589 + // In case of plain text or inline state not set wrap all non-empty textnodes with 1.13590 + // 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) 1.13591 + // In case of changing mode every textnode is addressed separatly 1.13592 + exec: function(composer, command, options) { 1.13593 + options = fixOptions(options); 1.13594 + 1.13595 + // Join adjactent textnodes first 1.13596 + composer.element.normalize(); 1.13597 + 1.13598 + var textNodes = getSelectedTextNodes(composer.selection, true), 1.13599 + state = getState(composer, options); 1.13600 + if (state.nodes.length > 0) { 1.13601 + // Text allready has the format applied 1.13602 + updateFormat(composer, textNodes, state, options); 1.13603 + } else { 1.13604 + // Selection is not in the applied format 1.13605 + applyFormat(composer, textNodes, options); 1.13606 + } 1.13607 + composer.element.normalize(); 1.13608 + }, 1.13609 + 1.13610 + remove: function(composer, command, options) { 1.13611 + options = fixOptions(options); 1.13612 + composer.element.normalize(); 1.13613 + 1.13614 + var textNodes = getSelectedTextNodes(composer.selection, true), 1.13615 + state = getState(composer, options); 1.13616 + 1.13617 + if (state.nodes.length > 0) { 1.13618 + // Text allready has the format applied 1.13619 + removeFormat(composer, textNodes, state, options); 1.13620 + } 1.13621 + 1.13622 + composer.element.normalize(); 1.13623 + }, 1.13624 + 1.13625 + state: function(composer, command, options) { 1.13626 + options = fixOptions(options); 1.13627 + var nodes = getState(composer, options, true).nodes; 1.13628 + return (nodes.length === 0) ? false : nodes; 1.13629 + } 1.13630 + }; 1.13631 + 1.13632 +})(wysihtml); 1.13633 + 1.13634 +(function(wysihtml){ 1.13635 + wysihtml.commands.indentList = { 1.13636 + exec: function(composer, command, value) { 1.13637 + var listEls = composer.selection.getSelectionParentsByTag('LI'); 1.13638 + if (listEls) { 1.13639 + return this.tryToPushLiLevel(listEls, composer.selection); 1.13640 + } 1.13641 + return false; 1.13642 + }, 1.13643 + 1.13644 + state: function(composer, command) { 1.13645 + return false; 1.13646 + }, 1.13647 + 1.13648 + tryToPushLiLevel: function(liNodes, selection) { 1.13649 + var listTag, list, prevLi, liNode, prevLiList, 1.13650 + found = false; 1.13651 + 1.13652 + selection.executeAndRestoreRangy(function() { 1.13653 + 1.13654 + for (var i = liNodes.length; i--;) { 1.13655 + liNode = liNodes[i]; 1.13656 + listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL'; 1.13657 + list = liNode.ownerDocument.createElement(listTag); 1.13658 + prevLi = wysihtml.dom.domNode(liNode).prev({nodeTypes: [wysihtml.ELEMENT_NODE]}); 1.13659 + prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null; 1.13660 + 1.13661 + if (prevLi) { 1.13662 + if (prevLiList) { 1.13663 + prevLiList.appendChild(liNode); 1.13664 + } else { 1.13665 + list.appendChild(liNode); 1.13666 + prevLi.appendChild(list); 1.13667 + } 1.13668 + found = true; 1.13669 + } 1.13670 + } 1.13671 + 1.13672 + }); 1.13673 + return found; 1.13674 + } 1.13675 + }; 1.13676 +}(wysihtml)); 1.13677 + 1.13678 +(function(wysihtml){ 1.13679 + wysihtml.commands.insertHTML = { 1.13680 + exec: function(composer, command, html) { 1.13681 + composer.selection.insertHTML(html); 1.13682 + }, 1.13683 + 1.13684 + state: function() { 1.13685 + return false; 1.13686 + } 1.13687 + }; 1.13688 +}(wysihtml)); 1.13689 + 1.13690 +(function(wysihtml) { 1.13691 + var LINE_BREAK = "<br>" + (wysihtml.browser.needsSpaceAfterLineBreak() ? " " : ""); 1.13692 + 1.13693 + wysihtml.commands.insertLineBreak = { 1.13694 + exec: function(composer, command) { 1.13695 + composer.selection.insertHTML(LINE_BREAK); 1.13696 + }, 1.13697 + 1.13698 + state: function() { 1.13699 + return false; 1.13700 + } 1.13701 + }; 1.13702 +})(wysihtml); 1.13703 + 1.13704 +wysihtml.commands.insertList = (function(wysihtml) { 1.13705 + 1.13706 + var isNode = function(node, name) { 1.13707 + if (node && node.nodeName) { 1.13708 + if (typeof name === 'string') { 1.13709 + name = [name]; 1.13710 + } 1.13711 + for (var n = name.length; n--;) { 1.13712 + if (node.nodeName === name[n]) { 1.13713 + return true; 1.13714 + } 1.13715 + } 1.13716 + } 1.13717 + return false; 1.13718 + }; 1.13719 + 1.13720 + var findListEl = function(node, nodeName, composer) { 1.13721 + var ret = { 1.13722 + el: null, 1.13723 + other: false 1.13724 + }; 1.13725 + 1.13726 + if (node) { 1.13727 + var parentLi = wysihtml.dom.getParentElement(node, { query: "li" }, false, composer.element), 1.13728 + otherNodeName = (nodeName === "UL") ? "OL" : "UL"; 1.13729 + 1.13730 + if (isNode(node, nodeName)) { 1.13731 + ret.el = node; 1.13732 + } else if (isNode(node, otherNodeName)) { 1.13733 + ret = { 1.13734 + el: node, 1.13735 + other: true 1.13736 + }; 1.13737 + } else if (parentLi) { 1.13738 + if (isNode(parentLi.parentNode, nodeName)) { 1.13739 + ret.el = parentLi.parentNode; 1.13740 + } else if (isNode(parentLi.parentNode, otherNodeName)) { 1.13741 + ret = { 1.13742 + el : parentLi.parentNode, 1.13743 + other: true 1.13744 + }; 1.13745 + } 1.13746 + } 1.13747 + } 1.13748 + 1.13749 + // do not count list elements outside of composer 1.13750 + if (ret.el && !composer.element.contains(ret.el)) { 1.13751 + ret.el = null; 1.13752 + } 1.13753 + 1.13754 + return ret; 1.13755 + }; 1.13756 + 1.13757 + var handleSameTypeList = function(el, nodeName, composer) { 1.13758 + var otherNodeName = (nodeName === "UL") ? "OL" : "UL", 1.13759 + otherLists, innerLists; 1.13760 + // Unwrap list 1.13761 + // <ul><li>foo</li><li>bar</li></ul> 1.13762 + // becomes: 1.13763 + // foo<br>bar<br> 1.13764 + 1.13765 + composer.selection.executeAndRestoreRangy(function() { 1.13766 + otherLists = getListsInSelection(otherNodeName, composer); 1.13767 + if (otherLists.length) { 1.13768 + for (var l = otherLists.length; l--;) { 1.13769 + wysihtml.dom.renameElement(otherLists[l], nodeName.toLowerCase()); 1.13770 + } 1.13771 + } else { 1.13772 + innerLists = getListsInSelection(['OL', 'UL'], composer); 1.13773 + for (var i = innerLists.length; i--;) { 1.13774 + wysihtml.dom.resolveList(innerLists[i], composer.config.useLineBreaks); 1.13775 + } 1.13776 + if (innerLists.length === 0) { 1.13777 + wysihtml.dom.resolveList(el, composer.config.useLineBreaks); 1.13778 + } 1.13779 + } 1.13780 + }); 1.13781 + }; 1.13782 + 1.13783 + var handleOtherTypeList = function(el, nodeName, composer) { 1.13784 + var otherNodeName = (nodeName === "UL") ? "OL" : "UL"; 1.13785 + // Turn an ordered list into an unordered list 1.13786 + // <ol><li>foo</li><li>bar</li></ol> 1.13787 + // becomes: 1.13788 + // <ul><li>foo</li><li>bar</li></ul> 1.13789 + // Also rename other lists in selection 1.13790 + composer.selection.executeAndRestoreRangy(function() { 1.13791 + var renameLists = [el].concat(getListsInSelection(otherNodeName, composer)); 1.13792 + 1.13793 + // All selection inner lists get renamed too 1.13794 + for (var l = renameLists.length; l--;) { 1.13795 + wysihtml.dom.renameElement(renameLists[l], nodeName.toLowerCase()); 1.13796 + } 1.13797 + }); 1.13798 + }; 1.13799 + 1.13800 + var getListsInSelection = function(nodeName, composer) { 1.13801 + var ranges = composer.selection.getOwnRanges(), 1.13802 + renameLists = []; 1.13803 + 1.13804 + for (var r = ranges.length; r--;) { 1.13805 + renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) { 1.13806 + return isNode(node, nodeName); 1.13807 + })); 1.13808 + } 1.13809 + 1.13810 + return renameLists; 1.13811 + }; 1.13812 + 1.13813 + var createListFallback = function(nodeName, composer) { 1.13814 + var sel = rangy.saveSelection(composer.win); 1.13815 + 1.13816 + // Fallback for Create list 1.13817 + var tempClassName = "_wysihtml-temp-" + new Date().getTime(), 1.13818 + isEmpty, list; 1.13819 + 1.13820 + composer.commands.exec("formatBlock", { 1.13821 + "nodeName": "div", 1.13822 + "className": tempClassName 1.13823 + }); 1.13824 + 1.13825 + var tempElement = composer.element.querySelector("." + tempClassName); 1.13826 + 1.13827 + // This space causes new lists to never break on enter 1.13828 + var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; 1.13829 + tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ""); 1.13830 + if (tempElement) { 1.13831 + isEmpty = (/^(\s|(<br>))+$/i).test(tempElement.innerHTML); 1.13832 + list = wysihtml.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer); 1.13833 + if (sel) { 1.13834 + rangy.restoreSelection(sel); 1.13835 + } 1.13836 + if (isEmpty) { 1.13837 + composer.selection.selectNode(list.querySelector("li"), true); 1.13838 + } 1.13839 + } 1.13840 + }; 1.13841 + 1.13842 + return { 1.13843 + exec: function(composer, command, nodeName) { 1.13844 + var doc = composer.doc, 1.13845 + cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList", 1.13846 + s = composer.selection.getSelection(), 1.13847 + anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, 1.13848 + fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode, 1.13849 + selectedNode, list; 1.13850 + 1.13851 + if (s.isBackwards()) { 1.13852 + // swap variables 1.13853 + anode = [fnode, fnode = anode][0]; 1.13854 + } 1.13855 + 1.13856 + if (wysihtml.dom.domNode(fnode).is.emptyTextNode(true) && fnode) { 1.13857 + fnode = wysihtml.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.13858 + } 1.13859 + if (wysihtml.dom.domNode(anode).is.emptyTextNode(true) && anode) { 1.13860 + anode = wysihtml.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.13861 + } 1.13862 + 1.13863 + if (anode && fnode) { 1.13864 + if (anode === fnode) { 1.13865 + selectedNode = anode; 1.13866 + } else { 1.13867 + selectedNode = wysihtml.dom.domNode(anode).commonAncestor(fnode, composer.element); 1.13868 + } 1.13869 + } else { 1.13870 + selectedNode = composer.selection.getSelectedNode(); 1.13871 + } 1.13872 + 1.13873 + list = findListEl(selectedNode, nodeName, composer); 1.13874 + 1.13875 + if (!list.el) { 1.13876 + if (composer.commands.support(cmd)) { 1.13877 + doc.execCommand(cmd, false, null); 1.13878 + } else { 1.13879 + createListFallback(nodeName, composer); 1.13880 + } 1.13881 + } else if (list.other) { 1.13882 + handleOtherTypeList(list.el, nodeName, composer); 1.13883 + } else { 1.13884 + handleSameTypeList(list.el, nodeName, composer); 1.13885 + } 1.13886 + }, 1.13887 + 1.13888 + state: function(composer, command, nodeName) { 1.13889 + var selectedNode = composer.selection.getSelectedNode(), 1.13890 + list = findListEl(selectedNode, nodeName, composer); 1.13891 + 1.13892 + return (list.el && !list.other) ? list.el : false; 1.13893 + } 1.13894 + }; 1.13895 + 1.13896 +})(wysihtml); 1.13897 + 1.13898 +(function(wysihtml){ 1.13899 + 1.13900 + wysihtml.commands.outdentList = { 1.13901 + exec: function(composer, command, value) { 1.13902 + var listEls = composer.selection.getSelectionParentsByTag('LI'); 1.13903 + if (listEls) { 1.13904 + return this.tryToPullLiLevel(listEls, composer); 1.13905 + } 1.13906 + return false; 1.13907 + }, 1.13908 + 1.13909 + state: function(composer, command) { 1.13910 + return false; 1.13911 + }, 1.13912 + 1.13913 + tryToPullLiLevel: function(liNodes, composer) { 1.13914 + var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList, 1.13915 + found = false, 1.13916 + that = this; 1.13917 + 1.13918 + composer.selection.executeAndRestoreRangy(function() { 1.13919 + 1.13920 + for (var i = liNodes.length; i--;) { 1.13921 + liNode = liNodes[i]; 1.13922 + if (liNode.parentNode) { 1.13923 + listNode = liNode.parentNode; 1.13924 + 1.13925 + if (listNode.tagName === 'OL' || listNode.tagName === 'UL') { 1.13926 + found = true; 1.13927 + 1.13928 + outerListNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element); 1.13929 + outerLiNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element); 1.13930 + 1.13931 + if (outerListNode && outerLiNode) { 1.13932 + 1.13933 + if (liNode.nextSibling) { 1.13934 + afterList = that.getAfterList(listNode, liNode); 1.13935 + liNode.appendChild(afterList); 1.13936 + } 1.13937 + outerListNode.insertBefore(liNode, outerLiNode.nextSibling); 1.13938 + 1.13939 + } else { 1.13940 + 1.13941 + if (liNode.nextSibling) { 1.13942 + afterList = that.getAfterList(listNode, liNode); 1.13943 + liNode.appendChild(afterList); 1.13944 + } 1.13945 + 1.13946 + for (var j = liNode.childNodes.length; j--;) { 1.13947 + listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling); 1.13948 + } 1.13949 + 1.13950 + listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling); 1.13951 + liNode.parentNode.removeChild(liNode); 1.13952 + 1.13953 + } 1.13954 + 1.13955 + // cleanup 1.13956 + if (listNode.childNodes.length === 0) { 1.13957 + listNode.parentNode.removeChild(listNode); 1.13958 + } 1.13959 + } 1.13960 + } 1.13961 + } 1.13962 + 1.13963 + }); 1.13964 + return found; 1.13965 + }, 1.13966 + 1.13967 + getAfterList: function(listNode, liNode) { 1.13968 + var nodeName = listNode.nodeName, 1.13969 + newList = document.createElement(nodeName); 1.13970 + 1.13971 + while (liNode.nextSibling) { 1.13972 + newList.appendChild(liNode.nextSibling); 1.13973 + } 1.13974 + return newList; 1.13975 + } 1.13976 + 1.13977 + }; 1.13978 +}(wysihtml)); 1.13979 + 1.13980 +(function(wysihtml){ 1.13981 + wysihtml.commands.redo = { 1.13982 + exec: function(composer) { 1.13983 + return composer.undoManager.redo(); 1.13984 + }, 1.13985 + 1.13986 + state: function(composer) { 1.13987 + return false; 1.13988 + } 1.13989 + }; 1.13990 +}(wysihtml)); 1.13991 + 1.13992 +(function(wysihtml) { 1.13993 + 1.13994 + var nodeOptions = { 1.13995 + nodeName: "A" 1.13996 + }; 1.13997 + 1.13998 + wysihtml.commands.removeLink = { 1.13999 + exec: function(composer, command) { 1.14000 + wysihtml.commands.formatInline.remove(composer, command, nodeOptions); 1.14001 + }, 1.14002 + 1.14003 + state: function(composer, command) { 1.14004 + return wysihtml.commands.formatInline.state(composer, command, nodeOptions); 1.14005 + } 1.14006 + }; 1.14007 + 1.14008 +})(wysihtml); 1.14009 + 1.14010 +(function(wysihtml){ 1.14011 + wysihtml.commands.undo = { 1.14012 + exec: function(composer) { 1.14013 + return composer.undoManager.undo(); 1.14014 + }, 1.14015 + 1.14016 + state: function(composer) { 1.14017 + return false; 1.14018 + } 1.14019 + }; 1.14020 +}(wysihtml)); 1.14021 + 1.14022 +/** 1.14023 + * Undo Manager for wysihtml 1.14024 + * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface 1.14025 + */ 1.14026 +(function(wysihtml) { 1.14027 + var Z_KEY = 90, 1.14028 + Y_KEY = 89, 1.14029 + BACKSPACE_KEY = 8, 1.14030 + DELETE_KEY = 46, 1.14031 + MAX_HISTORY_ENTRIES = 25, 1.14032 + DATA_ATTR_NODE = "data-wysihtml-selection-node", 1.14033 + DATA_ATTR_OFFSET = "data-wysihtml-selection-offset", 1.14034 + UNDO_HTML = '<span id="_wysihtml-undo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>', 1.14035 + REDO_HTML = '<span id="_wysihtml-redo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>', 1.14036 + dom = wysihtml.dom; 1.14037 + 1.14038 + function cleanTempElements(doc) { 1.14039 + var tempElement; 1.14040 + while (tempElement = doc.querySelector("._wysihtml-temp")) { 1.14041 + tempElement.parentNode.removeChild(tempElement); 1.14042 + } 1.14043 + } 1.14044 + 1.14045 + wysihtml.UndoManager = wysihtml.lang.Dispatcher.extend( 1.14046 + /** @scope wysihtml.UndoManager.prototype */ { 1.14047 + constructor: function(editor) { 1.14048 + this.editor = editor; 1.14049 + this.composer = editor.composer; 1.14050 + this.element = this.composer.element; 1.14051 + 1.14052 + this.position = 0; 1.14053 + this.historyStr = []; 1.14054 + this.historyDom = []; 1.14055 + 1.14056 + this.transact(); 1.14057 + 1.14058 + this._observe(); 1.14059 + }, 1.14060 + 1.14061 + _observe: function() { 1.14062 + var that = this, 1.14063 + doc = this.composer.sandbox.getDocument(), 1.14064 + lastKey; 1.14065 + 1.14066 + // Catch CTRL+Z and CTRL+Y 1.14067 + dom.observe(this.element, "keydown", function(event) { 1.14068 + if (event.altKey || (!event.ctrlKey && !event.metaKey)) { 1.14069 + return; 1.14070 + } 1.14071 + 1.14072 + var keyCode = event.keyCode, 1.14073 + isUndo = keyCode === Z_KEY && !event.shiftKey, 1.14074 + isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); 1.14075 + 1.14076 + if (isUndo) { 1.14077 + that.undo(); 1.14078 + event.preventDefault(); 1.14079 + } else if (isRedo) { 1.14080 + that.redo(); 1.14081 + event.preventDefault(); 1.14082 + } 1.14083 + }); 1.14084 + 1.14085 + // Catch delete and backspace 1.14086 + dom.observe(this.element, "keydown", function(event) { 1.14087 + var keyCode = event.keyCode; 1.14088 + if (keyCode === lastKey) { 1.14089 + return; 1.14090 + } 1.14091 + 1.14092 + lastKey = keyCode; 1.14093 + 1.14094 + if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { 1.14095 + that.transact(); 1.14096 + } 1.14097 + }); 1.14098 + 1.14099 + this.editor 1.14100 + .on("newword:composer", function() { 1.14101 + that.transact(); 1.14102 + }) 1.14103 + 1.14104 + .on("beforecommand:composer", function() { 1.14105 + that.transact(); 1.14106 + }); 1.14107 + }, 1.14108 + 1.14109 + transact: function() { 1.14110 + var previousHtml = this.historyStr[this.position - 1], 1.14111 + currentHtml = this.composer.getValue(false, false), 1.14112 + composerIsVisible = this.element.offsetWidth > 0 && this.element.offsetHeight > 0, 1.14113 + range, node, offset, element, position; 1.14114 + 1.14115 + if (currentHtml === previousHtml) { 1.14116 + return; 1.14117 + } 1.14118 + 1.14119 + var length = this.historyStr.length = this.historyDom.length = this.position; 1.14120 + if (length > MAX_HISTORY_ENTRIES) { 1.14121 + this.historyStr.shift(); 1.14122 + this.historyDom.shift(); 1.14123 + this.position--; 1.14124 + } 1.14125 + 1.14126 + this.position++; 1.14127 + 1.14128 + if (composerIsVisible) { 1.14129 + // Do not start saving selection if composer is not visible 1.14130 + range = this.composer.selection.getRange(); 1.14131 + node = (range && range.startContainer) ? range.startContainer : this.element; 1.14132 + offset = (range && range.startOffset) ? range.startOffset : 0; 1.14133 + 1.14134 + if (node.nodeType === wysihtml.ELEMENT_NODE) { 1.14135 + element = node; 1.14136 + } else { 1.14137 + element = node.parentNode; 1.14138 + position = this.getChildNodeIndex(element, node); 1.14139 + } 1.14140 + 1.14141 + element.setAttribute(DATA_ATTR_OFFSET, offset); 1.14142 + if (typeof(position) !== "undefined") { 1.14143 + element.setAttribute(DATA_ATTR_NODE, position); 1.14144 + } 1.14145 + } 1.14146 + 1.14147 + var clone = this.element.cloneNode(!!currentHtml); 1.14148 + this.historyDom.push(clone); 1.14149 + this.historyStr.push(currentHtml); 1.14150 + 1.14151 + if (element) { 1.14152 + element.removeAttribute(DATA_ATTR_OFFSET); 1.14153 + element.removeAttribute(DATA_ATTR_NODE); 1.14154 + } 1.14155 + 1.14156 + }, 1.14157 + 1.14158 + undo: function() { 1.14159 + this.transact(); 1.14160 + 1.14161 + if (!this.undoPossible()) { 1.14162 + return; 1.14163 + } 1.14164 + 1.14165 + this.set(this.historyDom[--this.position - 1]); 1.14166 + this.editor.fire("undo:composer"); 1.14167 + }, 1.14168 + 1.14169 + redo: function() { 1.14170 + if (!this.redoPossible()) { 1.14171 + return; 1.14172 + } 1.14173 + 1.14174 + this.set(this.historyDom[++this.position - 1]); 1.14175 + this.editor.fire("redo:composer"); 1.14176 + }, 1.14177 + 1.14178 + undoPossible: function() { 1.14179 + return this.position > 1; 1.14180 + }, 1.14181 + 1.14182 + redoPossible: function() { 1.14183 + return this.position < this.historyStr.length; 1.14184 + }, 1.14185 + 1.14186 + set: function(historyEntry) { 1.14187 + this.element.innerHTML = ""; 1.14188 + 1.14189 + var i = 0, 1.14190 + childNodes = historyEntry.childNodes, 1.14191 + length = historyEntry.childNodes.length; 1.14192 + 1.14193 + for (; i<length; i++) { 1.14194 + this.element.appendChild(childNodes[i].cloneNode(true)); 1.14195 + } 1.14196 + 1.14197 + // Restore selection 1.14198 + var offset, 1.14199 + node, 1.14200 + position; 1.14201 + 1.14202 + if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) { 1.14203 + offset = historyEntry.getAttribute(DATA_ATTR_OFFSET); 1.14204 + position = historyEntry.getAttribute(DATA_ATTR_NODE); 1.14205 + node = this.element; 1.14206 + } else { 1.14207 + node = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element; 1.14208 + offset = node.getAttribute(DATA_ATTR_OFFSET); 1.14209 + position = node.getAttribute(DATA_ATTR_NODE); 1.14210 + node.removeAttribute(DATA_ATTR_OFFSET); 1.14211 + node.removeAttribute(DATA_ATTR_NODE); 1.14212 + } 1.14213 + 1.14214 + if (position !== null) { 1.14215 + node = this.getChildNodeByIndex(node, +position); 1.14216 + } 1.14217 + 1.14218 + this.composer.selection.set(node, offset); 1.14219 + }, 1.14220 + 1.14221 + getChildNodeIndex: function(parent, child) { 1.14222 + var i = 0, 1.14223 + childNodes = parent.childNodes, 1.14224 + length = childNodes.length; 1.14225 + for (; i<length; i++) { 1.14226 + if (childNodes[i] === child) { 1.14227 + return i; 1.14228 + } 1.14229 + } 1.14230 + }, 1.14231 + 1.14232 + getChildNodeByIndex: function(parent, index) { 1.14233 + return parent.childNodes[index]; 1.14234 + } 1.14235 + }); 1.14236 +})(wysihtml); 1.14237 + 1.14238 +/** 1.14239 + * TODO: the following methods still need unit test coverage 1.14240 + */ 1.14241 +wysihtml.views.View = Base.extend( 1.14242 + /** @scope wysihtml.views.View.prototype */ { 1.14243 + constructor: function(parent, textareaElement, config) { 1.14244 + this.parent = parent; 1.14245 + this.element = textareaElement; 1.14246 + this.config = config; 1.14247 + if (!this.config.noTextarea) { 1.14248 + this._observeViewChange(); 1.14249 + } 1.14250 + }, 1.14251 + 1.14252 + _observeViewChange: function() { 1.14253 + var that = this; 1.14254 + this.parent.on("beforeload", function() { 1.14255 + that.parent.on("change_view", function(view) { 1.14256 + if (view === that.name) { 1.14257 + that.parent.currentView = that; 1.14258 + that.show(); 1.14259 + // Using tiny delay here to make sure that the placeholder is set before focusing 1.14260 + setTimeout(function() { that.focus(); }, 0); 1.14261 + } else { 1.14262 + that.hide(); 1.14263 + } 1.14264 + }); 1.14265 + }); 1.14266 + }, 1.14267 + 1.14268 + focus: function() { 1.14269 + if (this.element && this.element.ownerDocument && this.element.ownerDocument.querySelector(":focus") === this.element) { 1.14270 + return; 1.14271 + } 1.14272 + 1.14273 + try { if(this.element) { this.element.focus(); } } catch(e) {} 1.14274 + }, 1.14275 + 1.14276 + hide: function() { 1.14277 + this.element.style.display = "none"; 1.14278 + }, 1.14279 + 1.14280 + show: function() { 1.14281 + this.element.style.display = ""; 1.14282 + }, 1.14283 + 1.14284 + disable: function() { 1.14285 + this.element.setAttribute("disabled", "disabled"); 1.14286 + }, 1.14287 + 1.14288 + enable: function() { 1.14289 + this.element.removeAttribute("disabled"); 1.14290 + } 1.14291 +}); 1.14292 + 1.14293 +(function(wysihtml) { 1.14294 + var dom = wysihtml.dom, 1.14295 + browser = wysihtml.browser; 1.14296 + 1.14297 + wysihtml.views.Composer = wysihtml.views.View.extend( 1.14298 + /** @scope wysihtml.views.Composer.prototype */ { 1.14299 + name: "composer", 1.14300 + 1.14301 + constructor: function(parent, editableElement, config) { 1.14302 + this.base(parent, editableElement, config); 1.14303 + if (!this.config.noTextarea) { 1.14304 + this.textarea = this.parent.textarea; 1.14305 + } else { 1.14306 + this.editableArea = editableElement; 1.14307 + } 1.14308 + if (this.config.contentEditableMode) { 1.14309 + this._initContentEditableArea(); 1.14310 + } else { 1.14311 + this._initSandbox(); 1.14312 + } 1.14313 + }, 1.14314 + 1.14315 + clear: function() { 1.14316 + this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : "<br>"; 1.14317 + }, 1.14318 + 1.14319 + getValue: function(parse, clearInternals) { 1.14320 + var value = this.isEmpty() ? "" : wysihtml.quirks.getCorrectInnerHTML(this.element); 1.14321 + if (parse !== false) { 1.14322 + value = this.parent.parse(value, (clearInternals === false) ? false : true); 1.14323 + } 1.14324 + return value; 1.14325 + }, 1.14326 + 1.14327 + setValue: function(html, parse) { 1.14328 + if (parse !== false) { 1.14329 + html = this.parent.parse(html); 1.14330 + } 1.14331 + 1.14332 + try { 1.14333 + this.element.innerHTML = html; 1.14334 + } catch (e) { 1.14335 + this.element.innerText = html; 1.14336 + } 1.14337 + }, 1.14338 + 1.14339 + cleanUp: function(rules) { 1.14340 + var bookmark; 1.14341 + if (this.selection && this.selection.isInThisEditable()) { 1.14342 + bookmark = rangy.saveSelection(this.win); 1.14343 + } 1.14344 + this.parent.parse(this.element, undefined, rules); 1.14345 + if (bookmark) { 1.14346 + rangy.restoreSelection(bookmark); 1.14347 + } 1.14348 + }, 1.14349 + 1.14350 + show: function() { 1.14351 + this.editableArea.style.display = this._displayStyle || ""; 1.14352 + 1.14353 + if (!this.config.noTextarea && !this.textarea.element.disabled) { 1.14354 + // Firefox needs this, otherwise contentEditable becomes uneditable 1.14355 + this.disable(); 1.14356 + this.enable(); 1.14357 + } 1.14358 + }, 1.14359 + 1.14360 + hide: function() { 1.14361 + this._displayStyle = dom.getStyle("display").from(this.editableArea); 1.14362 + if (this._displayStyle === "none") { 1.14363 + this._displayStyle = null; 1.14364 + } 1.14365 + this.editableArea.style.display = "none"; 1.14366 + }, 1.14367 + 1.14368 + disable: function() { 1.14369 + this.parent.fire("disable:composer"); 1.14370 + this.element.removeAttribute("contentEditable"); 1.14371 + }, 1.14372 + 1.14373 + enable: function() { 1.14374 + this.parent.fire("enable:composer"); 1.14375 + this.element.setAttribute("contentEditable", "true"); 1.14376 + }, 1.14377 + 1.14378 + focus: function(setToEnd) { 1.14379 + // IE 8 fires the focus event after .focus() 1.14380 + // This is needed by our simulate_placeholder.js to work 1.14381 + // therefore we clear it ourselves this time 1.14382 + if (wysihtml.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { 1.14383 + this.clear(); 1.14384 + } 1.14385 + 1.14386 + this.base(); 1.14387 + 1.14388 + var lastChild = this.element.lastChild; 1.14389 + if (setToEnd && lastChild && this.selection) { 1.14390 + if (lastChild.nodeName === "BR") { 1.14391 + this.selection.setBefore(this.element.lastChild); 1.14392 + } else { 1.14393 + this.selection.setAfter(this.element.lastChild); 1.14394 + } 1.14395 + } 1.14396 + }, 1.14397 + 1.14398 + getScrollPos: function() { 1.14399 + if (this.doc && this.win) { 1.14400 + var pos = {}; 1.14401 + 1.14402 + if (typeof this.win.pageYOffset !== "undefined") { 1.14403 + pos.y = this.win.pageYOffset; 1.14404 + } else { 1.14405 + pos.y = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollTop; 1.14406 + } 1.14407 + 1.14408 + if (typeof this.win.pageXOffset !== "undefined") { 1.14409 + pos.x = this.win.pageXOffset; 1.14410 + } else { 1.14411 + pos.x = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollLeft; 1.14412 + } 1.14413 + 1.14414 + return pos; 1.14415 + } 1.14416 + }, 1.14417 + 1.14418 + setScrollPos: function(pos) { 1.14419 + if (pos && typeof pos.x !== "undefined" && typeof pos.y !== "undefined") { 1.14420 + this.win.scrollTo(pos.x, pos.y); 1.14421 + } 1.14422 + }, 1.14423 + 1.14424 + getTextContent: function() { 1.14425 + return dom.getTextContent(this.element); 1.14426 + }, 1.14427 + 1.14428 + hasPlaceholderSet: function() { 1.14429 + return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet; 1.14430 + }, 1.14431 + 1.14432 + isEmpty: function() { 1.14433 + var innerHTML = this.element.innerHTML.toLowerCase(); 1.14434 + return (/^(\s|<br>|<\/br>|<p>|<\/p>)*$/i).test(innerHTML) || 1.14435 + innerHTML === "" || 1.14436 + innerHTML === "<br>" || 1.14437 + innerHTML === "<p></p>" || 1.14438 + innerHTML === "<p><br></p>" || 1.14439 + this.hasPlaceholderSet(); 1.14440 + }, 1.14441 + 1.14442 + _initContentEditableArea: function() { 1.14443 + var that = this; 1.14444 + if (this.config.noTextarea) { 1.14445 + this.sandbox = new dom.ContentEditableArea(function() { 1.14446 + that._create(); 1.14447 + }, { 1.14448 + className: this.config.classNames.sandbox 1.14449 + }, this.editableArea); 1.14450 + } else { 1.14451 + this.sandbox = new dom.ContentEditableArea(function() { 1.14452 + that._create(); 1.14453 + }, { 1.14454 + className: this.config.classNames.sandbox 1.14455 + }); 1.14456 + this.editableArea = this.sandbox.getContentEditable(); 1.14457 + dom.insert(this.editableArea).after(this.textarea.element); 1.14458 + this._createWysiwygFormField(); 1.14459 + } 1.14460 + }, 1.14461 + 1.14462 + _initSandbox: function() { 1.14463 + var that = this; 1.14464 + this.sandbox = new dom.Sandbox(function() { 1.14465 + that._create(); 1.14466 + }, { 1.14467 + stylesheets: this.config.stylesheets, 1.14468 + className: this.config.classNames.sandbox 1.14469 + }); 1.14470 + this.editableArea = this.sandbox.getIframe(); 1.14471 + 1.14472 + var textareaElement = this.textarea.element; 1.14473 + dom.insert(this.editableArea).after(textareaElement); 1.14474 + 1.14475 + this._createWysiwygFormField(); 1.14476 + }, 1.14477 + 1.14478 + // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor 1.14479 + _createWysiwygFormField: function() { 1.14480 + if (this.textarea.element.form) { 1.14481 + var hiddenField = document.createElement("input"); 1.14482 + hiddenField.type = "hidden"; 1.14483 + hiddenField.name = "_wysihtml_mode"; 1.14484 + hiddenField.value = 1; 1.14485 + dom.insert(hiddenField).after(this.textarea.element); 1.14486 + } 1.14487 + }, 1.14488 + 1.14489 + _create: function() { 1.14490 + var that = this; 1.14491 + this.doc = this.sandbox.getDocument(); 1.14492 + this.win = this.sandbox.getWindow(); 1.14493 + this.element = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body; 1.14494 + if (!this.config.noTextarea) { 1.14495 + this.textarea = this.parent.textarea; 1.14496 + this.element.innerHTML = this.textarea.getValue(true, false); 1.14497 + } else { 1.14498 + this.cleanUp(); // cleans contenteditable on initiation as it may contain html 1.14499 + } 1.14500 + 1.14501 + // Make sure our selection handler is ready 1.14502 + this.selection = new wysihtml.Selection(this.parent, this.element, this.config.classNames.uneditableContainer); 1.14503 + 1.14504 + // Make sure commands dispatcher is ready 1.14505 + this.commands = new wysihtml.Commands(this.parent); 1.14506 + 1.14507 + if (!this.config.noTextarea) { 1.14508 + dom.copyAttributes([ 1.14509 + "className", "spellcheck", "title", "lang", "dir", "accessKey" 1.14510 + ]).from(this.textarea.element).to(this.element); 1.14511 + } 1.14512 + 1.14513 + this._initAutoLinking(); 1.14514 + 1.14515 + dom.addClass(this.element, this.config.classNames.composer); 1.14516 + // 1.14517 + // Make the editor look like the original textarea, by syncing styles 1.14518 + if (this.config.style && !this.config.contentEditableMode) { 1.14519 + this.style(); 1.14520 + } 1.14521 + 1.14522 + this.observe(); 1.14523 + 1.14524 + var name = this.config.name; 1.14525 + if (name) { 1.14526 + dom.addClass(this.element, name); 1.14527 + if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); } 1.14528 + } 1.14529 + 1.14530 + this.enable(); 1.14531 + 1.14532 + if (!this.config.noTextarea && this.textarea.element.disabled) { 1.14533 + this.disable(); 1.14534 + } 1.14535 + 1.14536 + // Simulate html5 placeholder attribute on contentEditable element 1.14537 + var placeholderText = typeof(this.config.placeholder) === "string" 1.14538 + ? this.config.placeholder 1.14539 + : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")); 1.14540 + if (placeholderText) { 1.14541 + dom.simulatePlaceholder(this.parent, this, placeholderText, this.config.classNames.placeholder); 1.14542 + } 1.14543 + 1.14544 + // Make sure that the browser avoids using inline styles whenever possible 1.14545 + this.commands.exec("styleWithCSS", false); 1.14546 + 1.14547 + this._initObjectResizing(); 1.14548 + this._initUndoManager(); 1.14549 + this._initLineBreaking(); 1.14550 + 1.14551 + // Simulate html5 autofocus on contentEditable element 1.14552 + // This doesn't work on IOS (5.1.1) 1.14553 + if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) { 1.14554 + setTimeout(function() { that.focus(true); }, 100); 1.14555 + } 1.14556 + 1.14557 + // IE sometimes leaves a single paragraph, which can't be removed by the user 1.14558 + if (!browser.clearsContentEditableCorrectly()) { 1.14559 + wysihtml.quirks.ensureProperClearing(this); 1.14560 + } 1.14561 + 1.14562 + // Set up a sync that makes sure that textarea and editor have the same content 1.14563 + if (this.initSync && this.config.sync) { 1.14564 + this.initSync(); 1.14565 + } 1.14566 + 1.14567 + // Okay hide the textarea, we are ready to go 1.14568 + if (!this.config.noTextarea) { this.textarea.hide(); } 1.14569 + 1.14570 + // Fire global (before-)load event 1.14571 + this.parent.fire("beforeload").fire("load"); 1.14572 + }, 1.14573 + 1.14574 + _initAutoLinking: function() { 1.14575 + var that = this, 1.14576 + supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(), 1.14577 + supportsAutoLinking = browser.doesAutoLinkingInContentEditable(); 1.14578 + 1.14579 + if (supportsDisablingOfAutoLinking) { 1.14580 + this.commands.exec("AutoUrlDetect", false, false); 1.14581 + } 1.14582 + 1.14583 + if (!this.config.autoLink) { 1.14584 + return; 1.14585 + } 1.14586 + 1.14587 + // Only do the auto linking by ourselves when the browser doesn't support auto linking 1.14588 + // OR when he supports auto linking but we were able to turn it off (IE9+) 1.14589 + if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { 1.14590 + this.parent.on("newword:composer", function() { 1.14591 + if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) { 1.14592 + var nodeWithSelection = that.selection.getSelectedNode(), 1.14593 + uneditables = that.element.querySelectorAll("." + that.config.classNames.uneditableContainer), 1.14594 + isInUneditable = false; 1.14595 + 1.14596 + for (var i = uneditables.length; i--;) { 1.14597 + if (wysihtml.dom.contains(uneditables[i], nodeWithSelection)) { 1.14598 + isInUneditable = true; 1.14599 + } 1.14600 + } 1.14601 + 1.14602 + if (!isInUneditable) dom.autoLink(nodeWithSelection, [that.config.classNames.uneditableContainer]); 1.14603 + } 1.14604 + }); 1.14605 + 1.14606 + dom.observe(this.element, "blur", function() { 1.14607 + dom.autoLink(that.element, [that.config.classNames.uneditableContainer]); 1.14608 + }); 1.14609 + } 1.14610 + 1.14611 + // Assuming we have the following: 1.14612 + // <a href="http://www.google.de">http://www.google.de</a> 1.14613 + // If a user now changes the url in the innerHTML we want to make sure that 1.14614 + // it's synchronized with the href attribute (as long as the innerHTML is still a url) 1.14615 + var // Use a live NodeList to check whether there are any links in the document 1.14616 + links = this.sandbox.getDocument().getElementsByTagName("a"), 1.14617 + // The autoLink helper method reveals a reg exp to detect correct urls 1.14618 + urlRegExp = dom.autoLink.URL_REG_EXP, 1.14619 + getTextContent = function(element) { 1.14620 + var textContent = wysihtml.lang.string(dom.getTextContent(element)).trim(); 1.14621 + if (textContent.substr(0, 4) === "www.") { 1.14622 + textContent = "http://" + textContent; 1.14623 + } 1.14624 + return textContent; 1.14625 + }; 1.14626 + 1.14627 + dom.observe(this.element, "keydown", function(event) { 1.14628 + if (!links.length) { 1.14629 + return; 1.14630 + } 1.14631 + 1.14632 + var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), 1.14633 + link = dom.getParentElement(selectedNode, { query: "a" }, 4), 1.14634 + textContent; 1.14635 + 1.14636 + if (!link) { 1.14637 + return; 1.14638 + } 1.14639 + 1.14640 + textContent = getTextContent(link); 1.14641 + // keydown is fired before the actual content is changed 1.14642 + // therefore we set a timeout to change the href 1.14643 + setTimeout(function() { 1.14644 + var newTextContent = getTextContent(link); 1.14645 + if (newTextContent === textContent) { 1.14646 + return; 1.14647 + } 1.14648 + 1.14649 + // Only set href when new href looks like a valid url 1.14650 + if (newTextContent.match(urlRegExp)) { 1.14651 + link.setAttribute("href", newTextContent); 1.14652 + } 1.14653 + }, 0); 1.14654 + }); 1.14655 + }, 1.14656 + 1.14657 + _initObjectResizing: function() { 1.14658 + this.commands.exec("enableObjectResizing", true); 1.14659 + 1.14660 + // IE sets inline styles after resizing objects 1.14661 + // The following lines make sure that the width/height css properties 1.14662 + // are copied over to the width/height attributes 1.14663 + if (browser.supportsEvent("resizeend")) { 1.14664 + var properties = ["width", "height"], 1.14665 + propertiesLength = properties.length, 1.14666 + element = this.element; 1.14667 + 1.14668 + dom.observe(element, "resizeend", function(event) { 1.14669 + var target = event.target || event.srcElement, 1.14670 + style = target.style, 1.14671 + i = 0, 1.14672 + property; 1.14673 + 1.14674 + if (target.nodeName !== "IMG") { 1.14675 + return; 1.14676 + } 1.14677 + 1.14678 + for (; i<propertiesLength; i++) { 1.14679 + property = properties[i]; 1.14680 + if (style[property]) { 1.14681 + target.setAttribute(property, parseInt(style[property], 10)); 1.14682 + style[property] = ""; 1.14683 + } 1.14684 + } 1.14685 + 1.14686 + // After resizing IE sometimes forgets to remove the old resize handles 1.14687 + wysihtml.quirks.redraw(element); 1.14688 + }); 1.14689 + } 1.14690 + }, 1.14691 + 1.14692 + _initUndoManager: function() { 1.14693 + this.undoManager = new wysihtml.UndoManager(this.parent); 1.14694 + }, 1.14695 + 1.14696 + _initLineBreaking: function() { 1.14697 + var that = this, 1.14698 + USE_NATIVE_LINE_BREAK_INSIDE_TAGS = "li, p, h1, h2, h3, h4, h5, h6", 1.14699 + LIST_TAGS = "ul, ol, menu"; 1.14700 + 1.14701 + function adjust(selectedNode) { 1.14702 + var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2); 1.14703 + if (parentElement && dom.contains(that.element, parentElement)) { 1.14704 + that.selection.executeAndRestoreRangy(function() { 1.14705 + if (that.config.useLineBreaks) { 1.14706 + if (!parentElement.firstChild || (parentElement.firstChild === parentElement.lastChild && parentElement.firstChild.nodeType === 1 && parentElement.firstChild.classList.contains('rangySelectionBoundary'))) { 1.14707 + parentElement.appendChild(that.doc.createElement('br')); 1.14708 + } 1.14709 + dom.replaceWithChildNodes(parentElement); 1.14710 + } else if (parentElement.nodeName !== "P") { 1.14711 + dom.renameElement(parentElement, "p"); 1.14712 + } 1.14713 + }); 1.14714 + } 1.14715 + } 1.14716 + 1.14717 + // Ensures when editor is empty and not line breaks mode, the inital state has a paragraph in it on focus with caret inside paragraph 1.14718 + if (!this.config.useLineBreaks) { 1.14719 + dom.observe(this.element, ["focus"], function() { 1.14720 + if (that.isEmpty()) { 1.14721 + setTimeout(function() { 1.14722 + var paragraph = that.doc.createElement("P"); 1.14723 + that.element.innerHTML = ""; 1.14724 + that.element.appendChild(paragraph); 1.14725 + if (!browser.displaysCaretInEmptyContentEditableCorrectly()) { 1.14726 + paragraph.innerHTML = "<br>"; 1.14727 + that.selection.setBefore(paragraph.firstChild); 1.14728 + } else { 1.14729 + that.selection.selectNode(paragraph, true); 1.14730 + } 1.14731 + }, 0); 1.14732 + } 1.14733 + }); 1.14734 + } 1.14735 + 1.14736 + dom.observe(this.element, "keydown", function(event) { 1.14737 + var keyCode = event.keyCode; 1.14738 + 1.14739 + if (event.shiftKey || event.ctrlKey || event.defaultPrevented) { 1.14740 + return; 1.14741 + } 1.14742 + 1.14743 + if (keyCode !== wysihtml.ENTER_KEY && keyCode !== wysihtml.BACKSPACE_KEY) { 1.14744 + return; 1.14745 + } 1.14746 + var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { query: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4); 1.14747 + if (blockElement) { 1.14748 + setTimeout(function() { 1.14749 + // Unwrap paragraph after leaving a list or a H1-6 1.14750 + var selectedNode = that.selection.getSelectedNode(), 1.14751 + list; 1.14752 + 1.14753 + if (blockElement.nodeName === "LI") { 1.14754 + if (!selectedNode) { 1.14755 + return; 1.14756 + } 1.14757 + 1.14758 + list = dom.getParentElement(selectedNode, { query: LIST_TAGS }, 2); 1.14759 + 1.14760 + if (!list) { 1.14761 + adjust(selectedNode); 1.14762 + } 1.14763 + } 1.14764 + 1.14765 + if (keyCode === wysihtml.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) { 1.14766 + adjust(selectedNode); 1.14767 + } 1.14768 + }, 0); 1.14769 + return; 1.14770 + } 1.14771 + if (that.config.useLineBreaks && keyCode === wysihtml.ENTER_KEY && !wysihtml.browser.insertsLineBreaksOnReturn()) { 1.14772 + event.preventDefault(); 1.14773 + that.commands.exec("insertLineBreak"); 1.14774 + } 1.14775 + }); 1.14776 + } 1.14777 + }); 1.14778 +})(wysihtml); 1.14779 + 1.14780 +(function(wysihtml) { 1.14781 + var dom = wysihtml.dom, 1.14782 + doc = document, 1.14783 + win = window, 1.14784 + HOST_TEMPLATE = doc.createElement("div"), 1.14785 + /** 1.14786 + * Styles to copy from textarea to the composer element 1.14787 + */ 1.14788 + TEXT_FORMATTING = [ 1.14789 + "background-color", 1.14790 + "color", "cursor", 1.14791 + "font-family", "font-size", "font-style", "font-variant", "font-weight", 1.14792 + "line-height", "letter-spacing", 1.14793 + "text-align", "text-decoration", "text-indent", "text-rendering", 1.14794 + "word-break", "word-wrap", "word-spacing" 1.14795 + ], 1.14796 + /** 1.14797 + * Styles to copy from textarea to the iframe 1.14798 + */ 1.14799 + BOX_FORMATTING = [ 1.14800 + "background-color", 1.14801 + "border-collapse", 1.14802 + "border-bottom-color", "border-bottom-style", "border-bottom-width", 1.14803 + "border-left-color", "border-left-style", "border-left-width", 1.14804 + "border-right-color", "border-right-style", "border-right-width", 1.14805 + "border-top-color", "border-top-style", "border-top-width", 1.14806 + "clear", "display", "float", 1.14807 + "margin-bottom", "margin-left", "margin-right", "margin-top", 1.14808 + "outline-color", "outline-offset", "outline-width", "outline-style", 1.14809 + "padding-left", "padding-right", "padding-top", "padding-bottom", 1.14810 + "position", "top", "left", "right", "bottom", "z-index", 1.14811 + "vertical-align", "text-align", 1.14812 + "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing", 1.14813 + "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow", 1.14814 + "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius", 1.14815 + "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius", 1.14816 + "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius", 1.14817 + "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius", 1.14818 + "width", "height" 1.14819 + ], 1.14820 + ADDITIONAL_CSS_RULES = [ 1.14821 + "html { height: 100%; }", 1.14822 + "body { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }", 1.14823 + "body > p:first-child { margin-top: 0; }", 1.14824 + "._wysihtml-temp { display: none; }", 1.14825 + wysihtml.browser.isGecko ? 1.14826 + "body.placeholder { color: graytext !important; }" : 1.14827 + "body.placeholder { color: #a9a9a9 !important; }", 1.14828 + // Ensure that user see's broken images and can delete them 1.14829 + "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" 1.14830 + ]; 1.14831 + 1.14832 + /** 1.14833 + * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: 1.14834 + * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx 1.14835 + * 1.14836 + * Other browsers need a more hacky way: (pssst don't tell my mama) 1.14837 + * In order to prevent the element being scrolled into view when focusing it, we simply 1.14838 + * move it out of the scrollable area, focus it, and reset it's position 1.14839 + */ 1.14840 + var focusWithoutScrolling = function(element) { 1.14841 + if (element.setActive) { 1.14842 + // Following line could cause a js error when the textarea is invisible 1.14843 + // See https://github.com/xing/wysihtml5/issues/9 1.14844 + try { element.setActive(); } catch(e) {} 1.14845 + } else { 1.14846 + var elementStyle = element.style, 1.14847 + originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, 1.14848 + originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, 1.14849 + originalStyles = { 1.14850 + position: elementStyle.position, 1.14851 + top: elementStyle.top, 1.14852 + left: elementStyle.left, 1.14853 + WebkitUserSelect: elementStyle.WebkitUserSelect 1.14854 + }; 1.14855 + 1.14856 + dom.setStyles({ 1.14857 + position: "absolute", 1.14858 + top: "-99999px", 1.14859 + left: "-99999px", 1.14860 + // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother 1.14861 + WebkitUserSelect: "none" 1.14862 + }).on(element); 1.14863 + 1.14864 + element.focus(); 1.14865 + 1.14866 + dom.setStyles(originalStyles).on(element); 1.14867 + 1.14868 + if (win.scrollTo) { 1.14869 + // Some browser extensions unset this method to prevent annoyances 1.14870 + // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 1.14871 + // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 1.14872 + win.scrollTo(originalScrollLeft, originalScrollTop); 1.14873 + } 1.14874 + } 1.14875 + }; 1.14876 + 1.14877 + 1.14878 + wysihtml.views.Composer.prototype.style = function() { 1.14879 + var that = this, 1.14880 + originalActiveElement = doc.querySelector(":focus"), 1.14881 + textareaElement = this.textarea.element, 1.14882 + hasPlaceholder = textareaElement.hasAttribute("placeholder"), 1.14883 + originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"), 1.14884 + originalDisplayValue = textareaElement.style.display, 1.14885 + originalDisabled = textareaElement.disabled, 1.14886 + displayValueForCopying; 1.14887 + 1.14888 + this.focusStylesHost = HOST_TEMPLATE.cloneNode(false); 1.14889 + this.blurStylesHost = HOST_TEMPLATE.cloneNode(false); 1.14890 + this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false); 1.14891 + 1.14892 + // Remove placeholder before copying (as the placeholder has an affect on the computed style) 1.14893 + if (hasPlaceholder) { 1.14894 + textareaElement.removeAttribute("placeholder"); 1.14895 + } 1.14896 + 1.14897 + if (textareaElement === originalActiveElement) { 1.14898 + textareaElement.blur(); 1.14899 + } 1.14900 + 1.14901 + // enable for copying styles 1.14902 + textareaElement.disabled = false; 1.14903 + 1.14904 + // set textarea to display="none" to get cascaded styles via getComputedStyle 1.14905 + textareaElement.style.display = displayValueForCopying = "none"; 1.14906 + 1.14907 + if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") || 1.14908 + (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) { 1.14909 + textareaElement.style.display = displayValueForCopying = originalDisplayValue; 1.14910 + } 1.14911 + 1.14912 + // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- 1.14913 + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost); 1.14914 + 1.14915 + // --------- editor styles --------- 1.14916 + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); 1.14917 + 1.14918 + // --------- apply standard rules --------- 1.14919 + dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); 1.14920 + 1.14921 + // --------- :disabled styles --------- 1.14922 + textareaElement.disabled = true; 1.14923 + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost); 1.14924 + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost); 1.14925 + textareaElement.disabled = originalDisabled; 1.14926 + 1.14927 + // --------- :focus styles --------- 1.14928 + textareaElement.style.display = originalDisplayValue; 1.14929 + focusWithoutScrolling(textareaElement); 1.14930 + textareaElement.style.display = displayValueForCopying; 1.14931 + 1.14932 + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); 1.14933 + dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); 1.14934 + 1.14935 + // reset textarea 1.14936 + textareaElement.style.display = originalDisplayValue; 1.14937 + 1.14938 + dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea); 1.14939 + 1.14940 + // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus 1.14941 + // this is needed for when the change_view event is fired where the iframe is hidden and then 1.14942 + // the blur event fires and re-displays it 1.14943 + var boxFormattingStyles = wysihtml.lang.array(BOX_FORMATTING).without(["display"]); 1.14944 + 1.14945 + // --------- restore focus --------- 1.14946 + if (originalActiveElement) { 1.14947 + focusWithoutScrolling(originalActiveElement); 1.14948 + } else { 1.14949 + textareaElement.blur(); 1.14950 + } 1.14951 + 1.14952 + // --------- restore placeholder --------- 1.14953 + if (hasPlaceholder) { 1.14954 + textareaElement.setAttribute("placeholder", originalPlaceholder); 1.14955 + } 1.14956 + 1.14957 + // --------- Sync focus/blur styles --------- 1.14958 + this.parent.on("focus:composer", function() { 1.14959 + dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea); 1.14960 + dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); 1.14961 + }); 1.14962 + 1.14963 + this.parent.on("blur:composer", function() { 1.14964 + dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea); 1.14965 + dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); 1.14966 + }); 1.14967 + 1.14968 + this.parent.observe("disable:composer", function() { 1.14969 + dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea); 1.14970 + dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element); 1.14971 + }); 1.14972 + 1.14973 + this.parent.observe("enable:composer", function() { 1.14974 + dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea); 1.14975 + dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); 1.14976 + }); 1.14977 + 1.14978 + return this; 1.14979 + }; 1.14980 +})(wysihtml); 1.14981 + 1.14982 +/** 1.14983 + * Taking care of events 1.14984 + * - Simulating 'change' event on contentEditable element 1.14985 + * - Handling drag & drop logic 1.14986 + * - Catch paste events 1.14987 + * - Dispatch proprietary newword:composer event 1.14988 + * - Keyboard shortcuts 1.14989 + */ 1.14990 +(function(wysihtml) { 1.14991 + var dom = wysihtml.dom, 1.14992 + domNode = dom.domNode, 1.14993 + browser = wysihtml.browser, 1.14994 + /** 1.14995 + * Map keyCodes to query commands 1.14996 + */ 1.14997 + shortcuts = { 1.14998 + "66": "bold", // B 1.14999 + "73": "italic", // I 1.15000 + "85": "underline" // U 1.15001 + }; 1.15002 + 1.15003 + var actions = { 1.15004 + 1.15005 + // Adds multiple eventlisteners to target, bound to one callback 1.15006 + // TODO: If needed elsewhere make it part of wysihtml.dom or sth 1.15007 + addListeners: function (target, events, callback) { 1.15008 + for(var i = 0, max = events.length; i < max; i++) { 1.15009 + target.addEventListener(events[i], callback, false); 1.15010 + } 1.15011 + }, 1.15012 + 1.15013 + // Removes multiple eventlisteners from target, bound to one callback 1.15014 + // TODO: If needed elsewhere make it part of wysihtml.dom or sth 1.15015 + removeListeners: function (target, events, callback) { 1.15016 + for(var i = 0, max = events.length; i < max; i++) { 1.15017 + target.removeEventListener(events[i], callback, false); 1.15018 + } 1.15019 + }, 1.15020 + 1.15021 + // Override for giving user ability to delete last line break in table cell 1.15022 + fixLastBrDeletionInTable: function(composer, force) { 1.15023 + if (composer.selection.caretIsInTheEndOfNode()) { 1.15024 + var sel = composer.selection.getSelection(), 1.15025 + aNode = sel.anchorNode; 1.15026 + if (aNode && aNode.nodeType === 1 && (wysihtml.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) { 1.15027 + var nextNode = aNode.childNodes[sel.anchorOffset]; 1.15028 + if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") { 1.15029 + nextNode.parentNode.removeChild(nextNode); 1.15030 + return true; 1.15031 + } 1.15032 + } 1.15033 + } 1.15034 + return false; 1.15035 + }, 1.15036 + 1.15037 + // If found an uneditable before caret then notify it before deletion 1.15038 + handleUneditableDeletion: function(composer) { 1.15039 + var before = composer.selection.getBeforeSelection(true); 1.15040 + if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) { 1.15041 + if (actions.fixLastBrDeletionInTable(composer, true)) { 1.15042 + return true; 1.15043 + } 1.15044 + try { 1.15045 + var ev = new CustomEvent("wysihtml:uneditable:delete", {bubbles: true, cancelable: false}); 1.15046 + before.node.dispatchEvent(ev); 1.15047 + } catch (err) {} 1.15048 + before.node.parentNode.removeChild(before.node); 1.15049 + return true; 1.15050 + } 1.15051 + return false; 1.15052 + }, 1.15053 + 1.15054 + // Deletion with caret in the beginning of headings and other block elvel elements needs special attention 1.15055 + // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit) 1.15056 + fixDeleteInTheBeginningOfBlock: function(composer) { 1.15057 + var selection = composer.selection, 1.15058 + prevNode = selection.getPreviousNode(); 1.15059 + 1.15060 + if (selection.caretIsFirstInSelection(wysihtml.browser.usesControlRanges()) && prevNode) { 1.15061 + if (prevNode.nodeType === 1 && 1.15062 + wysihtml.dom.domNode(prevNode).is.block() && 1.15063 + !domNode(prevNode).test({ 1.15064 + query: "ol, ul, table, tr, dl" 1.15065 + }) 1.15066 + ) { 1.15067 + if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) { 1.15068 + // If heading is empty remove the heading node 1.15069 + prevNode.parentNode.removeChild(prevNode); 1.15070 + return true; 1.15071 + } else { 1.15072 + if (prevNode.lastChild) { 1.15073 + var selNode = prevNode.lastChild, 1.15074 + selectedNode = selection.getSelectedNode(), 1.15075 + commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element), 1.15076 + curNode = wysihtml.dom.getParentElement(selectedNode, { 1.15077 + query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote" 1.15078 + }, false, commonAncestorNode || composer.element); 1.15079 + 1.15080 + if (curNode) { 1.15081 + domNode(curNode).transferContentTo(prevNode, true); 1.15082 + selection.setAfter(selNode); 1.15083 + return true; 1.15084 + } else if (wysihtml.browser.usesControlRanges()) { 1.15085 + selectedNode = selection.getCaretNode(); 1.15086 + domNode(selectedNode).transferContentTo(prevNode, true); 1.15087 + selection.setAfter(selNode); 1.15088 + return true; 1.15089 + } 1.15090 + } 1.15091 + } 1.15092 + } 1.15093 + } 1.15094 + return false; 1.15095 + }, 1.15096 + 1.15097 + /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */ 1.15098 + /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */ 1.15099 + fixDeleteInTheBeginningOfLi: function(composer) { 1.15100 + if (wysihtml.browser.hasLiDeletingProblem()) { 1.15101 + var selection = composer.selection.getSelection(), 1.15102 + aNode = selection.anchorNode, 1.15103 + listNode, prevNode, firstNode, 1.15104 + isInBeginnig = composer.selection.caretIsFirstInSelection(), 1.15105 + prevNode, 1.15106 + intermediaryNode; 1.15107 + 1.15108 + // Fix caret at the beginnig of first textNode in LI 1.15109 + if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) { 1.15110 + aNode = aNode.parentNode; 1.15111 + isInBeginnig = true; 1.15112 + } 1.15113 + 1.15114 + if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") { 1.15115 + prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.15116 + if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) { 1.15117 + prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}); 1.15118 + intermediaryNode = aNode.parentNode; 1.15119 + } 1.15120 + if (prevNode) { 1.15121 + firstNode = aNode.firstChild; 1.15122 + domNode(aNode).transferContentTo(prevNode, true); 1.15123 + 1.15124 + if (intermediaryNode && intermediaryNode.children.length === 0){ 1.15125 + intermediaryNode.remove(); 1.15126 + } 1.15127 + 1.15128 + if (firstNode) { 1.15129 + composer.selection.setBefore(firstNode); 1.15130 + } else if (prevNode) { 1.15131 + if (prevNode.nodeType === 1) { 1.15132 + if (prevNode.lastChild) { 1.15133 + composer.selection.setAfter(prevNode.lastChild); 1.15134 + } else { 1.15135 + composer.selection.selectNode(prevNode); 1.15136 + } 1.15137 + } else { 1.15138 + composer.selection.setAfter(prevNode); 1.15139 + } 1.15140 + } 1.15141 + return true; 1.15142 + } 1.15143 + } 1.15144 + } 1.15145 + return false; 1.15146 + }, 1.15147 + 1.15148 + fixDeleteInTheBeginningOfControlSelection: function(composer) { 1.15149 + var selection = composer.selection, 1.15150 + prevNode = selection.getPreviousNode(), 1.15151 + selectedNode = selection.getSelectedNode(), 1.15152 + afterCaretNode; 1.15153 + 1.15154 + if (selection.caretIsFirstInSelection()) { 1.15155 + if (selectedNode.nodeType === 3) { 1.15156 + selectedNode = selectedNode.parentNode; 1.15157 + } 1.15158 + afterCaretNode = selectedNode.firstChild; 1.15159 + domNode(selectedNode).transferContentTo(prevNode, true); 1.15160 + if (afterCaretNode) { 1.15161 + composer.selection.setBefore(afterCaretNode); 1.15162 + } 1.15163 + return true; 1.15164 + } 1.15165 + return false; 1.15166 + }, 1.15167 + 1.15168 + // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature) 1.15169 + // Returns true if some corrections is applied so events know when to prevent default 1.15170 + doLineBreaksModeEnterWithCaret: function(composer) { 1.15171 + var breakNodes = "p, pre, div, blockquote", 1.15172 + caretInfo, parent, txtNode, 1.15173 + ret = false; 1.15174 + 1.15175 + caretInfo = composer.selection.getNodesNearCaret(); 1.15176 + if (caretInfo) { 1.15177 + 1.15178 + if (caretInfo.caretNode || caretInfo.nextNode) { 1.15179 + parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2); 1.15180 + if (parent === composer.element) { 1.15181 + parent = undefined; 1.15182 + } 1.15183 + } 1.15184 + 1.15185 + if (parent && caretInfo.caretNode) { 1.15186 + if (domNode(caretInfo.caretNode).is.lineBreak()) { 1.15187 + 1.15188 + if (composer.config.doubleLineBreakEscapesBlock) { 1.15189 + // Double enter (enter on blank line) exits block element in useLineBreaks mode. 1.15190 + ret = true; 1.15191 + caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode); 1.15192 + 1.15193 + // Ensure surplous line breaks are not added to preceding element 1.15194 + if (domNode(caretInfo.nextNode).is.lineBreak()) { 1.15195 + caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode); 1.15196 + } 1.15197 + 1.15198 + var brNode = composer.doc.createElement('br'); 1.15199 + if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) { 1.15200 + parent.parentNode.insertBefore(brNode, parent.nextSibling); 1.15201 + } else { 1.15202 + composer.selection.splitElementAtCaret(parent, brNode); 1.15203 + } 1.15204 + 1.15205 + // Ensure surplous blank lines are not added to preceding element 1.15206 + if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) { 1.15207 + // Replaces blank lines at the beginning of textnode 1.15208 + caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, ''); 1.15209 + } 1.15210 + composer.selection.setBefore(brNode); 1.15211 + } 1.15212 + 1.15213 + } else if (caretInfo.caretNode.nodeType === 3 && wysihtml.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) { 1.15214 + 1.15215 + // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens. 1.15216 + // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br 1.15217 + ret = true; 1.15218 + var br1 = composer.doc.createElement('br'), 1.15219 + br2 = composer.doc.createElement('br'), 1.15220 + f = composer.doc.createDocumentFragment(); 1.15221 + f.appendChild(br1); 1.15222 + f.appendChild(br2); 1.15223 + composer.selection.insertNode(f); 1.15224 + composer.selection.setBefore(br2); 1.15225 + 1.15226 + } 1.15227 + } 1.15228 + } 1.15229 + return ret; 1.15230 + } 1.15231 + }; 1.15232 + 1.15233 + var handleDeleteKeyPress = function(event, composer) { 1.15234 + var selection = composer.selection, 1.15235 + element = composer.element; 1.15236 + 1.15237 + if (selection.isCollapsed()) { 1.15238 + /** 1.15239 + * when the editor is empty in useLineBreaks = false mode, preserve 1.15240 + * the default value in it which is <p><br></p> 1.15241 + */ 1.15242 + if (composer.isEmpty() && !composer.config.useLineBreaks) { 1.15243 + event.preventDefault(); 1.15244 + return; 1.15245 + } 1.15246 + if (actions.handleUneditableDeletion(composer)) { 1.15247 + event.preventDefault(); 1.15248 + return; 1.15249 + } 1.15250 + if (actions.fixDeleteInTheBeginningOfLi(composer)) { 1.15251 + event.preventDefault(); 1.15252 + return; 1.15253 + } 1.15254 + if (actions.fixDeleteInTheBeginningOfBlock(composer)) { 1.15255 + event.preventDefault(); 1.15256 + return; 1.15257 + } 1.15258 + if (actions.fixLastBrDeletionInTable(composer)) { 1.15259 + event.preventDefault(); 1.15260 + return; 1.15261 + } 1.15262 + if (wysihtml.browser.usesControlRanges()) { 1.15263 + if (actions.fixDeleteInTheBeginningOfControlSelection(composer)) { 1.15264 + event.preventDefault(); 1.15265 + return; 1.15266 + } 1.15267 + } 1.15268 + } else { 1.15269 + if (selection.containsUneditable()) { 1.15270 + event.preventDefault(); 1.15271 + selection.deleteContents(); 1.15272 + } 1.15273 + } 1.15274 + }; 1.15275 + 1.15276 + var handleEnterKeyPress = function(event, composer) { 1.15277 + if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) { 1.15278 + // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature) 1.15279 + 1.15280 + var breakNodes = "p, pre, div, blockquote", 1.15281 + caretInfo, parent, txtNode; 1.15282 + 1.15283 + if (composer.selection.isCollapsed()) { 1.15284 + if (actions.doLineBreaksModeEnterWithCaret(composer)) { 1.15285 + event.preventDefault(); 1.15286 + } 1.15287 + } 1.15288 + } 1.15289 + 1.15290 + if (browser.hasCaretAtLinkEndInsertionProblems() && composer.selection.caretIsInTheEndOfNode()) { 1.15291 + var target = composer.selection.getSelectedNode(true), 1.15292 + targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element 1.15293 + invisibleSpace, space; 1.15294 + 1.15295 + if (targetEl && targetEl.closest('a') && target.nodeType === 3 && target === targetEl.lastChild) { 1.15296 + // Seems like enter was pressed and caret was at the end of link node 1.15297 + // This means user wants to escape the link now (caret is last in link node too). 1.15298 + composer.selection.setAfter(targetEl); 1.15299 + } 1.15300 + } 1.15301 + }; 1.15302 + 1.15303 + var handleTabKeyDown = function(composer, element, shiftKey) { 1.15304 + if (!composer.selection.isCollapsed()) { 1.15305 + composer.selection.deleteContents(); 1.15306 + } else if (composer.selection.caretIsInTheBeginnig('li')) { 1.15307 + if (shiftKey) { 1.15308 + if (composer.commands.exec('outdentList')) return; 1.15309 + } else { 1.15310 + if (composer.commands.exec('indentList')) return; 1.15311 + } 1.15312 + } 1.15313 + 1.15314 + // Is   close enough to tab. Could not find enough counter arguments for now. 1.15315 + composer.commands.exec("insertHTML", " "); 1.15316 + }; 1.15317 + 1.15318 + var handleDomNodeRemoved = function(event) { 1.15319 + if (this.domNodeRemovedInterval) { 1.15320 + clearInterval(domNodeRemovedInterval); 1.15321 + } 1.15322 + this.parent.fire("destroy:composer"); 1.15323 + }; 1.15324 + 1.15325 + // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires 1.15326 + var handleUserInteraction = function (event) { 1.15327 + this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event); 1.15328 + setTimeout((function() { 1.15329 + this.parent.fire("interaction", event).fire("interaction:composer", event); 1.15330 + }).bind(this), 0); 1.15331 + }; 1.15332 + 1.15333 + var handleFocus = function(event) { 1.15334 + this.parent.fire("focus", event).fire("focus:composer", event); 1.15335 + 1.15336 + // Delay storing of state until all focus handler are fired 1.15337 + // especially the one which resets the placeholder 1.15338 + setTimeout((function() { 1.15339 + this.focusState = this.getValue(false, false); 1.15340 + }).bind(this), 0); 1.15341 + }; 1.15342 + 1.15343 + var handleBlur = function(event) { 1.15344 + if (this.focusState !== this.getValue(false, false)) { 1.15345 + //create change event if supported (all except IE8) 1.15346 + var changeevent = event; 1.15347 + if(typeof Object.create == 'function') { 1.15348 + changeevent = Object.create(event, { type: { value: 'change' } }); 1.15349 + } 1.15350 + this.parent.fire("change", changeevent).fire("change:composer", changeevent); 1.15351 + } 1.15352 + this.parent.fire("blur", event).fire("blur:composer", event); 1.15353 + }; 1.15354 + 1.15355 + var handlePaste = function(event) { 1.15356 + this.parent.fire(event.type, event).fire(event.type + ":composer", event); 1.15357 + if (event.type === "paste") { 1.15358 + setTimeout((function() { 1.15359 + this.parent.fire("newword:composer"); 1.15360 + }).bind(this), 0); 1.15361 + } 1.15362 + }; 1.15363 + 1.15364 + var handleCopy = function(event) { 1.15365 + if (this.config.copyedFromMarking) { 1.15366 + // If supported the copied source can be based directly on selection 1.15367 + // 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. 1.15368 + if (wysihtml.browser.supportsModernPaste()) { 1.15369 + event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml()); 1.15370 + event.clipboardData.setData("text/plain", this.selection.getPlainText()); 1.15371 + event.preventDefault(); 1.15372 + } 1.15373 + this.parent.fire(event.type, event).fire(event.type + ":composer", event); 1.15374 + } 1.15375 + }; 1.15376 + 1.15377 + var handleKeyUp = function(event) { 1.15378 + var keyCode = event.keyCode; 1.15379 + if (keyCode === wysihtml.SPACE_KEY || keyCode === wysihtml.ENTER_KEY) { 1.15380 + this.parent.fire("newword:composer"); 1.15381 + } 1.15382 + }; 1.15383 + 1.15384 + var handleMouseDown = function(event) { 1.15385 + if (!browser.canSelectImagesInContentEditable()) { 1.15386 + // Make sure that images are selected when clicking on them 1.15387 + var target = event.target, 1.15388 + allImages = this.element.querySelectorAll('img'), 1.15389 + notMyImages = this.element.querySelectorAll('.' + this.config.classNames.uneditableContainer + ' img'), 1.15390 + myImages = wysihtml.lang.array(allImages).without(notMyImages); 1.15391 + 1.15392 + if (target.nodeName === "IMG" && wysihtml.lang.array(myImages).contains(target)) { 1.15393 + this.selection.selectNode(target); 1.15394 + } 1.15395 + } 1.15396 + 1.15397 + // Saves mousedown position for IE controlSelect fix 1.15398 + if (wysihtml.browser.usesControlRanges()) { 1.15399 + this.selection.lastMouseDownPos = {x: event.clientX, y: event.clientY}; 1.15400 + setTimeout(function() { 1.15401 + delete this.selection.lastMouseDownPos; 1.15402 + }.bind(this), 0); 1.15403 + } 1.15404 + }; 1.15405 + 1.15406 + // IE has this madness of control selects of overflowed and some other elements (weird box around element on selection and second click selects text) 1.15407 + // This fix handles the second click problem by adding cursor to the right position under cursor inside when controlSelection is made 1.15408 + var handleIEControlSelect = function(event) { 1.15409 + var target = event.target, 1.15410 + pos = this.selection.lastMouseDownPos; 1.15411 + if (pos) { 1.15412 + var caretPosition = document.body.createTextRange(); 1.15413 + setTimeout(function() { 1.15414 + try { 1.15415 + caretPosition.moveToPoint(pos.x, pos.y); 1.15416 + caretPosition.select(); 1.15417 + } catch (e) {} 1.15418 + }.bind(this), 0); 1.15419 + } 1.15420 + }; 1.15421 + 1.15422 + var handleClick = function(event) { 1.15423 + if (this.config.classNames.uneditableContainer) { 1.15424 + // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text) 1.15425 + // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour 1.15426 + var uneditable = wysihtml.dom.getParentElement(event.target, { query: "." + this.config.classNames.uneditableContainer }, false, this.element); 1.15427 + if (uneditable) { 1.15428 + this.selection.setAfter(uneditable); 1.15429 + } 1.15430 + } 1.15431 + }; 1.15432 + 1.15433 + var handleDrop = function(event) { 1.15434 + if (!browser.canSelectImagesInContentEditable()) { 1.15435 + // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case 1.15436 + setTimeout((function() { 1.15437 + this.selection.getSelection().removeAllRanges(); 1.15438 + }).bind(this), 0); 1.15439 + } 1.15440 + }; 1.15441 + 1.15442 + var handleKeyDown = function(event) { 1.15443 + var keyCode = event.keyCode, 1.15444 + command = shortcuts[keyCode], 1.15445 + target = this.selection.getSelectedNode(true), 1.15446 + targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element 1.15447 + parent; 1.15448 + 1.15449 + // Select all (meta/ctrl + a) 1.15450 + if ((event.ctrlKey || event.metaKey) && !event.altKey && keyCode === 65) { 1.15451 + this.selection.selectAll(); 1.15452 + event.preventDefault(); 1.15453 + return; 1.15454 + } 1.15455 + 1.15456 + // Shortcut logic 1.15457 + if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { 1.15458 + this.commands.exec(command); 1.15459 + event.preventDefault(); 1.15460 + } 1.15461 + 1.15462 + if (keyCode === wysihtml.BACKSPACE_KEY) { 1.15463 + // Delete key override for special cases 1.15464 + handleDeleteKeyPress(event, this); 1.15465 + } 1.15466 + 1.15467 + // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor 1.15468 + if (keyCode === wysihtml.BACKSPACE_KEY || keyCode === wysihtml.DELETE_KEY) { 1.15469 + if (target && target.nodeName === "IMG") { 1.15470 + event.preventDefault(); 1.15471 + parent = target.parentNode; 1.15472 + parent.removeChild(target);// delete the <img> 1.15473 + // And it's parent <a> too if it hasn't got any other child nodes 1.15474 + if (parent.nodeName === "A" && !parent.firstChild) { 1.15475 + parent.parentNode.removeChild(parent); 1.15476 + } 1.15477 + setTimeout((function() { 1.15478 + wysihtml.quirks.redraw(this.element); 1.15479 + }).bind(this), 0); 1.15480 + } 1.15481 + } 1.15482 + 1.15483 + if (this.config.handleTabKey && keyCode === wysihtml.TAB_KEY) { 1.15484 + // TAB key handling 1.15485 + event.preventDefault(); 1.15486 + handleTabKeyDown(this, this.element, event.shiftKey); 1.15487 + } 1.15488 + 1.15489 + if (keyCode === wysihtml.ENTER_KEY) { 1.15490 + handleEnterKeyPress(event, this); 1.15491 + } 1.15492 + 1.15493 + }; 1.15494 + 1.15495 + var handleKeyPress = function(event) { 1.15496 + 1.15497 + // This block should run only if some character is inserted (nor command keys like delete, backspace, enter, etc.) 1.15498 + if (event.which !== 0) { 1.15499 + 1.15500 + // Test if caret is last in a link in webkit and try to fix webkit problem, 1.15501 + // that all inserted content is added outside of link. 1.15502 + // This issue was added as a not thought through fix for getting caret after link in contenteditable if it is last in editable area. 1.15503 + // Allthough it fixes this minor case it actually introduces a cascade of problems when editing links. 1.15504 + // The standard approachi in other wysiwygs seems as a step backwards - introducing a separate modal for managing links content text. 1.15505 + // I find it to be too big of a tradeoff in terms of expected simple UI flow, thus trying to fight against it. 1.15506 + // Also adds link escaping by double space with caret at the end of link for all browsers 1.15507 + 1.15508 + if (this.selection.caretIsInTheEndOfNode()) { 1.15509 + var target = this.selection.getSelectedNode(true), 1.15510 + targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element 1.15511 + invisibleSpace, space; 1.15512 + 1.15513 + if (targetEl && targetEl.closest('a') && target === targetEl.lastChild) { 1.15514 + 1.15515 + if (event.which !== 32 || this.selection.caretIsInTheEndOfNode(true) && browser.hasCaretAtLinkEndInsertionProblems()) { 1.15516 + // Executed if there is no whitespace before caret in textnode in case of pressing space. 1.15517 + // Whitespace before marks that user wants to escape the node by pressing double space. 1.15518 + // Otherwise insert the character in the link not out as it would like to go natively 1.15519 + 1.15520 + invisibleSpace = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE); 1.15521 + this.selection.insertNode(invisibleSpace); 1.15522 + this.selection.setBefore(invisibleSpace); 1.15523 + setTimeout(function() { 1.15524 + 1.15525 + if (invisibleSpace.textContent.length > 1) { 1.15526 + invisibleSpace.textContent = invisibleSpace.textContent.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, ''); 1.15527 + this.selection.setAfter(invisibleSpace); 1.15528 + } else { 1.15529 + invisibleSpace.remove(); 1.15530 + } 1.15531 + 1.15532 + }.bind(this), 0); 1.15533 + } else if (event.which === 32) { 1.15534 + // Seems like space was pressed and there was a space before the caret allready 1.15535 + // This means user wants to escape the link now (caret is last in link node too) so we let the native browser do it-s job and escape. 1.15536 + // But lets move the trailing space too out of link if present 1.15537 + 1.15538 + if (target.nodeType === 3 && (/[\u00A0 ]$/).test(target.textContent)) { 1.15539 + 1.15540 + target.textContent = target.textContent.replace(/[\u00A0 ]$/, ''); 1.15541 + space = this.doc.createTextNode(' '); 1.15542 + targetEl.parentNode.insertBefore(space, targetEl.nextSibling); 1.15543 + this.selection.setAfter(space, false); 1.15544 + event.preventDefault(); 1.15545 + 1.15546 + } 1.15547 + } 1.15548 + } 1.15549 + } 1.15550 + } 1.15551 + } 1.15552 + 1.15553 + var handleIframeFocus = function(event) { 1.15554 + setTimeout((function() { 1.15555 + if (this.doc.querySelector(":focus") !== this.element) { 1.15556 + this.focus(); 1.15557 + } 1.15558 + }).bind(this), 0); 1.15559 + }; 1.15560 + 1.15561 + var handleIframeBlur = function(event) { 1.15562 + setTimeout((function() { 1.15563 + this.selection.getSelection().removeAllRanges(); 1.15564 + }).bind(this), 0); 1.15565 + }; 1.15566 + 1.15567 + // Testing requires actions to be accessible from out of scope 1.15568 + wysihtml.views.Composer.prototype.observeActions = actions; 1.15569 + 1.15570 + wysihtml.views.Composer.prototype.observe = function() { 1.15571 + var that = this, 1.15572 + container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(), 1.15573 + element = this.element, 1.15574 + focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow(); 1.15575 + 1.15576 + this.focusState = this.getValue(false, false); 1.15577 + this.actions = actions; 1.15578 + 1.15579 + // --------- destroy:composer event --------- 1.15580 + container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false); 1.15581 + 1.15582 + // DOMNodeRemoved event is not supported in IE 8 1.15583 + // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed 1.15584 + if (!browser.supportsMutationEvents()) { 1.15585 + this.domNodeRemovedInterval = setInterval(function() { 1.15586 + if (!dom.contains(document.documentElement, container)) { 1.15587 + handleDomNodeRemoved.call(this); 1.15588 + } 1.15589 + }, 250); 1.15590 + } 1.15591 + 1.15592 + actions.addListeners(focusBlurElement, ['drop', 'paste', 'mouseup', 'focus', 'keyup'], handleUserInteraction.bind(this)); 1.15593 + focusBlurElement.addEventListener('focus', handleFocus.bind(this), false); 1.15594 + focusBlurElement.addEventListener('blur', handleBlur.bind(this), false); 1.15595 + 1.15596 + actions.addListeners(this.element, ['drop', 'paste', 'beforepaste'], handlePaste.bind(this), false); 1.15597 + this.element.addEventListener('copy', handleCopy.bind(this), false); 1.15598 + this.element.addEventListener('mousedown', handleMouseDown.bind(this), false); 1.15599 + this.element.addEventListener('click', handleClick.bind(this), false); 1.15600 + this.element.addEventListener('drop', handleDrop.bind(this), false); 1.15601 + this.element.addEventListener('keyup', handleKeyUp.bind(this), false); 1.15602 + this.element.addEventListener('keydown', handleKeyDown.bind(this), false); 1.15603 + this.element.addEventListener('keypress', handleKeyPress.bind(this), false); 1.15604 + 1.15605 + // IE controlselect madness fix 1.15606 + if (wysihtml.browser.usesControlRanges()) { 1.15607 + this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false); 1.15608 + } 1.15609 + 1.15610 + this.element.addEventListener("dragenter", (function() { 1.15611 + this.parent.fire("unset_placeholder"); 1.15612 + }).bind(this), false); 1.15613 + 1.15614 + }; 1.15615 +})(wysihtml); 1.15616 + 1.15617 +/** 1.15618 + * Class that takes care that the value of the composer and the textarea is always in sync 1.15619 + */ 1.15620 +(function(wysihtml) { 1.15621 + var INTERVAL = 400; 1.15622 + 1.15623 + wysihtml.views.Synchronizer = Base.extend( 1.15624 + /** @scope wysihtml.views.Synchronizer.prototype */ { 1.15625 + 1.15626 + constructor: function(editor, textarea, composer) { 1.15627 + this.editor = editor; 1.15628 + this.textarea = textarea; 1.15629 + this.composer = composer; 1.15630 + 1.15631 + this._observe(); 1.15632 + }, 1.15633 + 1.15634 + /** 1.15635 + * Sync html from composer to textarea 1.15636 + * Takes care of placeholders 1.15637 + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea 1.15638 + */ 1.15639 + fromComposerToTextarea: function(shouldParseHtml) { 1.15640 + this.textarea.setValue(wysihtml.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml); 1.15641 + }, 1.15642 + 1.15643 + /** 1.15644 + * Sync value of textarea to composer 1.15645 + * Takes care of placeholders 1.15646 + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer 1.15647 + */ 1.15648 + fromTextareaToComposer: function(shouldParseHtml) { 1.15649 + var textareaValue = this.textarea.getValue(false, false); 1.15650 + if (textareaValue) { 1.15651 + this.composer.setValue(textareaValue, shouldParseHtml); 1.15652 + } else { 1.15653 + this.composer.clear(); 1.15654 + this.editor.fire("set_placeholder"); 1.15655 + } 1.15656 + }, 1.15657 + 1.15658 + /** 1.15659 + * Invoke syncing based on view state 1.15660 + * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea 1.15661 + */ 1.15662 + sync: function(shouldParseHtml) { 1.15663 + if (this.editor.currentView.name === "textarea") { 1.15664 + this.fromTextareaToComposer(shouldParseHtml); 1.15665 + } else { 1.15666 + this.fromComposerToTextarea(shouldParseHtml); 1.15667 + } 1.15668 + }, 1.15669 + 1.15670 + /** 1.15671 + * Initializes interval-based syncing 1.15672 + * also makes sure that on-submit the composer's content is synced with the textarea 1.15673 + * immediately when the form gets submitted 1.15674 + */ 1.15675 + _observe: function() { 1.15676 + var interval, 1.15677 + that = this, 1.15678 + form = this.textarea.element.form, 1.15679 + startInterval = function() { 1.15680 + interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL); 1.15681 + }, 1.15682 + stopInterval = function() { 1.15683 + clearInterval(interval); 1.15684 + interval = null; 1.15685 + }; 1.15686 + 1.15687 + startInterval(); 1.15688 + 1.15689 + if (form) { 1.15690 + // If the textarea is in a form make sure that after onreset and onsubmit the composer 1.15691 + // has the correct state 1.15692 + wysihtml.dom.observe(form, "submit", function() { 1.15693 + that.sync(true); 1.15694 + }); 1.15695 + wysihtml.dom.observe(form, "reset", function() { 1.15696 + setTimeout(function() { that.fromTextareaToComposer(); }, 0); 1.15697 + }); 1.15698 + } 1.15699 + 1.15700 + this.editor.on("change_view", function(view) { 1.15701 + if (view === "composer" && !interval) { 1.15702 + that.fromTextareaToComposer(true); 1.15703 + startInterval(); 1.15704 + } else if (view === "textarea") { 1.15705 + that.fromComposerToTextarea(true); 1.15706 + stopInterval(); 1.15707 + } 1.15708 + }); 1.15709 + 1.15710 + this.editor.on("destroy:composer", stopInterval); 1.15711 + } 1.15712 + }); 1.15713 +})(wysihtml); 1.15714 + 1.15715 +(function(wysihtml) { 1.15716 + 1.15717 + wysihtml.views.SourceView = Base.extend( 1.15718 + /** @scope wysihtml.views.SourceView.prototype */ { 1.15719 + 1.15720 + constructor: function(editor, composer) { 1.15721 + this.editor = editor; 1.15722 + this.composer = composer; 1.15723 + 1.15724 + this._observe(); 1.15725 + }, 1.15726 + 1.15727 + switchToTextarea: function(shouldParseHtml) { 1.15728 + var composerStyles = this.composer.win.getComputedStyle(this.composer.element), 1.15729 + width = parseFloat(composerStyles.width), 1.15730 + height = Math.max(parseFloat(composerStyles.height), 100); 1.15731 + 1.15732 + if (!this.textarea) { 1.15733 + this.textarea = this.composer.doc.createElement('textarea'); 1.15734 + this.textarea.className = "wysihtml-source-view"; 1.15735 + } 1.15736 + this.textarea.style.width = width + 'px'; 1.15737 + this.textarea.style.height = height + 'px'; 1.15738 + this.textarea.value = this.editor.getValue(shouldParseHtml, true); 1.15739 + this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element); 1.15740 + this.editor.currentView = "source"; 1.15741 + this.composer.element.style.display = 'none'; 1.15742 + }, 1.15743 + 1.15744 + switchToComposer: function(shouldParseHtml) { 1.15745 + var textareaValue = this.textarea.value; 1.15746 + if (textareaValue) { 1.15747 + this.composer.setValue(textareaValue, shouldParseHtml); 1.15748 + } else { 1.15749 + this.composer.clear(); 1.15750 + this.editor.fire("set_placeholder"); 1.15751 + } 1.15752 + this.textarea.parentNode.removeChild(this.textarea); 1.15753 + this.editor.currentView = this.composer; 1.15754 + this.composer.element.style.display = ''; 1.15755 + }, 1.15756 + 1.15757 + _observe: function() { 1.15758 + this.editor.on("change_view", function(view) { 1.15759 + if (view === "composer") { 1.15760 + this.switchToComposer(true); 1.15761 + } else if (view === "textarea") { 1.15762 + this.switchToTextarea(true); 1.15763 + } 1.15764 + }.bind(this)); 1.15765 + } 1.15766 + 1.15767 + }); 1.15768 + 1.15769 +})(wysihtml); 1.15770 + 1.15771 +wysihtml.views.Textarea = wysihtml.views.View.extend( 1.15772 + /** @scope wysihtml.views.Textarea.prototype */ { 1.15773 + name: "textarea", 1.15774 + 1.15775 + constructor: function(parent, textareaElement, config) { 1.15776 + this.base(parent, textareaElement, config); 1.15777 + 1.15778 + this._observe(); 1.15779 + }, 1.15780 + 1.15781 + clear: function() { 1.15782 + this.element.value = ""; 1.15783 + }, 1.15784 + 1.15785 + getValue: function(parse) { 1.15786 + var value = this.isEmpty() ? "" : this.element.value; 1.15787 + if (parse !== false) { 1.15788 + value = this.parent.parse(value); 1.15789 + } 1.15790 + return value; 1.15791 + }, 1.15792 + 1.15793 + setValue: function(html, parse) { 1.15794 + if (parse !== false) { 1.15795 + html = this.parent.parse(html); 1.15796 + } 1.15797 + this.element.value = html; 1.15798 + }, 1.15799 + 1.15800 + cleanUp: function(rules) { 1.15801 + var html = this.parent.parse(this.element.value, undefined, rules); 1.15802 + this.element.value = html; 1.15803 + }, 1.15804 + 1.15805 + hasPlaceholderSet: function() { 1.15806 + var supportsPlaceholder = wysihtml.browser.supportsPlaceholderAttributeOn(this.element), 1.15807 + placeholderText = this.element.getAttribute("placeholder") || null, 1.15808 + value = this.element.value, 1.15809 + isEmpty = !value; 1.15810 + return (supportsPlaceholder && isEmpty) || (value === placeholderText); 1.15811 + }, 1.15812 + 1.15813 + isEmpty: function() { 1.15814 + return !wysihtml.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); 1.15815 + }, 1.15816 + 1.15817 + _observe: function() { 1.15818 + var element = this.element, 1.15819 + parent = this.parent, 1.15820 + eventMapping = { 1.15821 + focusin: "focus", 1.15822 + focusout: "blur" 1.15823 + }, 1.15824 + /** 1.15825 + * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events 1.15826 + * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai 1.15827 + */ 1.15828 + events = wysihtml.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; 1.15829 + 1.15830 + parent.on("beforeload", function() { 1.15831 + wysihtml.dom.observe(element, events, function(event) { 1.15832 + var eventName = eventMapping[event.type] || event.type; 1.15833 + parent.fire(eventName).fire(eventName + ":textarea"); 1.15834 + }); 1.15835 + 1.15836 + wysihtml.dom.observe(element, ["paste", "drop"], function() { 1.15837 + setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); 1.15838 + }); 1.15839 + }); 1.15840 + } 1.15841 +}); 1.15842 + 1.15843 +/** 1.15844 + * WYSIHTML Editor 1.15845 + * 1.15846 + * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface 1.15847 + * @param {Object} [config] See defaults object below for explanation of each individual config option 1.15848 + * 1.15849 + * @events 1.15850 + * load 1.15851 + * beforeload (for internal use only) 1.15852 + * focus 1.15853 + * focus:composer 1.15854 + * focus:textarea 1.15855 + * blur 1.15856 + * blur:composer 1.15857 + * blur:textarea 1.15858 + * change 1.15859 + * change:composer 1.15860 + * change:textarea 1.15861 + * paste 1.15862 + * paste:composer 1.15863 + * paste:textarea 1.15864 + * newword:composer 1.15865 + * destroy:composer 1.15866 + * undo:composer 1.15867 + * redo:composer 1.15868 + * beforecommand:composer 1.15869 + * aftercommand:composer 1.15870 + * enable:composer 1.15871 + * disable:composer 1.15872 + * change_view 1.15873 + */ 1.15874 +(function(wysihtml) { 1.15875 + var undef; 1.15876 + 1.15877 + wysihtml.Editor = wysihtml.lang.Dispatcher.extend({ 1.15878 + /** @scope wysihtml.Editor.prototype */ 1.15879 + defaults: { 1.15880 + // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 1.15881 + name: undef, 1.15882 + // Whether the editor should look like the textarea (by adopting styles) 1.15883 + style: true, 1.15884 + // Whether urls, entered by the user should automatically become clickable-links 1.15885 + autoLink: true, 1.15886 + // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation 1.15887 + handleTabKey: true, 1.15888 + // Object which includes parser rules to apply when html gets cleaned 1.15889 + // See parser_rules/*.js for examples 1.15890 + parserRules: { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} }, 1.15891 + // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead 1.15892 + pasteParserRulesets: null, 1.15893 + // Parser method to use when the user inserts content 1.15894 + parser: wysihtml.dom.parse, 1.15895 + // By default wysihtml will insert a <br> for line breaks, set this to false to use <p> 1.15896 + useLineBreaks: true, 1.15897 + // Double enter (enter on blank line) exits block element in useLineBreaks mode. 1.15898 + // It enables a way of escaping out of block elements and splitting block elements 1.15899 + doubleLineBreakEscapesBlock: true, 1.15900 + // Array (or single string) of stylesheet urls to be loaded in the editor's iframe 1.15901 + stylesheets: [], 1.15902 + // Placeholder text to use, defaults to the placeholder attribute on the textarea element 1.15903 + placeholderText: undef, 1.15904 + // Whether the rich text editor should be rendered on touch devices (wysihtml >= 0.3.0 comes with basic support for iOS 5) 1.15905 + supportTouchDevices: true, 1.15906 + // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content 1.15907 + cleanUp: true, 1.15908 + // Whether to use div instead of secure iframe 1.15909 + contentEditableMode: false, 1.15910 + classNames: { 1.15911 + // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option 1.15912 + composer: "wysihtml-editor", 1.15913 + // Class name to add to the body when the wysihtml editor is supported 1.15914 + body: "wysihtml-supported", 1.15915 + // classname added to editable area element (iframe/div) on creation 1.15916 + sandbox: "wysihtml-sandbox", 1.15917 + // class on editable area with placeholder 1.15918 + placeholder: "wysihtml-placeholder", 1.15919 + // Classname of container that editor should not touch and pass through 1.15920 + uneditableContainer: "wysihtml-uneditable-container" 1.15921 + }, 1.15922 + // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste) 1.15923 + // Also copied source is based directly on selection - 1.15924 + // (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). 1.15925 + // If falsy value is passed source override is also disabled 1.15926 + copyedFromMarking: '<meta name="copied-from" content="wysihtml">' 1.15927 + }, 1.15928 + 1.15929 + constructor: function(editableElement, config) { 1.15930 + this.editableElement = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement; 1.15931 + this.config = wysihtml.lang.object({}).merge(this.defaults).merge(config).get(); 1.15932 + this._isCompatible = wysihtml.browser.supported(); 1.15933 + 1.15934 + // merge classNames 1.15935 + if (config && config.classNames) { 1.15936 + wysihtml.lang.object(this.config.classNames).merge(config.classNames); 1.15937 + } 1.15938 + 1.15939 + if (this.editableElement.nodeName.toLowerCase() != "textarea") { 1.15940 + this.config.contentEditableMode = true; 1.15941 + this.config.noTextarea = true; 1.15942 + } 1.15943 + if (!this.config.noTextarea) { 1.15944 + this.textarea = new wysihtml.views.Textarea(this, this.editableElement, this.config); 1.15945 + this.currentView = this.textarea; 1.15946 + } 1.15947 + 1.15948 + // Sort out unsupported/unwanted browsers here 1.15949 + if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml.browser.isTouchDevice())) { 1.15950 + var that = this; 1.15951 + setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); 1.15952 + return; 1.15953 + } 1.15954 + 1.15955 + // Add class name to body, to indicate that the editor is supported 1.15956 + wysihtml.dom.addClass(document.body, this.config.classNames.body); 1.15957 + 1.15958 + this.composer = new wysihtml.views.Composer(this, this.editableElement, this.config); 1.15959 + this.currentView = this.composer; 1.15960 + 1.15961 + if (typeof(this.config.parser) === "function") { 1.15962 + this._initParser(); 1.15963 + } 1.15964 + 1.15965 + this.on("beforeload", this.handleBeforeLoad); 1.15966 + }, 1.15967 + 1.15968 + handleBeforeLoad: function() { 1.15969 + if (!this.config.noTextarea) { 1.15970 + this.synchronizer = new wysihtml.views.Synchronizer(this, this.textarea, this.composer); 1.15971 + } else { 1.15972 + this.sourceView = new wysihtml.views.SourceView(this, this.composer); 1.15973 + } 1.15974 + this.runEditorExtenders(); 1.15975 + }, 1.15976 + 1.15977 + runEditorExtenders: function() { 1.15978 + wysihtml.editorExtenders.forEach(function(extender) { 1.15979 + extender(this); 1.15980 + }.bind(this)); 1.15981 + }, 1.15982 + 1.15983 + isCompatible: function() { 1.15984 + return this._isCompatible; 1.15985 + }, 1.15986 + 1.15987 + clear: function() { 1.15988 + this.currentView.clear(); 1.15989 + return this; 1.15990 + }, 1.15991 + 1.15992 + getValue: function(parse, clearInternals) { 1.15993 + return this.currentView.getValue(parse, clearInternals); 1.15994 + }, 1.15995 + 1.15996 + setValue: function(html, parse) { 1.15997 + this.fire("unset_placeholder"); 1.15998 + 1.15999 + if (!html) { 1.16000 + return this.clear(); 1.16001 + } 1.16002 + 1.16003 + this.currentView.setValue(html, parse); 1.16004 + return this; 1.16005 + }, 1.16006 + 1.16007 + cleanUp: function(rules) { 1.16008 + this.currentView.cleanUp(rules); 1.16009 + }, 1.16010 + 1.16011 + focus: function(setToEnd) { 1.16012 + this.currentView.focus(setToEnd); 1.16013 + return this; 1.16014 + }, 1.16015 + 1.16016 + /** 1.16017 + * Deactivate editor (make it readonly) 1.16018 + */ 1.16019 + disable: function() { 1.16020 + this.currentView.disable(); 1.16021 + return this; 1.16022 + }, 1.16023 + 1.16024 + /** 1.16025 + * Activate editor 1.16026 + */ 1.16027 + enable: function() { 1.16028 + this.currentView.enable(); 1.16029 + return this; 1.16030 + }, 1.16031 + 1.16032 + isEmpty: function() { 1.16033 + return this.currentView.isEmpty(); 1.16034 + }, 1.16035 + 1.16036 + hasPlaceholderSet: function() { 1.16037 + return this.currentView.hasPlaceholderSet(); 1.16038 + }, 1.16039 + 1.16040 + destroy: function() { 1.16041 + if (this.composer && this.composer.sandbox) { 1.16042 + this.composer.sandbox.destroy(); 1.16043 + } 1.16044 + this.fire("destroy:composer"); 1.16045 + this.off(); 1.16046 + }, 1.16047 + 1.16048 + parse: function(htmlOrElement, clearInternals, customRules) { 1.16049 + var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null); 1.16050 + var returnValue = this.config.parser(htmlOrElement, { 1.16051 + "rules": customRules || this.config.parserRules, 1.16052 + "cleanUp": this.config.cleanUp, 1.16053 + "context": parseContext, 1.16054 + "uneditableClass": this.config.classNames.uneditableContainer, 1.16055 + "clearInternals" : clearInternals 1.16056 + }); 1.16057 + if (typeof(htmlOrElement) === "object") { 1.16058 + wysihtml.quirks.redraw(htmlOrElement); 1.16059 + } 1.16060 + return returnValue; 1.16061 + }, 1.16062 + 1.16063 + /** 1.16064 + * Prepare html parser logic 1.16065 + * - Observes for paste and drop 1.16066 + */ 1.16067 + _initParser: function() { 1.16068 + var oldHtml; 1.16069 + 1.16070 + if (wysihtml.browser.supportsModernPaste()) { 1.16071 + this.on("paste:composer", function(event) { 1.16072 + event.preventDefault(); 1.16073 + oldHtml = wysihtml.dom.getPastedHtml(event); 1.16074 + if (oldHtml) { 1.16075 + this._cleanAndPaste(oldHtml); 1.16076 + } 1.16077 + }.bind(this)); 1.16078 + 1.16079 + } else { 1.16080 + this.on("beforepaste:composer", function(event) { 1.16081 + event.preventDefault(); 1.16082 + var scrollPos = this.composer.getScrollPos(); 1.16083 + 1.16084 + wysihtml.dom.getPastedHtmlWithDiv(this.composer, function(pastedHTML) { 1.16085 + if (pastedHTML) { 1.16086 + this._cleanAndPaste(pastedHTML); 1.16087 + } 1.16088 + this.composer.setScrollPos(scrollPos); 1.16089 + }.bind(this)); 1.16090 + 1.16091 + }.bind(this)); 1.16092 + } 1.16093 + }, 1.16094 + 1.16095 + _cleanAndPaste: function (oldHtml) { 1.16096 + var cleanHtml = wysihtml.quirks.cleanPastedHTML(oldHtml, { 1.16097 + "referenceNode": this.composer.element, 1.16098 + "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}], 1.16099 + "uneditableClass": this.config.classNames.uneditableContainer 1.16100 + }); 1.16101 + this.composer.selection.deleteContents(); 1.16102 + this.composer.selection.insertHTML(cleanHtml); 1.16103 + } 1.16104 + }); 1.16105 +})(wysihtml);