liquid_feedback_frontend

annotate static/wysihtml/wysihtml.js @ 1469:914611dc446b

Fixed heading of admin unit and area views
author bsw
date Thu Oct 18 17:41:02 2018 +0200 (2018-10-18)
parents 32cc544d5a5b
children
rev   line source
bsw/jbe@1309 1 /**
bsw/jbe@1309 2 * @license wysihtml v0.6.0-beta1
bsw/jbe@1309 3 * https://github.com/Voog/wysihtml
bsw/jbe@1309 4 *
bsw/jbe@1309 5 * Author: Christopher Blum (https://github.com/tiff)
bsw/jbe@1309 6 * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
bsw/jbe@1309 7 *
bsw/jbe@1309 8 * Copyright (C) 2012 XING AG
bsw/jbe@1309 9 * Licensed under the MIT license (MIT)
bsw/jbe@1309 10 *
bsw/jbe@1309 11 */
bsw/jbe@1309 12 var wysihtml = {
bsw/jbe@1309 13 version: '0.6.0-beta1',
bsw/jbe@1309 14
bsw/jbe@1309 15 // namespaces
bsw/jbe@1309 16 commands: {},
bsw/jbe@1309 17 dom: {},
bsw/jbe@1309 18 quirks: {},
bsw/jbe@1309 19 toolbar: {},
bsw/jbe@1309 20 lang: {},
bsw/jbe@1309 21 selection: {},
bsw/jbe@1309 22 views: {},
bsw/jbe@1309 23
bsw/jbe@1309 24 editorExtenders: [],
bsw/jbe@1309 25 extendEditor: function(extender) {
bsw/jbe@1309 26 this.editorExtenders.push(extender);
bsw/jbe@1309 27 },
bsw/jbe@1309 28
bsw/jbe@1309 29 INVISIBLE_SPACE: '\uFEFF',
bsw/jbe@1309 30 INVISIBLE_SPACE_REG_EXP: /\uFEFF/g,
bsw/jbe@1309 31
bsw/jbe@1309 32 VOID_ELEMENTS: 'area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr',
bsw/jbe@1309 33 PERMITTED_PHRASING_CONTENT_ONLY: 'h1, h2, h3, h4, h5, h6, p, pre',
bsw/jbe@1309 34
bsw/jbe@1309 35 EMPTY_FUNCTION: function() {},
bsw/jbe@1309 36
bsw/jbe@1309 37 ELEMENT_NODE: 1,
bsw/jbe@1309 38 TEXT_NODE: 3,
bsw/jbe@1309 39
bsw/jbe@1309 40 BACKSPACE_KEY: 8,
bsw/jbe@1309 41 ENTER_KEY: 13,
bsw/jbe@1309 42 ESCAPE_KEY: 27,
bsw/jbe@1309 43 SPACE_KEY: 32,
bsw/jbe@1309 44 TAB_KEY: 9,
bsw/jbe@1309 45 DELETE_KEY: 46
bsw/jbe@1309 46 };
bsw/jbe@1309 47
bsw/jbe@1309 48 wysihtml.polyfills = function(win, doc) {
bsw/jbe@1309 49
bsw/jbe@1309 50 var methods = {
bsw/jbe@1309 51
bsw/jbe@1309 52 // Safary has a bug of not restoring selection after node.normalize correctly.
bsw/jbe@1309 53 // Detects the misbegaviour and patches it
bsw/jbe@1309 54 normalizeHasCaretError: function() {
bsw/jbe@1309 55 if ("createRange" in doc && "getSelection" in win) {
bsw/jbe@1309 56 var originalTarget,
bsw/jbe@1309 57 scrollTop = window.pageYOffset,
bsw/jbe@1309 58 scrollLeft = window.pageXOffset,
bsw/jbe@1309 59 e = doc.createElement('div'),
bsw/jbe@1309 60 t1 = doc.createTextNode('a'),
bsw/jbe@1309 61 t2 = doc.createTextNode('a'),
bsw/jbe@1309 62 t3 = doc.createTextNode('a'),
bsw/jbe@1309 63 r = doc.createRange(),
bsw/jbe@1309 64 s, ret;
bsw/jbe@1309 65
bsw/jbe@1309 66 if (document.activeElement) {
bsw/jbe@1309 67 if (document.activeElement.nodeType === 1 && ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].indexOf(document.activeElement.nodeName) > -1) {
bsw/jbe@1309 68 originalTarget = {
bsw/jbe@1309 69 type: 'form',
bsw/jbe@1309 70 node: document.activeElement,
bsw/jbe@1309 71 start: document.activeElement.selectionStart,
bsw/jbe@1309 72 end: document.activeElement.selectionEnd,
bsw/jbe@1309 73 };
bsw/jbe@1309 74 } else {
bsw/jbe@1309 75 s = win.getSelection();
bsw/jbe@1309 76 if (s && s.anchorNode) {
bsw/jbe@1309 77 originalTarget = {
bsw/jbe@1309 78 type: 'range',
bsw/jbe@1309 79 anchorNode: s.anchorNode,
bsw/jbe@1309 80 anchorOffset: s.anchorOffset,
bsw/jbe@1309 81 focusNode: s.focusNode,
bsw/jbe@1309 82 focusOffset: s.focusOffset
bsw/jbe@1309 83 };
bsw/jbe@1309 84 }
bsw/jbe@1309 85 }
bsw/jbe@1309 86 }
bsw/jbe@1309 87
bsw/jbe@1309 88 e.setAttribute('contenteditable', 'true');
bsw/jbe@1309 89 e.appendChild(t1);
bsw/jbe@1309 90 e.appendChild(t2);
bsw/jbe@1309 91 e.appendChild(t3);
bsw/jbe@1309 92 doc.body.appendChild(e);
bsw/jbe@1309 93 r.setStart(t2, 1);
bsw/jbe@1309 94 r.setEnd(t2, 1);
bsw/jbe@1309 95
bsw/jbe@1309 96 s = win.getSelection();
bsw/jbe@1309 97 s.removeAllRanges();
bsw/jbe@1309 98 s.addRange(r);
bsw/jbe@1309 99 e.normalize();
bsw/jbe@1309 100 s = win.getSelection();
bsw/jbe@1309 101
bsw/jbe@1309 102 ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2);
bsw/jbe@1309 103 e.parentNode.removeChild(e);
bsw/jbe@1309 104 s.removeAllRanges();
bsw/jbe@1309 105
bsw/jbe@1309 106 if (originalTarget) {
bsw/jbe@1309 107 if (originalTarget.type === 'form') {
bsw/jbe@1309 108 // The selection parameters are not present for all form elements
bsw/jbe@1309 109 if (typeof originalTarget.start !== 'undefined' && typeof originalTarget.end !== 'undefined') {
bsw/jbe@1309 110 originalTarget.node.setSelectionRange(originalTarget.start, originalTarget.end);
bsw/jbe@1309 111 }
bsw/jbe@1309 112 originalTarget.node.focus();
bsw/jbe@1309 113 } else if (originalTarget.type === 'range') {
bsw/jbe@1309 114 r = doc.createRange();
bsw/jbe@1309 115 r.setStart(originalTarget.anchorNode, originalTarget.anchorOffset);
bsw/jbe@1309 116 r.setEnd(originalTarget.focusNode, originalTarget.focusOffset);
bsw/jbe@1309 117 s.addRange(r);
bsw/jbe@1309 118 }
bsw/jbe@1309 119 }
bsw/jbe@1309 120
bsw/jbe@1309 121 if (scrollTop !== window.pageYOffset || scrollLeft !== window.pageXOffset) {
bsw/jbe@1309 122 win.scrollTo(scrollLeft, scrollTop);
bsw/jbe@1309 123 }
bsw/jbe@1309 124
bsw/jbe@1309 125 return ret;
bsw/jbe@1309 126 }
bsw/jbe@1309 127 },
bsw/jbe@1309 128
bsw/jbe@1309 129 apply: function() {
bsw/jbe@1309 130 // closest, matches, and remove polyfill
bsw/jbe@1309 131 // https://github.com/jonathantneal/closest
bsw/jbe@1309 132 (function (ELEMENT) {
bsw/jbe@1309 133 ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
bsw/jbe@1309 134 var
bsw/jbe@1309 135 element = this,
bsw/jbe@1309 136 elements = (element.document || element.ownerDocument).querySelectorAll(selector),
bsw/jbe@1309 137 index = 0;
bsw/jbe@1309 138
bsw/jbe@1309 139 while (elements[index] && elements[index] !== element) {
bsw/jbe@1309 140 ++index;
bsw/jbe@1309 141 }
bsw/jbe@1309 142
bsw/jbe@1309 143 return elements[index] ? true : false;
bsw/jbe@1309 144 };
bsw/jbe@1309 145
bsw/jbe@1309 146 ELEMENT.closest = ELEMENT.closest || function closest(selector) {
bsw/jbe@1309 147 var element = this;
bsw/jbe@1309 148
bsw/jbe@1309 149 while (element) {
bsw/jbe@1309 150 if (element.matches(selector)) {
bsw/jbe@1309 151 break;
bsw/jbe@1309 152 }
bsw/jbe@1309 153
bsw/jbe@1309 154 element = element.parentElement;
bsw/jbe@1309 155 }
bsw/jbe@1309 156
bsw/jbe@1309 157 return element;
bsw/jbe@1309 158 };
bsw/jbe@1309 159
bsw/jbe@1309 160 ELEMENT.remove = ELEMENT.remove || function remove() {
bsw/jbe@1309 161 if (this.parentNode) {
bsw/jbe@1309 162 this.parentNode.removeChild(this);
bsw/jbe@1309 163 }
bsw/jbe@1309 164 };
bsw/jbe@1309 165
bsw/jbe@1309 166 }(win.Element.prototype));
bsw/jbe@1309 167
bsw/jbe@1309 168 if (!('classList' in doc.documentElement) && win.Object.defineProperty && typeof win.HTMLElement !== 'undefined') {
bsw/jbe@1309 169 win.Object.defineProperty(win.HTMLElement.prototype, 'classList', {
bsw/jbe@1309 170 get: function() {
bsw/jbe@1309 171 var self = this;
bsw/jbe@1309 172 function update(fn) {
bsw/jbe@1309 173 return function(value) {
bsw/jbe@1309 174 var classes = self.className.split(/\s+/),
bsw/jbe@1309 175 index = classes.indexOf(value);
bsw/jbe@1309 176
bsw/jbe@1309 177 fn(classes, index, value);
bsw/jbe@1309 178 self.className = classes.join(' ');
bsw/jbe@1309 179 };
bsw/jbe@1309 180 }
bsw/jbe@1309 181
bsw/jbe@1309 182 var ret = {
bsw/jbe@1309 183 add: update(function(classes, index, value) {
bsw/jbe@1309 184 ~index || classes.push(value);
bsw/jbe@1309 185 }),
bsw/jbe@1309 186
bsw/jbe@1309 187 remove: update(function(classes, index) {
bsw/jbe@1309 188 ~index && classes.splice(index, 1);
bsw/jbe@1309 189 }),
bsw/jbe@1309 190
bsw/jbe@1309 191 toggle: update(function(classes, index, value) {
bsw/jbe@1309 192 ~index ? classes.splice(index, 1) : classes.push(value);
bsw/jbe@1309 193 }),
bsw/jbe@1309 194
bsw/jbe@1309 195 contains: function(value) {
bsw/jbe@1309 196 return !!~self.className.split(/\s+/).indexOf(value);
bsw/jbe@1309 197 },
bsw/jbe@1309 198
bsw/jbe@1309 199 item: function(i) {
bsw/jbe@1309 200 return self.className.split(/\s+/)[i] || null;
bsw/jbe@1309 201 }
bsw/jbe@1309 202 };
bsw/jbe@1309 203
bsw/jbe@1309 204 win.Object.defineProperty(ret, 'length', {
bsw/jbe@1309 205 get: function() {
bsw/jbe@1309 206 return self.className.split(/\s+/).length;
bsw/jbe@1309 207 }
bsw/jbe@1309 208 });
bsw/jbe@1309 209
bsw/jbe@1309 210 return ret;
bsw/jbe@1309 211 }
bsw/jbe@1309 212 });
bsw/jbe@1309 213 }
bsw/jbe@1309 214
bsw/jbe@1309 215 var getTextNodes = function(node){
bsw/jbe@1309 216 var all = [];
bsw/jbe@1309 217 for (node=node.firstChild;node;node=node.nextSibling){
bsw/jbe@1309 218 if (node.nodeType == 3) {
bsw/jbe@1309 219 all.push(node);
bsw/jbe@1309 220 } else {
bsw/jbe@1309 221 all = all.concat(getTextNodes(node));
bsw/jbe@1309 222 }
bsw/jbe@1309 223 }
bsw/jbe@1309 224 return all;
bsw/jbe@1309 225 };
bsw/jbe@1309 226
bsw/jbe@1309 227 var isInDom = function(node) {
bsw/jbe@1309 228 var doc = node.ownerDocument,
bsw/jbe@1309 229 n = node;
bsw/jbe@1309 230
bsw/jbe@1309 231 do {
bsw/jbe@1309 232 if (n === doc) {
bsw/jbe@1309 233 return true;
bsw/jbe@1309 234 }
bsw/jbe@1309 235 n = n.parentNode;
bsw/jbe@1309 236 } while(n);
bsw/jbe@1309 237
bsw/jbe@1309 238 return false;
bsw/jbe@1309 239 };
bsw/jbe@1309 240
bsw/jbe@1309 241 var normalizeFix = function() {
bsw/jbe@1309 242 var f = win.Node.prototype.normalize;
bsw/jbe@1309 243 var nf = function() {
bsw/jbe@1309 244 var texts = getTextNodes(this),
bsw/jbe@1309 245 s = this.ownerDocument.defaultView.getSelection(),
bsw/jbe@1309 246 anode = s.anchorNode,
bsw/jbe@1309 247 aoffset = s.anchorOffset,
bsw/jbe@1309 248 aelement = anode && anode.nodeType === 1 && anode.childNodes.length > 0 ? anode.childNodes[aoffset] : undefined,
bsw/jbe@1309 249 fnode = s.focusNode,
bsw/jbe@1309 250 foffset = s.focusOffset,
bsw/jbe@1309 251 felement = fnode && fnode.nodeType === 1 && foffset > 0 ? fnode.childNodes[foffset -1] : undefined,
bsw/jbe@1309 252 r = this.ownerDocument.createRange(),
bsw/jbe@1309 253 prevTxt = texts.shift(),
bsw/jbe@1309 254 curText = prevTxt ? texts.shift() : null;
bsw/jbe@1309 255
bsw/jbe@1309 256 if (felement && felement.nodeType === 3) {
bsw/jbe@1309 257 fnode = felement;
bsw/jbe@1309 258 foffset = felement.nodeValue.length;
bsw/jbe@1309 259 felement = undefined;
bsw/jbe@1309 260 }
bsw/jbe@1309 261
bsw/jbe@1309 262 if (aelement && aelement.nodeType === 3) {
bsw/jbe@1309 263 anode = aelement;
bsw/jbe@1309 264 aoffset = 0;
bsw/jbe@1309 265 aelement = undefined;
bsw/jbe@1309 266 }
bsw/jbe@1309 267
bsw/jbe@1309 268 if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_CONTAINS))) {
bsw/jbe@1309 269 fnode = [anode, anode = fnode][0];
bsw/jbe@1309 270 foffset = [aoffset, aoffset = foffset][0];
bsw/jbe@1309 271 }
bsw/jbe@1309 272
bsw/jbe@1309 273 while(prevTxt && curText) {
bsw/jbe@1309 274 if (curText.previousSibling && curText.previousSibling === prevTxt) {
bsw/jbe@1309 275 if (anode === curText) {
bsw/jbe@1309 276 anode = prevTxt;
bsw/jbe@1309 277 aoffset = prevTxt.nodeValue.length + aoffset;
bsw/jbe@1309 278 }
bsw/jbe@1309 279 if (fnode === curText) {
bsw/jbe@1309 280 fnode = prevTxt;
bsw/jbe@1309 281 foffset = prevTxt.nodeValue.length + foffset;
bsw/jbe@1309 282 }
bsw/jbe@1309 283 prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue;
bsw/jbe@1309 284 curText.parentNode.removeChild(curText);
bsw/jbe@1309 285 curText = texts.shift();
bsw/jbe@1309 286 } else {
bsw/jbe@1309 287 prevTxt = curText;
bsw/jbe@1309 288 curText = texts.shift();
bsw/jbe@1309 289 }
bsw/jbe@1309 290 }
bsw/jbe@1309 291
bsw/jbe@1309 292 if (felement) {
bsw/jbe@1309 293 foffset = Array.prototype.indexOf.call(felement.parentNode.childNodes, felement) + 1;
bsw/jbe@1309 294 }
bsw/jbe@1309 295
bsw/jbe@1309 296 if (aelement) {
bsw/jbe@1309 297 aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement);
bsw/jbe@1309 298 }
bsw/jbe@1309 299
bsw/jbe@1309 300 if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) {
bsw/jbe@1309 301 r.setStart(anode, aoffset);
bsw/jbe@1309 302 r.setEnd(fnode, foffset);
bsw/jbe@1309 303 s.removeAllRanges();
bsw/jbe@1309 304 s.addRange(r);
bsw/jbe@1309 305 }
bsw/jbe@1309 306 };
bsw/jbe@1309 307 win.Node.prototype.normalize = nf;
bsw/jbe@1309 308 };
bsw/jbe@1309 309
bsw/jbe@1309 310 var F = function() {
bsw/jbe@1309 311 win.removeEventListener("load", F);
bsw/jbe@1309 312 if ("Node" in win && "normalize" in win.Node.prototype && methods.normalizeHasCaretError()) {
bsw/jbe@1309 313 normalizeFix();
bsw/jbe@1309 314 }
bsw/jbe@1309 315 };
bsw/jbe@1309 316
bsw/jbe@1309 317 if (doc.readyState !== "complete") {
bsw/jbe@1309 318 win.addEventListener("load", F);
bsw/jbe@1309 319 } else {
bsw/jbe@1309 320 F();
bsw/jbe@1309 321 }
bsw/jbe@1309 322
bsw/jbe@1309 323 // CustomEvent for ie9 and up
bsw/jbe@1309 324 function nativeCustomEventSupported() {
bsw/jbe@1309 325 try {
bsw/jbe@1309 326 var p = new win.CustomEvent('cat', {detail: {foo: 'bar'}});
bsw/jbe@1309 327 return 'cat' === p.type && 'bar' === p.detail.foo;
bsw/jbe@1309 328 } catch (e) {}
bsw/jbe@1309 329 return false;
bsw/jbe@1309 330 }
bsw/jbe@1309 331
bsw/jbe@1309 332 // Polyfills CustomEvent object for IE9 and up
bsw/jbe@1309 333 (function() {
bsw/jbe@1309 334 if (!nativeCustomEventSupported() && "CustomEvent" in win) {
bsw/jbe@1309 335 function CustomEvent(event, params) {
bsw/jbe@1309 336 params = params || {bubbles: false, cancelable: false, detail: undefined};
bsw/jbe@1309 337 var evt = doc.createEvent('CustomEvent');
bsw/jbe@1309 338 evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
bsw/jbe@1309 339 return evt;
bsw/jbe@1309 340 }
bsw/jbe@1309 341 CustomEvent.prototype = win.Event.prototype;
bsw/jbe@1309 342 win.CustomEvent = CustomEvent;
bsw/jbe@1309 343 }
bsw/jbe@1309 344 })();
bsw/jbe@1309 345 }
bsw/jbe@1309 346 }
bsw/jbe@1309 347
bsw/jbe@1309 348 return methods;
bsw/jbe@1309 349 };
bsw/jbe@1309 350
bsw/jbe@1309 351 wysihtml.polyfills(window, document).apply();
bsw/jbe@1309 352
bsw/jbe@1309 353 /*
bsw/jbe@1309 354 Base.js, version 1.1a
bsw/jbe@1309 355 Copyright 2006-2010, Dean Edwards
bsw/jbe@1309 356 License: http://www.opensource.org/licenses/mit-license.php
bsw/jbe@1309 357 */
bsw/jbe@1309 358
bsw/jbe@1309 359 var Base = function() {
bsw/jbe@1309 360 // dummy
bsw/jbe@1309 361 };
bsw/jbe@1309 362
bsw/jbe@1309 363 Base.extend = function(_instance, _static) { // subclass
bsw/jbe@1309 364 var extend = Base.prototype.extend;
bsw/jbe@1309 365
bsw/jbe@1309 366 // build the prototype
bsw/jbe@1309 367 Base._prototyping = true;
bsw/jbe@1309 368 var proto = new this;
bsw/jbe@1309 369 extend.call(proto, _instance);
bsw/jbe@1309 370 proto.base = function() {
bsw/jbe@1309 371 // call this method from any other method to invoke that method's ancestor
bsw/jbe@1309 372 };
bsw/jbe@1309 373 delete Base._prototyping;
bsw/jbe@1309 374
bsw/jbe@1309 375 // create the wrapper for the constructor function
bsw/jbe@1309 376 //var constructor = proto.constructor.valueOf(); //-dean
bsw/jbe@1309 377 var constructor = proto.constructor;
bsw/jbe@1309 378 var klass = proto.constructor = function() {
bsw/jbe@1309 379 if (!Base._prototyping) {
bsw/jbe@1309 380 if (this._constructing || this.constructor == klass) { // instantiation
bsw/jbe@1309 381 this._constructing = true;
bsw/jbe@1309 382 constructor.apply(this, arguments);
bsw/jbe@1309 383 delete this._constructing;
bsw/jbe@1309 384 } else if (arguments[0] != null) { // casting
bsw/jbe@1309 385 return (arguments[0].extend || extend).call(arguments[0], proto);
bsw/jbe@1309 386 }
bsw/jbe@1309 387 }
bsw/jbe@1309 388 };
bsw/jbe@1309 389
bsw/jbe@1309 390 // build the class interface
bsw/jbe@1309 391 klass.ancestor = this;
bsw/jbe@1309 392 klass.extend = this.extend;
bsw/jbe@1309 393 klass.forEach = this.forEach;
bsw/jbe@1309 394 klass.implement = this.implement;
bsw/jbe@1309 395 klass.prototype = proto;
bsw/jbe@1309 396 klass.toString = this.toString;
bsw/jbe@1309 397 klass.valueOf = function(type) {
bsw/jbe@1309 398 //return (type == "object") ? klass : constructor; //-dean
bsw/jbe@1309 399 return (type == "object") ? klass : constructor.valueOf();
bsw/jbe@1309 400 };
bsw/jbe@1309 401 extend.call(klass, _static);
bsw/jbe@1309 402 // class initialisation
bsw/jbe@1309 403 if (typeof klass.init == "function") klass.init();
bsw/jbe@1309 404 return klass;
bsw/jbe@1309 405 };
bsw/jbe@1309 406
bsw/jbe@1309 407 Base.prototype = {
bsw/jbe@1309 408 extend: function(source, value) {
bsw/jbe@1309 409 if (arguments.length > 1) { // extending with a name/value pair
bsw/jbe@1309 410 var ancestor = this[source];
bsw/jbe@1309 411 if (ancestor && (typeof value == "function") && // overriding a method?
bsw/jbe@1309 412 // the valueOf() comparison is to avoid circular references
bsw/jbe@1309 413 (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
bsw/jbe@1309 414 /\bbase\b/.test(value)) {
bsw/jbe@1309 415 // get the underlying method
bsw/jbe@1309 416 var method = value.valueOf();
bsw/jbe@1309 417 // override
bsw/jbe@1309 418 value = function() {
bsw/jbe@1309 419 var previous = this.base || Base.prototype.base;
bsw/jbe@1309 420 this.base = ancestor;
bsw/jbe@1309 421 var returnValue = method.apply(this, arguments);
bsw/jbe@1309 422 this.base = previous;
bsw/jbe@1309 423 return returnValue;
bsw/jbe@1309 424 };
bsw/jbe@1309 425 // point to the underlying method
bsw/jbe@1309 426 value.valueOf = function(type) {
bsw/jbe@1309 427 return (type == "object") ? value : method;
bsw/jbe@1309 428 };
bsw/jbe@1309 429 value.toString = Base.toString;
bsw/jbe@1309 430 }
bsw/jbe@1309 431 this[source] = value;
bsw/jbe@1309 432 } else if (source) { // extending with an object literal
bsw/jbe@1309 433 var extend = Base.prototype.extend;
bsw/jbe@1309 434 // if this object has a customised extend method then use it
bsw/jbe@1309 435 if (!Base._prototyping && typeof this != "function") {
bsw/jbe@1309 436 extend = this.extend || extend;
bsw/jbe@1309 437 }
bsw/jbe@1309 438 var proto = {toSource: null};
bsw/jbe@1309 439 // do the "toString" and other methods manually
bsw/jbe@1309 440 var hidden = ["constructor", "toString", "valueOf"];
bsw/jbe@1309 441 // if we are prototyping then include the constructor
bsw/jbe@1309 442 var i = Base._prototyping ? 0 : 1;
bsw/jbe@1309 443 while (key = hidden[i++]) {
bsw/jbe@1309 444 if (source[key] != proto[key]) {
bsw/jbe@1309 445 extend.call(this, key, source[key]);
bsw/jbe@1309 446
bsw/jbe@1309 447 }
bsw/jbe@1309 448 }
bsw/jbe@1309 449 // copy each of the source object's properties to this object
bsw/jbe@1309 450 for (var key in source) {
bsw/jbe@1309 451 if (!proto[key]) extend.call(this, key, source[key]);
bsw/jbe@1309 452 }
bsw/jbe@1309 453 }
bsw/jbe@1309 454 return this;
bsw/jbe@1309 455 }
bsw/jbe@1309 456 };
bsw/jbe@1309 457
bsw/jbe@1309 458 // initialise
bsw/jbe@1309 459 Base = Base.extend({
bsw/jbe@1309 460 constructor: function() {
bsw/jbe@1309 461 this.extend(arguments[0]);
bsw/jbe@1309 462 }
bsw/jbe@1309 463 }, {
bsw/jbe@1309 464 ancestor: Object,
bsw/jbe@1309 465 version: "1.1",
bsw/jbe@1309 466
bsw/jbe@1309 467 forEach: function(object, block, context) {
bsw/jbe@1309 468 for (var key in object) {
bsw/jbe@1309 469 if (this.prototype[key] === undefined) {
bsw/jbe@1309 470 block.call(context, object[key], key, object);
bsw/jbe@1309 471 }
bsw/jbe@1309 472 }
bsw/jbe@1309 473 },
bsw/jbe@1309 474
bsw/jbe@1309 475 implement: function() {
bsw/jbe@1309 476 for (var i = 0; i < arguments.length; i++) {
bsw/jbe@1309 477 if (typeof arguments[i] == "function") {
bsw/jbe@1309 478 // if it's a function, call it
bsw/jbe@1309 479 arguments[i](this.prototype);
bsw/jbe@1309 480 } else {
bsw/jbe@1309 481 // add the interface using the extend method
bsw/jbe@1309 482 this.prototype.extend(arguments[i]);
bsw/jbe@1309 483 }
bsw/jbe@1309 484 }
bsw/jbe@1309 485 return this;
bsw/jbe@1309 486 },
bsw/jbe@1309 487
bsw/jbe@1309 488 toString: function() {
bsw/jbe@1309 489 return String(this.valueOf());
bsw/jbe@1309 490 }
bsw/jbe@1309 491 });
bsw/jbe@1309 492 /**
bsw/jbe@1309 493 * Rangy, a cross-browser JavaScript range and selection library
bsw/jbe@1309 494 * https://github.com/timdown/rangy
bsw/jbe@1309 495 *
bsw/jbe@1309 496 * Copyright 2015, Tim Down
bsw/jbe@1309 497 * Licensed under the MIT license.
bsw/jbe@1309 498 * Version: 1.3.1-dev
bsw/jbe@1309 499 * Build date: 20 May 2015
bsw/jbe@1309 500 *
bsw/jbe@1309 501 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
bsw/jbe@1309 502 */
bsw/jbe@1309 503 var rangy;
bsw/jbe@1309 504
bsw/jbe@1309 505 (function() {
bsw/jbe@1309 506 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
bsw/jbe@1309 507
bsw/jbe@1309 508 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
bsw/jbe@1309 509 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
bsw/jbe@1309 510 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
bsw/jbe@1309 511 "commonAncestorContainer"];
bsw/jbe@1309 512
bsw/jbe@1309 513 // Minimal set of methods required for DOM Level 2 Range compliance
bsw/jbe@1309 514 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
bsw/jbe@1309 515 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
bsw/jbe@1309 516 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
bsw/jbe@1309 517
bsw/jbe@1309 518 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
bsw/jbe@1309 519
bsw/jbe@1309 520 // Subset of TextRange's full set of methods that we're interested in
bsw/jbe@1309 521 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
bsw/jbe@1309 522 "setEndPoint", "getBoundingClientRect"];
bsw/jbe@1309 523
bsw/jbe@1309 524 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 525
bsw/jbe@1309 526 // Trio of functions taken from Peter Michaux's article:
bsw/jbe@1309 527 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
bsw/jbe@1309 528 function isHostMethod(o, p) {
bsw/jbe@1309 529 var t = typeof o[p];
bsw/jbe@1309 530 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
bsw/jbe@1309 531 }
bsw/jbe@1309 532
bsw/jbe@1309 533 function isHostObject(o, p) {
bsw/jbe@1309 534 return !!(typeof o[p] == OBJECT && o[p]);
bsw/jbe@1309 535 }
bsw/jbe@1309 536
bsw/jbe@1309 537 function isHostProperty(o, p) {
bsw/jbe@1309 538 return typeof o[p] != UNDEFINED;
bsw/jbe@1309 539 }
bsw/jbe@1309 540
bsw/jbe@1309 541 // Creates a convenience function to save verbose repeated calls to tests functions
bsw/jbe@1309 542 function createMultiplePropertyTest(testFunc) {
bsw/jbe@1309 543 return function(o, props) {
bsw/jbe@1309 544 var i = props.length;
bsw/jbe@1309 545 while (i--) {
bsw/jbe@1309 546 if (!testFunc(o, props[i])) {
bsw/jbe@1309 547 return false;
bsw/jbe@1309 548 }
bsw/jbe@1309 549 }
bsw/jbe@1309 550 return true;
bsw/jbe@1309 551 };
bsw/jbe@1309 552 }
bsw/jbe@1309 553
bsw/jbe@1309 554 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
bsw/jbe@1309 555 var areHostMethods = createMultiplePropertyTest(isHostMethod);
bsw/jbe@1309 556 var areHostObjects = createMultiplePropertyTest(isHostObject);
bsw/jbe@1309 557 var areHostProperties = createMultiplePropertyTest(isHostProperty);
bsw/jbe@1309 558
bsw/jbe@1309 559 function isTextRange(range) {
bsw/jbe@1309 560 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
bsw/jbe@1309 561 }
bsw/jbe@1309 562
bsw/jbe@1309 563 function getBody(doc) {
bsw/jbe@1309 564 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
bsw/jbe@1309 565 }
bsw/jbe@1309 566
bsw/jbe@1309 567 var forEach = [].forEach ?
bsw/jbe@1309 568 function(arr, func) {
bsw/jbe@1309 569 arr.forEach(func);
bsw/jbe@1309 570 } :
bsw/jbe@1309 571 function(arr, func) {
bsw/jbe@1309 572 for (var i = 0, len = arr.length; i < len; ++i) {
bsw/jbe@1309 573 func(arr[i], i);
bsw/jbe@1309 574 }
bsw/jbe@1309 575 };
bsw/jbe@1309 576
bsw/jbe@1309 577 var modules = {};
bsw/jbe@1309 578
bsw/jbe@1309 579 var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
bsw/jbe@1309 580
bsw/jbe@1309 581 var util = {
bsw/jbe@1309 582 isHostMethod: isHostMethod,
bsw/jbe@1309 583 isHostObject: isHostObject,
bsw/jbe@1309 584 isHostProperty: isHostProperty,
bsw/jbe@1309 585 areHostMethods: areHostMethods,
bsw/jbe@1309 586 areHostObjects: areHostObjects,
bsw/jbe@1309 587 areHostProperties: areHostProperties,
bsw/jbe@1309 588 isTextRange: isTextRange,
bsw/jbe@1309 589 getBody: getBody,
bsw/jbe@1309 590 forEach: forEach
bsw/jbe@1309 591 };
bsw/jbe@1309 592
bsw/jbe@1309 593 var api = {
bsw/jbe@1309 594 version: "1.3.1-dev",
bsw/jbe@1309 595 initialized: false,
bsw/jbe@1309 596 isBrowser: isBrowser,
bsw/jbe@1309 597 supported: true,
bsw/jbe@1309 598 util: util,
bsw/jbe@1309 599 features: {},
bsw/jbe@1309 600 modules: modules,
bsw/jbe@1309 601 config: {
bsw/jbe@1309 602 alertOnFail: false,
bsw/jbe@1309 603 alertOnWarn: false,
bsw/jbe@1309 604 preferTextRange: false,
bsw/jbe@1309 605 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
bsw/jbe@1309 606 }
bsw/jbe@1309 607 };
bsw/jbe@1309 608
bsw/jbe@1309 609 function consoleLog(msg) {
bsw/jbe@1309 610 if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
bsw/jbe@1309 611 console.log(msg);
bsw/jbe@1309 612 }
bsw/jbe@1309 613 }
bsw/jbe@1309 614
bsw/jbe@1309 615 function alertOrLog(msg, shouldAlert) {
bsw/jbe@1309 616 if (isBrowser && shouldAlert) {
bsw/jbe@1309 617 alert(msg);
bsw/jbe@1309 618 } else {
bsw/jbe@1309 619 consoleLog(msg);
bsw/jbe@1309 620 }
bsw/jbe@1309 621 }
bsw/jbe@1309 622
bsw/jbe@1309 623 function fail(reason) {
bsw/jbe@1309 624 api.initialized = true;
bsw/jbe@1309 625 api.supported = false;
bsw/jbe@1309 626 alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
bsw/jbe@1309 627 }
bsw/jbe@1309 628
bsw/jbe@1309 629 api.fail = fail;
bsw/jbe@1309 630
bsw/jbe@1309 631 function warn(msg) {
bsw/jbe@1309 632 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
bsw/jbe@1309 633 }
bsw/jbe@1309 634
bsw/jbe@1309 635 api.warn = warn;
bsw/jbe@1309 636
bsw/jbe@1309 637 // Add utility extend() method
bsw/jbe@1309 638 var extend;
bsw/jbe@1309 639 if ({}.hasOwnProperty) {
bsw/jbe@1309 640 util.extend = extend = function(obj, props, deep) {
bsw/jbe@1309 641 var o, p;
bsw/jbe@1309 642 for (var i in props) {
bsw/jbe@1309 643 if (props.hasOwnProperty(i)) {
bsw/jbe@1309 644 o = obj[i];
bsw/jbe@1309 645 p = props[i];
bsw/jbe@1309 646 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
bsw/jbe@1309 647 extend(o, p, true);
bsw/jbe@1309 648 }
bsw/jbe@1309 649 obj[i] = p;
bsw/jbe@1309 650 }
bsw/jbe@1309 651 }
bsw/jbe@1309 652 // Special case for toString, which does not show up in for...in loops in IE <= 8
bsw/jbe@1309 653 if (props.hasOwnProperty("toString")) {
bsw/jbe@1309 654 obj.toString = props.toString;
bsw/jbe@1309 655 }
bsw/jbe@1309 656 return obj;
bsw/jbe@1309 657 };
bsw/jbe@1309 658
bsw/jbe@1309 659 util.createOptions = function(optionsParam, defaults) {
bsw/jbe@1309 660 var options = {};
bsw/jbe@1309 661 extend(options, defaults);
bsw/jbe@1309 662 if (optionsParam) {
bsw/jbe@1309 663 extend(options, optionsParam);
bsw/jbe@1309 664 }
bsw/jbe@1309 665 return options;
bsw/jbe@1309 666 };
bsw/jbe@1309 667 } else {
bsw/jbe@1309 668 fail("hasOwnProperty not supported");
bsw/jbe@1309 669 }
bsw/jbe@1309 670
bsw/jbe@1309 671 // Test whether we're in a browser and bail out if not
bsw/jbe@1309 672 if (!isBrowser) {
bsw/jbe@1309 673 fail("Rangy can only run in a browser");
bsw/jbe@1309 674 }
bsw/jbe@1309 675
bsw/jbe@1309 676 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
bsw/jbe@1309 677 (function() {
bsw/jbe@1309 678 var toArray;
bsw/jbe@1309 679
bsw/jbe@1309 680 if (isBrowser) {
bsw/jbe@1309 681 var el = document.createElement("div");
bsw/jbe@1309 682 el.appendChild(document.createElement("span"));
bsw/jbe@1309 683 var slice = [].slice;
bsw/jbe@1309 684 try {
bsw/jbe@1309 685 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
bsw/jbe@1309 686 toArray = function(arrayLike) {
bsw/jbe@1309 687 return slice.call(arrayLike, 0);
bsw/jbe@1309 688 };
bsw/jbe@1309 689 }
bsw/jbe@1309 690 } catch (e) {}
bsw/jbe@1309 691 }
bsw/jbe@1309 692
bsw/jbe@1309 693 if (!toArray) {
bsw/jbe@1309 694 toArray = function(arrayLike) {
bsw/jbe@1309 695 var arr = [];
bsw/jbe@1309 696 for (var i = 0, len = arrayLike.length; i < len; ++i) {
bsw/jbe@1309 697 arr[i] = arrayLike[i];
bsw/jbe@1309 698 }
bsw/jbe@1309 699 return arr;
bsw/jbe@1309 700 };
bsw/jbe@1309 701 }
bsw/jbe@1309 702
bsw/jbe@1309 703 util.toArray = toArray;
bsw/jbe@1309 704 })();
bsw/jbe@1309 705
bsw/jbe@1309 706 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
bsw/jbe@1309 707 // normalization of event properties
bsw/jbe@1309 708 var addListener;
bsw/jbe@1309 709 if (isBrowser) {
bsw/jbe@1309 710 if (isHostMethod(document, "addEventListener")) {
bsw/jbe@1309 711 addListener = function(obj, eventType, listener) {
bsw/jbe@1309 712 obj.addEventListener(eventType, listener, false);
bsw/jbe@1309 713 };
bsw/jbe@1309 714 } else if (isHostMethod(document, "attachEvent")) {
bsw/jbe@1309 715 addListener = function(obj, eventType, listener) {
bsw/jbe@1309 716 obj.attachEvent("on" + eventType, listener);
bsw/jbe@1309 717 };
bsw/jbe@1309 718 } else {
bsw/jbe@1309 719 fail("Document does not have required addEventListener or attachEvent method");
bsw/jbe@1309 720 }
bsw/jbe@1309 721
bsw/jbe@1309 722 util.addListener = addListener;
bsw/jbe@1309 723 }
bsw/jbe@1309 724
bsw/jbe@1309 725 var initListeners = [];
bsw/jbe@1309 726
bsw/jbe@1309 727 function getErrorDesc(ex) {
bsw/jbe@1309 728 return ex.message || ex.description || String(ex);
bsw/jbe@1309 729 }
bsw/jbe@1309 730
bsw/jbe@1309 731 // Initialization
bsw/jbe@1309 732 function init() {
bsw/jbe@1309 733 if (!isBrowser || api.initialized) {
bsw/jbe@1309 734 return;
bsw/jbe@1309 735 }
bsw/jbe@1309 736 var testRange;
bsw/jbe@1309 737 var implementsDomRange = false, implementsTextRange = false;
bsw/jbe@1309 738
bsw/jbe@1309 739 // First, perform basic feature tests
bsw/jbe@1309 740
bsw/jbe@1309 741 if (isHostMethod(document, "createRange")) {
bsw/jbe@1309 742 testRange = document.createRange();
bsw/jbe@1309 743 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
bsw/jbe@1309 744 implementsDomRange = true;
bsw/jbe@1309 745 }
bsw/jbe@1309 746 }
bsw/jbe@1309 747
bsw/jbe@1309 748 var body = getBody(document);
bsw/jbe@1309 749 if (!body || body.nodeName.toLowerCase() != "body") {
bsw/jbe@1309 750 fail("No body element found");
bsw/jbe@1309 751 return;
bsw/jbe@1309 752 }
bsw/jbe@1309 753
bsw/jbe@1309 754 if (body && isHostMethod(body, "createTextRange")) {
bsw/jbe@1309 755 testRange = body.createTextRange();
bsw/jbe@1309 756 if (isTextRange(testRange)) {
bsw/jbe@1309 757 implementsTextRange = true;
bsw/jbe@1309 758 }
bsw/jbe@1309 759 }
bsw/jbe@1309 760
bsw/jbe@1309 761 if (!implementsDomRange && !implementsTextRange) {
bsw/jbe@1309 762 fail("Neither Range nor TextRange are available");
bsw/jbe@1309 763 return;
bsw/jbe@1309 764 }
bsw/jbe@1309 765
bsw/jbe@1309 766 api.initialized = true;
bsw/jbe@1309 767 api.features = {
bsw/jbe@1309 768 implementsDomRange: implementsDomRange,
bsw/jbe@1309 769 implementsTextRange: implementsTextRange
bsw/jbe@1309 770 };
bsw/jbe@1309 771
bsw/jbe@1309 772 // Initialize modules
bsw/jbe@1309 773 var module, errorMessage;
bsw/jbe@1309 774 for (var moduleName in modules) {
bsw/jbe@1309 775 if ( (module = modules[moduleName]) instanceof Module ) {
bsw/jbe@1309 776 module.init(module, api);
bsw/jbe@1309 777 }
bsw/jbe@1309 778 }
bsw/jbe@1309 779
bsw/jbe@1309 780 // Call init listeners
bsw/jbe@1309 781 for (var i = 0, len = initListeners.length; i < len; ++i) {
bsw/jbe@1309 782 try {
bsw/jbe@1309 783 initListeners[i](api);
bsw/jbe@1309 784 } catch (ex) {
bsw/jbe@1309 785 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
bsw/jbe@1309 786 consoleLog(errorMessage);
bsw/jbe@1309 787 }
bsw/jbe@1309 788 }
bsw/jbe@1309 789 }
bsw/jbe@1309 790
bsw/jbe@1309 791 function deprecationNotice(deprecated, replacement, module) {
bsw/jbe@1309 792 if (module) {
bsw/jbe@1309 793 deprecated += " in module " + module.name;
bsw/jbe@1309 794 }
bsw/jbe@1309 795 api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
bsw/jbe@1309 796 replacement + " instead.");
bsw/jbe@1309 797 }
bsw/jbe@1309 798
bsw/jbe@1309 799 function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
bsw/jbe@1309 800 owner[deprecated] = function() {
bsw/jbe@1309 801 deprecationNotice(deprecated, replacement, module);
bsw/jbe@1309 802 return owner[replacement].apply(owner, util.toArray(arguments));
bsw/jbe@1309 803 };
bsw/jbe@1309 804 }
bsw/jbe@1309 805
bsw/jbe@1309 806 util.deprecationNotice = deprecationNotice;
bsw/jbe@1309 807 util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
bsw/jbe@1309 808
bsw/jbe@1309 809 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
bsw/jbe@1309 810 api.init = init;
bsw/jbe@1309 811
bsw/jbe@1309 812 // Execute listener immediately if already initialized
bsw/jbe@1309 813 api.addInitListener = function(listener) {
bsw/jbe@1309 814 if (api.initialized) {
bsw/jbe@1309 815 listener(api);
bsw/jbe@1309 816 } else {
bsw/jbe@1309 817 initListeners.push(listener);
bsw/jbe@1309 818 }
bsw/jbe@1309 819 };
bsw/jbe@1309 820
bsw/jbe@1309 821 var shimListeners = [];
bsw/jbe@1309 822
bsw/jbe@1309 823 api.addShimListener = function(listener) {
bsw/jbe@1309 824 shimListeners.push(listener);
bsw/jbe@1309 825 };
bsw/jbe@1309 826
bsw/jbe@1309 827 function shim(win) {
bsw/jbe@1309 828 win = win || window;
bsw/jbe@1309 829 init();
bsw/jbe@1309 830
bsw/jbe@1309 831 // Notify listeners
bsw/jbe@1309 832 for (var i = 0, len = shimListeners.length; i < len; ++i) {
bsw/jbe@1309 833 shimListeners[i](win);
bsw/jbe@1309 834 }
bsw/jbe@1309 835 }
bsw/jbe@1309 836
bsw/jbe@1309 837 if (isBrowser) {
bsw/jbe@1309 838 api.shim = api.createMissingNativeApi = shim;
bsw/jbe@1309 839 createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
bsw/jbe@1309 840 }
bsw/jbe@1309 841
bsw/jbe@1309 842 function Module(name, dependencies, initializer) {
bsw/jbe@1309 843 this.name = name;
bsw/jbe@1309 844 this.dependencies = dependencies;
bsw/jbe@1309 845 this.initialized = false;
bsw/jbe@1309 846 this.supported = false;
bsw/jbe@1309 847 this.initializer = initializer;
bsw/jbe@1309 848 }
bsw/jbe@1309 849
bsw/jbe@1309 850 Module.prototype = {
bsw/jbe@1309 851 init: function() {
bsw/jbe@1309 852 var requiredModuleNames = this.dependencies || [];
bsw/jbe@1309 853 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
bsw/jbe@1309 854 moduleName = requiredModuleNames[i];
bsw/jbe@1309 855
bsw/jbe@1309 856 requiredModule = modules[moduleName];
bsw/jbe@1309 857 if (!requiredModule || !(requiredModule instanceof Module)) {
bsw/jbe@1309 858 throw new Error("required module '" + moduleName + "' not found");
bsw/jbe@1309 859 }
bsw/jbe@1309 860
bsw/jbe@1309 861 requiredModule.init();
bsw/jbe@1309 862
bsw/jbe@1309 863 if (!requiredModule.supported) {
bsw/jbe@1309 864 throw new Error("required module '" + moduleName + "' not supported");
bsw/jbe@1309 865 }
bsw/jbe@1309 866 }
bsw/jbe@1309 867
bsw/jbe@1309 868 // Now run initializer
bsw/jbe@1309 869 this.initializer(this);
bsw/jbe@1309 870 },
bsw/jbe@1309 871
bsw/jbe@1309 872 fail: function(reason) {
bsw/jbe@1309 873 this.initialized = true;
bsw/jbe@1309 874 this.supported = false;
bsw/jbe@1309 875 throw new Error(reason);
bsw/jbe@1309 876 },
bsw/jbe@1309 877
bsw/jbe@1309 878 warn: function(msg) {
bsw/jbe@1309 879 api.warn("Module " + this.name + ": " + msg);
bsw/jbe@1309 880 },
bsw/jbe@1309 881
bsw/jbe@1309 882 deprecationNotice: function(deprecated, replacement) {
bsw/jbe@1309 883 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
bsw/jbe@1309 884 replacement + " instead");
bsw/jbe@1309 885 },
bsw/jbe@1309 886
bsw/jbe@1309 887 createError: function(msg) {
bsw/jbe@1309 888 return new Error("Error in Rangy " + this.name + " module: " + msg);
bsw/jbe@1309 889 }
bsw/jbe@1309 890 };
bsw/jbe@1309 891
bsw/jbe@1309 892 function createModule(name, dependencies, initFunc) {
bsw/jbe@1309 893 var newModule = new Module(name, dependencies, function(module) {
bsw/jbe@1309 894 if (!module.initialized) {
bsw/jbe@1309 895 module.initialized = true;
bsw/jbe@1309 896 try {
bsw/jbe@1309 897 initFunc(api, module);
bsw/jbe@1309 898 module.supported = true;
bsw/jbe@1309 899 } catch (ex) {
bsw/jbe@1309 900 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
bsw/jbe@1309 901 consoleLog(errorMessage);
bsw/jbe@1309 902 if (ex.stack) {
bsw/jbe@1309 903 consoleLog(ex.stack);
bsw/jbe@1309 904 }
bsw/jbe@1309 905 }
bsw/jbe@1309 906 }
bsw/jbe@1309 907 });
bsw/jbe@1309 908 modules[name] = newModule;
bsw/jbe@1309 909 return newModule;
bsw/jbe@1309 910 }
bsw/jbe@1309 911
bsw/jbe@1309 912 api.createModule = function(name) {
bsw/jbe@1309 913 // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
bsw/jbe@1309 914 var initFunc, dependencies;
bsw/jbe@1309 915 if (arguments.length == 2) {
bsw/jbe@1309 916 initFunc = arguments[1];
bsw/jbe@1309 917 dependencies = [];
bsw/jbe@1309 918 } else {
bsw/jbe@1309 919 initFunc = arguments[2];
bsw/jbe@1309 920 dependencies = arguments[1];
bsw/jbe@1309 921 }
bsw/jbe@1309 922
bsw/jbe@1309 923 var module = createModule(name, dependencies, initFunc);
bsw/jbe@1309 924
bsw/jbe@1309 925 // Initialize the module immediately if the core is already initialized
bsw/jbe@1309 926 if (api.initialized && api.supported) {
bsw/jbe@1309 927 module.init();
bsw/jbe@1309 928 }
bsw/jbe@1309 929 };
bsw/jbe@1309 930
bsw/jbe@1309 931 api.createCoreModule = function(name, dependencies, initFunc) {
bsw/jbe@1309 932 createModule(name, dependencies, initFunc);
bsw/jbe@1309 933 };
bsw/jbe@1309 934
bsw/jbe@1309 935 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 936
bsw/jbe@1309 937 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
bsw/jbe@1309 938
bsw/jbe@1309 939 function RangePrototype() {}
bsw/jbe@1309 940 api.RangePrototype = RangePrototype;
bsw/jbe@1309 941 api.rangePrototype = new RangePrototype();
bsw/jbe@1309 942
bsw/jbe@1309 943 function SelectionPrototype() {}
bsw/jbe@1309 944 api.selectionPrototype = new SelectionPrototype();
bsw/jbe@1309 945
bsw/jbe@1309 946 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 947
bsw/jbe@1309 948 // DOM utility methods used by Rangy
bsw/jbe@1309 949 api.createCoreModule("DomUtil", [], function(api, module) {
bsw/jbe@1309 950 var UNDEF = "undefined";
bsw/jbe@1309 951 var util = api.util;
bsw/jbe@1309 952 var getBody = util.getBody;
bsw/jbe@1309 953
bsw/jbe@1309 954 // Perform feature tests
bsw/jbe@1309 955 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
bsw/jbe@1309 956 module.fail("document missing a Node creation method");
bsw/jbe@1309 957 }
bsw/jbe@1309 958
bsw/jbe@1309 959 if (!util.isHostMethod(document, "getElementsByTagName")) {
bsw/jbe@1309 960 module.fail("document missing getElementsByTagName method");
bsw/jbe@1309 961 }
bsw/jbe@1309 962
bsw/jbe@1309 963 var el = document.createElement("div");
bsw/jbe@1309 964 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
bsw/jbe@1309 965 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
bsw/jbe@1309 966 module.fail("Incomplete Element implementation");
bsw/jbe@1309 967 }
bsw/jbe@1309 968
bsw/jbe@1309 969 // innerHTML is required for Range's createContextualFragment method
bsw/jbe@1309 970 if (!util.isHostProperty(el, "innerHTML")) {
bsw/jbe@1309 971 module.fail("Element is missing innerHTML property");
bsw/jbe@1309 972 }
bsw/jbe@1309 973
bsw/jbe@1309 974 var textNode = document.createTextNode("test");
bsw/jbe@1309 975 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
bsw/jbe@1309 976 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
bsw/jbe@1309 977 !util.areHostProperties(textNode, ["data"]))) {
bsw/jbe@1309 978 module.fail("Incomplete Text Node implementation");
bsw/jbe@1309 979 }
bsw/jbe@1309 980
bsw/jbe@1309 981 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 982
bsw/jbe@1309 983 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
bsw/jbe@1309 984 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
bsw/jbe@1309 985 // contains just the document as a single element and the value searched for is the document.
bsw/jbe@1309 986 var arrayContains = /*Array.prototype.indexOf ?
bsw/jbe@1309 987 function(arr, val) {
bsw/jbe@1309 988 return arr.indexOf(val) > -1;
bsw/jbe@1309 989 }:*/
bsw/jbe@1309 990
bsw/jbe@1309 991 function(arr, val) {
bsw/jbe@1309 992 var i = arr.length;
bsw/jbe@1309 993 while (i--) {
bsw/jbe@1309 994 if (arr[i] === val) {
bsw/jbe@1309 995 return true;
bsw/jbe@1309 996 }
bsw/jbe@1309 997 }
bsw/jbe@1309 998 return false;
bsw/jbe@1309 999 };
bsw/jbe@1309 1000
bsw/jbe@1309 1001 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
bsw/jbe@1309 1002 function isHtmlNamespace(node) {
bsw/jbe@1309 1003 var ns;
bsw/jbe@1309 1004 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
bsw/jbe@1309 1005 }
bsw/jbe@1309 1006
bsw/jbe@1309 1007 function parentElement(node) {
bsw/jbe@1309 1008 var parent = node.parentNode;
bsw/jbe@1309 1009 return (parent.nodeType == 1) ? parent : null;
bsw/jbe@1309 1010 }
bsw/jbe@1309 1011
bsw/jbe@1309 1012 function getNodeIndex(node) {
bsw/jbe@1309 1013 var i = 0;
bsw/jbe@1309 1014 while( (node = node.previousSibling) ) {
bsw/jbe@1309 1015 ++i;
bsw/jbe@1309 1016 }
bsw/jbe@1309 1017 return i;
bsw/jbe@1309 1018 }
bsw/jbe@1309 1019
bsw/jbe@1309 1020 function getNodeLength(node) {
bsw/jbe@1309 1021 switch (node.nodeType) {
bsw/jbe@1309 1022 case 7:
bsw/jbe@1309 1023 case 10:
bsw/jbe@1309 1024 return 0;
bsw/jbe@1309 1025 case 3:
bsw/jbe@1309 1026 case 8:
bsw/jbe@1309 1027 return node.length;
bsw/jbe@1309 1028 default:
bsw/jbe@1309 1029 return node.childNodes.length;
bsw/jbe@1309 1030 }
bsw/jbe@1309 1031 }
bsw/jbe@1309 1032
bsw/jbe@1309 1033 function getCommonAncestor(node1, node2) {
bsw/jbe@1309 1034 var ancestors = [], n;
bsw/jbe@1309 1035 for (n = node1; n; n = n.parentNode) {
bsw/jbe@1309 1036 ancestors.push(n);
bsw/jbe@1309 1037 }
bsw/jbe@1309 1038
bsw/jbe@1309 1039 for (n = node2; n; n = n.parentNode) {
bsw/jbe@1309 1040 if (arrayContains(ancestors, n)) {
bsw/jbe@1309 1041 return n;
bsw/jbe@1309 1042 }
bsw/jbe@1309 1043 }
bsw/jbe@1309 1044
bsw/jbe@1309 1045 return null;
bsw/jbe@1309 1046 }
bsw/jbe@1309 1047
bsw/jbe@1309 1048 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
bsw/jbe@1309 1049 var n = selfIsAncestor ? descendant : descendant.parentNode;
bsw/jbe@1309 1050 while (n) {
bsw/jbe@1309 1051 if (n === ancestor) {
bsw/jbe@1309 1052 return true;
bsw/jbe@1309 1053 } else {
bsw/jbe@1309 1054 n = n.parentNode;
bsw/jbe@1309 1055 }
bsw/jbe@1309 1056 }
bsw/jbe@1309 1057 return false;
bsw/jbe@1309 1058 }
bsw/jbe@1309 1059
bsw/jbe@1309 1060 function isOrIsAncestorOf(ancestor, descendant) {
bsw/jbe@1309 1061 return isAncestorOf(ancestor, descendant, true);
bsw/jbe@1309 1062 }
bsw/jbe@1309 1063
bsw/jbe@1309 1064 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
bsw/jbe@1309 1065 var p, n = selfIsAncestor ? node : node.parentNode;
bsw/jbe@1309 1066 while (n) {
bsw/jbe@1309 1067 p = n.parentNode;
bsw/jbe@1309 1068 if (p === ancestor) {
bsw/jbe@1309 1069 return n;
bsw/jbe@1309 1070 }
bsw/jbe@1309 1071 n = p;
bsw/jbe@1309 1072 }
bsw/jbe@1309 1073 return null;
bsw/jbe@1309 1074 }
bsw/jbe@1309 1075
bsw/jbe@1309 1076 function isCharacterDataNode(node) {
bsw/jbe@1309 1077 var t = node.nodeType;
bsw/jbe@1309 1078 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
bsw/jbe@1309 1079 }
bsw/jbe@1309 1080
bsw/jbe@1309 1081 function isTextOrCommentNode(node) {
bsw/jbe@1309 1082 if (!node) {
bsw/jbe@1309 1083 return false;
bsw/jbe@1309 1084 }
bsw/jbe@1309 1085 var t = node.nodeType;
bsw/jbe@1309 1086 return t == 3 || t == 8 ; // Text or Comment
bsw/jbe@1309 1087 }
bsw/jbe@1309 1088
bsw/jbe@1309 1089 function insertAfter(node, precedingNode) {
bsw/jbe@1309 1090 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
bsw/jbe@1309 1091 if (nextNode) {
bsw/jbe@1309 1092 parent.insertBefore(node, nextNode);
bsw/jbe@1309 1093 } else {
bsw/jbe@1309 1094 parent.appendChild(node);
bsw/jbe@1309 1095 }
bsw/jbe@1309 1096 return node;
bsw/jbe@1309 1097 }
bsw/jbe@1309 1098
bsw/jbe@1309 1099 // Note that we cannot use splitText() because it is bugridden in IE 9.
bsw/jbe@1309 1100 function splitDataNode(node, index, positionsToPreserve) {
bsw/jbe@1309 1101 var newNode = node.cloneNode(false);
bsw/jbe@1309 1102 newNode.deleteData(0, index);
bsw/jbe@1309 1103 node.deleteData(index, node.length - index);
bsw/jbe@1309 1104 insertAfter(newNode, node);
bsw/jbe@1309 1105
bsw/jbe@1309 1106 // Preserve positions
bsw/jbe@1309 1107 if (positionsToPreserve) {
bsw/jbe@1309 1108 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
bsw/jbe@1309 1109 // Handle case where position was inside the portion of node after the split point
bsw/jbe@1309 1110 if (position.node == node && position.offset > index) {
bsw/jbe@1309 1111 position.node = newNode;
bsw/jbe@1309 1112 position.offset -= index;
bsw/jbe@1309 1113 }
bsw/jbe@1309 1114 // Handle the case where the position is a node offset within node's parent
bsw/jbe@1309 1115 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
bsw/jbe@1309 1116 ++position.offset;
bsw/jbe@1309 1117 }
bsw/jbe@1309 1118 }
bsw/jbe@1309 1119 }
bsw/jbe@1309 1120 return newNode;
bsw/jbe@1309 1121 }
bsw/jbe@1309 1122
bsw/jbe@1309 1123 function getDocument(node) {
bsw/jbe@1309 1124 if (node.nodeType == 9) {
bsw/jbe@1309 1125 return node;
bsw/jbe@1309 1126 } else if (typeof node.ownerDocument != UNDEF) {
bsw/jbe@1309 1127 return node.ownerDocument;
bsw/jbe@1309 1128 } else if (typeof node.document != UNDEF) {
bsw/jbe@1309 1129 return node.document;
bsw/jbe@1309 1130 } else if (node.parentNode) {
bsw/jbe@1309 1131 return getDocument(node.parentNode);
bsw/jbe@1309 1132 } else {
bsw/jbe@1309 1133 throw module.createError("getDocument: no document found for node");
bsw/jbe@1309 1134 }
bsw/jbe@1309 1135 }
bsw/jbe@1309 1136
bsw/jbe@1309 1137 function getWindow(node) {
bsw/jbe@1309 1138 var doc = getDocument(node);
bsw/jbe@1309 1139 if (typeof doc.defaultView != UNDEF) {
bsw/jbe@1309 1140 return doc.defaultView;
bsw/jbe@1309 1141 } else if (typeof doc.parentWindow != UNDEF) {
bsw/jbe@1309 1142 return doc.parentWindow;
bsw/jbe@1309 1143 } else {
bsw/jbe@1309 1144 throw module.createError("Cannot get a window object for node");
bsw/jbe@1309 1145 }
bsw/jbe@1309 1146 }
bsw/jbe@1309 1147
bsw/jbe@1309 1148 function getIframeDocument(iframeEl) {
bsw/jbe@1309 1149 if (typeof iframeEl.contentDocument != UNDEF) {
bsw/jbe@1309 1150 return iframeEl.contentDocument;
bsw/jbe@1309 1151 } else if (typeof iframeEl.contentWindow != UNDEF) {
bsw/jbe@1309 1152 return iframeEl.contentWindow.document;
bsw/jbe@1309 1153 } else {
bsw/jbe@1309 1154 throw module.createError("getIframeDocument: No Document object found for iframe element");
bsw/jbe@1309 1155 }
bsw/jbe@1309 1156 }
bsw/jbe@1309 1157
bsw/jbe@1309 1158 function getIframeWindow(iframeEl) {
bsw/jbe@1309 1159 if (typeof iframeEl.contentWindow != UNDEF) {
bsw/jbe@1309 1160 return iframeEl.contentWindow;
bsw/jbe@1309 1161 } else if (typeof iframeEl.contentDocument != UNDEF) {
bsw/jbe@1309 1162 return iframeEl.contentDocument.defaultView;
bsw/jbe@1309 1163 } else {
bsw/jbe@1309 1164 throw module.createError("getIframeWindow: No Window object found for iframe element");
bsw/jbe@1309 1165 }
bsw/jbe@1309 1166 }
bsw/jbe@1309 1167
bsw/jbe@1309 1168 // This looks bad. Is it worth it?
bsw/jbe@1309 1169 function isWindow(obj) {
bsw/jbe@1309 1170 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
bsw/jbe@1309 1171 }
bsw/jbe@1309 1172
bsw/jbe@1309 1173 function getContentDocument(obj, module, methodName) {
bsw/jbe@1309 1174 var doc;
bsw/jbe@1309 1175
bsw/jbe@1309 1176 if (!obj) {
bsw/jbe@1309 1177 doc = document;
bsw/jbe@1309 1178 }
bsw/jbe@1309 1179
bsw/jbe@1309 1180 // Test if a DOM node has been passed and obtain a document object for it if so
bsw/jbe@1309 1181 else if (util.isHostProperty(obj, "nodeType")) {
bsw/jbe@1309 1182 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
bsw/jbe@1309 1183 getIframeDocument(obj) : getDocument(obj);
bsw/jbe@1309 1184 }
bsw/jbe@1309 1185
bsw/jbe@1309 1186 // Test if the doc parameter appears to be a Window object
bsw/jbe@1309 1187 else if (isWindow(obj)) {
bsw/jbe@1309 1188 doc = obj.document;
bsw/jbe@1309 1189 }
bsw/jbe@1309 1190
bsw/jbe@1309 1191 if (!doc) {
bsw/jbe@1309 1192 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
bsw/jbe@1309 1193 }
bsw/jbe@1309 1194
bsw/jbe@1309 1195 return doc;
bsw/jbe@1309 1196 }
bsw/jbe@1309 1197
bsw/jbe@1309 1198 function getRootContainer(node) {
bsw/jbe@1309 1199 var parent;
bsw/jbe@1309 1200 while ( (parent = node.parentNode) ) {
bsw/jbe@1309 1201 node = parent;
bsw/jbe@1309 1202 }
bsw/jbe@1309 1203 return node;
bsw/jbe@1309 1204 }
bsw/jbe@1309 1205
bsw/jbe@1309 1206 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
bsw/jbe@1309 1207 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
bsw/jbe@1309 1208 var nodeC, root, childA, childB, n;
bsw/jbe@1309 1209 if (nodeA == nodeB) {
bsw/jbe@1309 1210 // Case 1: nodes are the same
bsw/jbe@1309 1211 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
bsw/jbe@1309 1212 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
bsw/jbe@1309 1213 // Case 2: node C (container B or an ancestor) is a child node of A
bsw/jbe@1309 1214 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
bsw/jbe@1309 1215 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
bsw/jbe@1309 1216 // Case 3: node C (container A or an ancestor) is a child node of B
bsw/jbe@1309 1217 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
bsw/jbe@1309 1218 } else {
bsw/jbe@1309 1219 root = getCommonAncestor(nodeA, nodeB);
bsw/jbe@1309 1220 if (!root) {
bsw/jbe@1309 1221 throw new Error("comparePoints error: nodes have no common ancestor");
bsw/jbe@1309 1222 }
bsw/jbe@1309 1223
bsw/jbe@1309 1224 // Case 4: containers are siblings or descendants of siblings
bsw/jbe@1309 1225 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
bsw/jbe@1309 1226 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
bsw/jbe@1309 1227
bsw/jbe@1309 1228 if (childA === childB) {
bsw/jbe@1309 1229 // This shouldn't be possible
bsw/jbe@1309 1230 throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
bsw/jbe@1309 1231 } else {
bsw/jbe@1309 1232 n = root.firstChild;
bsw/jbe@1309 1233 while (n) {
bsw/jbe@1309 1234 if (n === childA) {
bsw/jbe@1309 1235 return -1;
bsw/jbe@1309 1236 } else if (n === childB) {
bsw/jbe@1309 1237 return 1;
bsw/jbe@1309 1238 }
bsw/jbe@1309 1239 n = n.nextSibling;
bsw/jbe@1309 1240 }
bsw/jbe@1309 1241 }
bsw/jbe@1309 1242 }
bsw/jbe@1309 1243 }
bsw/jbe@1309 1244
bsw/jbe@1309 1245 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1246
bsw/jbe@1309 1247 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
bsw/jbe@1309 1248 var crashyTextNodes = false;
bsw/jbe@1309 1249
bsw/jbe@1309 1250 function isBrokenNode(node) {
bsw/jbe@1309 1251 var n;
bsw/jbe@1309 1252 try {
bsw/jbe@1309 1253 n = node.parentNode;
bsw/jbe@1309 1254 return false;
bsw/jbe@1309 1255 } catch (e) {
bsw/jbe@1309 1256 return true;
bsw/jbe@1309 1257 }
bsw/jbe@1309 1258 }
bsw/jbe@1309 1259
bsw/jbe@1309 1260 (function() {
bsw/jbe@1309 1261 var el = document.createElement("b");
bsw/jbe@1309 1262 el.innerHTML = "1";
bsw/jbe@1309 1263 var textNode = el.firstChild;
bsw/jbe@1309 1264 el.innerHTML = "<br />";
bsw/jbe@1309 1265 crashyTextNodes = isBrokenNode(textNode);
bsw/jbe@1309 1266
bsw/jbe@1309 1267 api.features.crashyTextNodes = crashyTextNodes;
bsw/jbe@1309 1268 })();
bsw/jbe@1309 1269
bsw/jbe@1309 1270 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1271
bsw/jbe@1309 1272 function inspectNode(node) {
bsw/jbe@1309 1273 if (!node) {
bsw/jbe@1309 1274 return "[No node]";
bsw/jbe@1309 1275 }
bsw/jbe@1309 1276 if (crashyTextNodes && isBrokenNode(node)) {
bsw/jbe@1309 1277 return "[Broken node]";
bsw/jbe@1309 1278 }
bsw/jbe@1309 1279 if (isCharacterDataNode(node)) {
bsw/jbe@1309 1280 return '"' + node.data + '"';
bsw/jbe@1309 1281 }
bsw/jbe@1309 1282 if (node.nodeType == 1) {
bsw/jbe@1309 1283 var idAttr = node.id ? ' id="' + node.id + '"' : "";
bsw/jbe@1309 1284 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
bsw/jbe@1309 1285 }
bsw/jbe@1309 1286 return node.nodeName;
bsw/jbe@1309 1287 }
bsw/jbe@1309 1288
bsw/jbe@1309 1289 function fragmentFromNodeChildren(node) {
bsw/jbe@1309 1290 var fragment = getDocument(node).createDocumentFragment(), child;
bsw/jbe@1309 1291 while ( (child = node.firstChild) ) {
bsw/jbe@1309 1292 fragment.appendChild(child);
bsw/jbe@1309 1293 }
bsw/jbe@1309 1294 return fragment;
bsw/jbe@1309 1295 }
bsw/jbe@1309 1296
bsw/jbe@1309 1297 var getComputedStyleProperty;
bsw/jbe@1309 1298 if (typeof window.getComputedStyle != UNDEF) {
bsw/jbe@1309 1299 getComputedStyleProperty = function(el, propName) {
bsw/jbe@1309 1300 return getWindow(el).getComputedStyle(el, null)[propName];
bsw/jbe@1309 1301 };
bsw/jbe@1309 1302 } else if (typeof document.documentElement.currentStyle != UNDEF) {
bsw/jbe@1309 1303 getComputedStyleProperty = function(el, propName) {
bsw/jbe@1309 1304 return el.currentStyle ? el.currentStyle[propName] : "";
bsw/jbe@1309 1305 };
bsw/jbe@1309 1306 } else {
bsw/jbe@1309 1307 module.fail("No means of obtaining computed style properties found");
bsw/jbe@1309 1308 }
bsw/jbe@1309 1309
bsw/jbe@1309 1310 function createTestElement(doc, html, contentEditable) {
bsw/jbe@1309 1311 var body = getBody(doc);
bsw/jbe@1309 1312 var el = doc.createElement("div");
bsw/jbe@1309 1313 el.contentEditable = "" + !!contentEditable;
bsw/jbe@1309 1314 if (html) {
bsw/jbe@1309 1315 el.innerHTML = html;
bsw/jbe@1309 1316 }
bsw/jbe@1309 1317
bsw/jbe@1309 1318 // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
bsw/jbe@1309 1319 var bodyFirstChild = body.firstChild;
bsw/jbe@1309 1320 if (bodyFirstChild) {
bsw/jbe@1309 1321 body.insertBefore(el, bodyFirstChild);
bsw/jbe@1309 1322 } else {
bsw/jbe@1309 1323 body.appendChild(el);
bsw/jbe@1309 1324 }
bsw/jbe@1309 1325
bsw/jbe@1309 1326 return el;
bsw/jbe@1309 1327 }
bsw/jbe@1309 1328
bsw/jbe@1309 1329 function removeNode(node) {
bsw/jbe@1309 1330 return node.parentNode.removeChild(node);
bsw/jbe@1309 1331 }
bsw/jbe@1309 1332
bsw/jbe@1309 1333 function NodeIterator(root) {
bsw/jbe@1309 1334 this.root = root;
bsw/jbe@1309 1335 this._next = root;
bsw/jbe@1309 1336 }
bsw/jbe@1309 1337
bsw/jbe@1309 1338 NodeIterator.prototype = {
bsw/jbe@1309 1339 _current: null,
bsw/jbe@1309 1340
bsw/jbe@1309 1341 hasNext: function() {
bsw/jbe@1309 1342 return !!this._next;
bsw/jbe@1309 1343 },
bsw/jbe@1309 1344
bsw/jbe@1309 1345 next: function() {
bsw/jbe@1309 1346 var n = this._current = this._next;
bsw/jbe@1309 1347 var child, next;
bsw/jbe@1309 1348 if (this._current) {
bsw/jbe@1309 1349 child = n.firstChild;
bsw/jbe@1309 1350 if (child) {
bsw/jbe@1309 1351 this._next = child;
bsw/jbe@1309 1352 } else {
bsw/jbe@1309 1353 next = null;
bsw/jbe@1309 1354 while ((n !== this.root) && !(next = n.nextSibling)) {
bsw/jbe@1309 1355 n = n.parentNode;
bsw/jbe@1309 1356 }
bsw/jbe@1309 1357 this._next = next;
bsw/jbe@1309 1358 }
bsw/jbe@1309 1359 }
bsw/jbe@1309 1360 return this._current;
bsw/jbe@1309 1361 },
bsw/jbe@1309 1362
bsw/jbe@1309 1363 detach: function() {
bsw/jbe@1309 1364 this._current = this._next = this.root = null;
bsw/jbe@1309 1365 }
bsw/jbe@1309 1366 };
bsw/jbe@1309 1367
bsw/jbe@1309 1368 function createIterator(root) {
bsw/jbe@1309 1369 return new NodeIterator(root);
bsw/jbe@1309 1370 }
bsw/jbe@1309 1371
bsw/jbe@1309 1372 function DomPosition(node, offset) {
bsw/jbe@1309 1373 this.node = node;
bsw/jbe@1309 1374 this.offset = offset;
bsw/jbe@1309 1375 }
bsw/jbe@1309 1376
bsw/jbe@1309 1377 DomPosition.prototype = {
bsw/jbe@1309 1378 equals: function(pos) {
bsw/jbe@1309 1379 return !!pos && this.node === pos.node && this.offset == pos.offset;
bsw/jbe@1309 1380 },
bsw/jbe@1309 1381
bsw/jbe@1309 1382 inspect: function() {
bsw/jbe@1309 1383 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
bsw/jbe@1309 1384 },
bsw/jbe@1309 1385
bsw/jbe@1309 1386 toString: function() {
bsw/jbe@1309 1387 return this.inspect();
bsw/jbe@1309 1388 }
bsw/jbe@1309 1389 };
bsw/jbe@1309 1390
bsw/jbe@1309 1391 function DOMException(codeName) {
bsw/jbe@1309 1392 this.code = this[codeName];
bsw/jbe@1309 1393 this.codeName = codeName;
bsw/jbe@1309 1394 this.message = "DOMException: " + this.codeName;
bsw/jbe@1309 1395 }
bsw/jbe@1309 1396
bsw/jbe@1309 1397 DOMException.prototype = {
bsw/jbe@1309 1398 INDEX_SIZE_ERR: 1,
bsw/jbe@1309 1399 HIERARCHY_REQUEST_ERR: 3,
bsw/jbe@1309 1400 WRONG_DOCUMENT_ERR: 4,
bsw/jbe@1309 1401 NO_MODIFICATION_ALLOWED_ERR: 7,
bsw/jbe@1309 1402 NOT_FOUND_ERR: 8,
bsw/jbe@1309 1403 NOT_SUPPORTED_ERR: 9,
bsw/jbe@1309 1404 INVALID_STATE_ERR: 11,
bsw/jbe@1309 1405 INVALID_NODE_TYPE_ERR: 24
bsw/jbe@1309 1406 };
bsw/jbe@1309 1407
bsw/jbe@1309 1408 DOMException.prototype.toString = function() {
bsw/jbe@1309 1409 return this.message;
bsw/jbe@1309 1410 };
bsw/jbe@1309 1411
bsw/jbe@1309 1412 api.dom = {
bsw/jbe@1309 1413 arrayContains: arrayContains,
bsw/jbe@1309 1414 isHtmlNamespace: isHtmlNamespace,
bsw/jbe@1309 1415 parentElement: parentElement,
bsw/jbe@1309 1416 getNodeIndex: getNodeIndex,
bsw/jbe@1309 1417 getNodeLength: getNodeLength,
bsw/jbe@1309 1418 getCommonAncestor: getCommonAncestor,
bsw/jbe@1309 1419 isAncestorOf: isAncestorOf,
bsw/jbe@1309 1420 isOrIsAncestorOf: isOrIsAncestorOf,
bsw/jbe@1309 1421 getClosestAncestorIn: getClosestAncestorIn,
bsw/jbe@1309 1422 isCharacterDataNode: isCharacterDataNode,
bsw/jbe@1309 1423 isTextOrCommentNode: isTextOrCommentNode,
bsw/jbe@1309 1424 insertAfter: insertAfter,
bsw/jbe@1309 1425 splitDataNode: splitDataNode,
bsw/jbe@1309 1426 getDocument: getDocument,
bsw/jbe@1309 1427 getWindow: getWindow,
bsw/jbe@1309 1428 getIframeWindow: getIframeWindow,
bsw/jbe@1309 1429 getIframeDocument: getIframeDocument,
bsw/jbe@1309 1430 getBody: getBody,
bsw/jbe@1309 1431 isWindow: isWindow,
bsw/jbe@1309 1432 getContentDocument: getContentDocument,
bsw/jbe@1309 1433 getRootContainer: getRootContainer,
bsw/jbe@1309 1434 comparePoints: comparePoints,
bsw/jbe@1309 1435 isBrokenNode: isBrokenNode,
bsw/jbe@1309 1436 inspectNode: inspectNode,
bsw/jbe@1309 1437 getComputedStyleProperty: getComputedStyleProperty,
bsw/jbe@1309 1438 createTestElement: createTestElement,
bsw/jbe@1309 1439 removeNode: removeNode,
bsw/jbe@1309 1440 fragmentFromNodeChildren: fragmentFromNodeChildren,
bsw/jbe@1309 1441 createIterator: createIterator,
bsw/jbe@1309 1442 DomPosition: DomPosition
bsw/jbe@1309 1443 };
bsw/jbe@1309 1444
bsw/jbe@1309 1445 api.DOMException = DOMException;
bsw/jbe@1309 1446 });
bsw/jbe@1309 1447
bsw/jbe@1309 1448 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1449
bsw/jbe@1309 1450 // Pure JavaScript implementation of DOM Range
bsw/jbe@1309 1451 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
bsw/jbe@1309 1452 var dom = api.dom;
bsw/jbe@1309 1453 var util = api.util;
bsw/jbe@1309 1454 var DomPosition = dom.DomPosition;
bsw/jbe@1309 1455 var DOMException = api.DOMException;
bsw/jbe@1309 1456
bsw/jbe@1309 1457 var isCharacterDataNode = dom.isCharacterDataNode;
bsw/jbe@1309 1458 var getNodeIndex = dom.getNodeIndex;
bsw/jbe@1309 1459 var isOrIsAncestorOf = dom.isOrIsAncestorOf;
bsw/jbe@1309 1460 var getDocument = dom.getDocument;
bsw/jbe@1309 1461 var comparePoints = dom.comparePoints;
bsw/jbe@1309 1462 var splitDataNode = dom.splitDataNode;
bsw/jbe@1309 1463 var getClosestAncestorIn = dom.getClosestAncestorIn;
bsw/jbe@1309 1464 var getNodeLength = dom.getNodeLength;
bsw/jbe@1309 1465 var arrayContains = dom.arrayContains;
bsw/jbe@1309 1466 var getRootContainer = dom.getRootContainer;
bsw/jbe@1309 1467 var crashyTextNodes = api.features.crashyTextNodes;
bsw/jbe@1309 1468
bsw/jbe@1309 1469 var removeNode = dom.removeNode;
bsw/jbe@1309 1470
bsw/jbe@1309 1471 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1472
bsw/jbe@1309 1473 // Utility functions
bsw/jbe@1309 1474
bsw/jbe@1309 1475 function isNonTextPartiallySelected(node, range) {
bsw/jbe@1309 1476 return (node.nodeType != 3) &&
bsw/jbe@1309 1477 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
bsw/jbe@1309 1478 }
bsw/jbe@1309 1479
bsw/jbe@1309 1480 function getRangeDocument(range) {
bsw/jbe@1309 1481 return range.document || getDocument(range.startContainer);
bsw/jbe@1309 1482 }
bsw/jbe@1309 1483
bsw/jbe@1309 1484 function getRangeRoot(range) {
bsw/jbe@1309 1485 return getRootContainer(range.startContainer);
bsw/jbe@1309 1486 }
bsw/jbe@1309 1487
bsw/jbe@1309 1488 function getBoundaryBeforeNode(node) {
bsw/jbe@1309 1489 return new DomPosition(node.parentNode, getNodeIndex(node));
bsw/jbe@1309 1490 }
bsw/jbe@1309 1491
bsw/jbe@1309 1492 function getBoundaryAfterNode(node) {
bsw/jbe@1309 1493 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
bsw/jbe@1309 1494 }
bsw/jbe@1309 1495
bsw/jbe@1309 1496 function insertNodeAtPosition(node, n, o) {
bsw/jbe@1309 1497 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
bsw/jbe@1309 1498 if (isCharacterDataNode(n)) {
bsw/jbe@1309 1499 if (o == n.length) {
bsw/jbe@1309 1500 dom.insertAfter(node, n);
bsw/jbe@1309 1501 } else {
bsw/jbe@1309 1502 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
bsw/jbe@1309 1503 }
bsw/jbe@1309 1504 } else if (o >= n.childNodes.length) {
bsw/jbe@1309 1505 n.appendChild(node);
bsw/jbe@1309 1506 } else {
bsw/jbe@1309 1507 n.insertBefore(node, n.childNodes[o]);
bsw/jbe@1309 1508 }
bsw/jbe@1309 1509 return firstNodeInserted;
bsw/jbe@1309 1510 }
bsw/jbe@1309 1511
bsw/jbe@1309 1512 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
bsw/jbe@1309 1513 assertRangeValid(rangeA);
bsw/jbe@1309 1514 assertRangeValid(rangeB);
bsw/jbe@1309 1515
bsw/jbe@1309 1516 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
bsw/jbe@1309 1517 throw new DOMException("WRONG_DOCUMENT_ERR");
bsw/jbe@1309 1518 }
bsw/jbe@1309 1519
bsw/jbe@1309 1520 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
bsw/jbe@1309 1521 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
bsw/jbe@1309 1522
bsw/jbe@1309 1523 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
bsw/jbe@1309 1524 }
bsw/jbe@1309 1525
bsw/jbe@1309 1526 function cloneSubtree(iterator) {
bsw/jbe@1309 1527 var partiallySelected;
bsw/jbe@1309 1528 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
bsw/jbe@1309 1529 partiallySelected = iterator.isPartiallySelectedSubtree();
bsw/jbe@1309 1530 node = node.cloneNode(!partiallySelected);
bsw/jbe@1309 1531 if (partiallySelected) {
bsw/jbe@1309 1532 subIterator = iterator.getSubtreeIterator();
bsw/jbe@1309 1533 node.appendChild(cloneSubtree(subIterator));
bsw/jbe@1309 1534 subIterator.detach();
bsw/jbe@1309 1535 }
bsw/jbe@1309 1536
bsw/jbe@1309 1537 if (node.nodeType == 10) { // DocumentType
bsw/jbe@1309 1538 throw new DOMException("HIERARCHY_REQUEST_ERR");
bsw/jbe@1309 1539 }
bsw/jbe@1309 1540 frag.appendChild(node);
bsw/jbe@1309 1541 }
bsw/jbe@1309 1542 return frag;
bsw/jbe@1309 1543 }
bsw/jbe@1309 1544
bsw/jbe@1309 1545 function iterateSubtree(rangeIterator, func, iteratorState) {
bsw/jbe@1309 1546 var it, n;
bsw/jbe@1309 1547 iteratorState = iteratorState || { stop: false };
bsw/jbe@1309 1548 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
bsw/jbe@1309 1549 if (rangeIterator.isPartiallySelectedSubtree()) {
bsw/jbe@1309 1550 if (func(node) === false) {
bsw/jbe@1309 1551 iteratorState.stop = true;
bsw/jbe@1309 1552 return;
bsw/jbe@1309 1553 } else {
bsw/jbe@1309 1554 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
bsw/jbe@1309 1555 // the node selected by the Range.
bsw/jbe@1309 1556 subRangeIterator = rangeIterator.getSubtreeIterator();
bsw/jbe@1309 1557 iterateSubtree(subRangeIterator, func, iteratorState);
bsw/jbe@1309 1558 subRangeIterator.detach();
bsw/jbe@1309 1559 if (iteratorState.stop) {
bsw/jbe@1309 1560 return;
bsw/jbe@1309 1561 }
bsw/jbe@1309 1562 }
bsw/jbe@1309 1563 } else {
bsw/jbe@1309 1564 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
bsw/jbe@1309 1565 // descendants
bsw/jbe@1309 1566 it = dom.createIterator(node);
bsw/jbe@1309 1567 while ( (n = it.next()) ) {
bsw/jbe@1309 1568 if (func(n) === false) {
bsw/jbe@1309 1569 iteratorState.stop = true;
bsw/jbe@1309 1570 return;
bsw/jbe@1309 1571 }
bsw/jbe@1309 1572 }
bsw/jbe@1309 1573 }
bsw/jbe@1309 1574 }
bsw/jbe@1309 1575 }
bsw/jbe@1309 1576
bsw/jbe@1309 1577 function deleteSubtree(iterator) {
bsw/jbe@1309 1578 var subIterator;
bsw/jbe@1309 1579 while (iterator.next()) {
bsw/jbe@1309 1580 if (iterator.isPartiallySelectedSubtree()) {
bsw/jbe@1309 1581 subIterator = iterator.getSubtreeIterator();
bsw/jbe@1309 1582 deleteSubtree(subIterator);
bsw/jbe@1309 1583 subIterator.detach();
bsw/jbe@1309 1584 } else {
bsw/jbe@1309 1585 iterator.remove();
bsw/jbe@1309 1586 }
bsw/jbe@1309 1587 }
bsw/jbe@1309 1588 }
bsw/jbe@1309 1589
bsw/jbe@1309 1590 function extractSubtree(iterator) {
bsw/jbe@1309 1591 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
bsw/jbe@1309 1592
bsw/jbe@1309 1593 if (iterator.isPartiallySelectedSubtree()) {
bsw/jbe@1309 1594 node = node.cloneNode(false);
bsw/jbe@1309 1595 subIterator = iterator.getSubtreeIterator();
bsw/jbe@1309 1596 node.appendChild(extractSubtree(subIterator));
bsw/jbe@1309 1597 subIterator.detach();
bsw/jbe@1309 1598 } else {
bsw/jbe@1309 1599 iterator.remove();
bsw/jbe@1309 1600 }
bsw/jbe@1309 1601 if (node.nodeType == 10) { // DocumentType
bsw/jbe@1309 1602 throw new DOMException("HIERARCHY_REQUEST_ERR");
bsw/jbe@1309 1603 }
bsw/jbe@1309 1604 frag.appendChild(node);
bsw/jbe@1309 1605 }
bsw/jbe@1309 1606 return frag;
bsw/jbe@1309 1607 }
bsw/jbe@1309 1608
bsw/jbe@1309 1609 function getNodesInRange(range, nodeTypes, filter) {
bsw/jbe@1309 1610 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
bsw/jbe@1309 1611 var filterExists = !!filter;
bsw/jbe@1309 1612 if (filterNodeTypes) {
bsw/jbe@1309 1613 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
bsw/jbe@1309 1614 }
bsw/jbe@1309 1615
bsw/jbe@1309 1616 var nodes = [];
bsw/jbe@1309 1617 iterateSubtree(new RangeIterator(range, false), function(node) {
bsw/jbe@1309 1618 if (filterNodeTypes && !regex.test(node.nodeType)) {
bsw/jbe@1309 1619 return;
bsw/jbe@1309 1620 }
bsw/jbe@1309 1621 if (filterExists && !filter(node)) {
bsw/jbe@1309 1622 return;
bsw/jbe@1309 1623 }
bsw/jbe@1309 1624 // Don't include a boundary container if it is a character data node and the range does not contain any
bsw/jbe@1309 1625 // of its character data. See issue 190.
bsw/jbe@1309 1626 var sc = range.startContainer;
bsw/jbe@1309 1627 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
bsw/jbe@1309 1628 return;
bsw/jbe@1309 1629 }
bsw/jbe@1309 1630
bsw/jbe@1309 1631 var ec = range.endContainer;
bsw/jbe@1309 1632 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
bsw/jbe@1309 1633 return;
bsw/jbe@1309 1634 }
bsw/jbe@1309 1635
bsw/jbe@1309 1636 nodes.push(node);
bsw/jbe@1309 1637 });
bsw/jbe@1309 1638 return nodes;
bsw/jbe@1309 1639 }
bsw/jbe@1309 1640
bsw/jbe@1309 1641 function inspect(range) {
bsw/jbe@1309 1642 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
bsw/jbe@1309 1643 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
bsw/jbe@1309 1644 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
bsw/jbe@1309 1645 }
bsw/jbe@1309 1646
bsw/jbe@1309 1647 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1648
bsw/jbe@1309 1649 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
bsw/jbe@1309 1650
bsw/jbe@1309 1651 function RangeIterator(range, clonePartiallySelectedTextNodes) {
bsw/jbe@1309 1652 this.range = range;
bsw/jbe@1309 1653 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
bsw/jbe@1309 1654
bsw/jbe@1309 1655
bsw/jbe@1309 1656 if (!range.collapsed) {
bsw/jbe@1309 1657 this.sc = range.startContainer;
bsw/jbe@1309 1658 this.so = range.startOffset;
bsw/jbe@1309 1659 this.ec = range.endContainer;
bsw/jbe@1309 1660 this.eo = range.endOffset;
bsw/jbe@1309 1661 var root = range.commonAncestorContainer;
bsw/jbe@1309 1662
bsw/jbe@1309 1663 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
bsw/jbe@1309 1664 this.isSingleCharacterDataNode = true;
bsw/jbe@1309 1665 this._first = this._last = this._next = this.sc;
bsw/jbe@1309 1666 } else {
bsw/jbe@1309 1667 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
bsw/jbe@1309 1668 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
bsw/jbe@1309 1669 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
bsw/jbe@1309 1670 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
bsw/jbe@1309 1671 }
bsw/jbe@1309 1672 }
bsw/jbe@1309 1673 }
bsw/jbe@1309 1674
bsw/jbe@1309 1675 RangeIterator.prototype = {
bsw/jbe@1309 1676 _current: null,
bsw/jbe@1309 1677 _next: null,
bsw/jbe@1309 1678 _first: null,
bsw/jbe@1309 1679 _last: null,
bsw/jbe@1309 1680 isSingleCharacterDataNode: false,
bsw/jbe@1309 1681
bsw/jbe@1309 1682 reset: function() {
bsw/jbe@1309 1683 this._current = null;
bsw/jbe@1309 1684 this._next = this._first;
bsw/jbe@1309 1685 },
bsw/jbe@1309 1686
bsw/jbe@1309 1687 hasNext: function() {
bsw/jbe@1309 1688 return !!this._next;
bsw/jbe@1309 1689 },
bsw/jbe@1309 1690
bsw/jbe@1309 1691 next: function() {
bsw/jbe@1309 1692 // Move to next node
bsw/jbe@1309 1693 var current = this._current = this._next;
bsw/jbe@1309 1694 if (current) {
bsw/jbe@1309 1695 this._next = (current !== this._last) ? current.nextSibling : null;
bsw/jbe@1309 1696
bsw/jbe@1309 1697 // Check for partially selected text nodes
bsw/jbe@1309 1698 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
bsw/jbe@1309 1699 if (current === this.ec) {
bsw/jbe@1309 1700 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
bsw/jbe@1309 1701 }
bsw/jbe@1309 1702 if (this._current === this.sc) {
bsw/jbe@1309 1703 (current = current.cloneNode(true)).deleteData(0, this.so);
bsw/jbe@1309 1704 }
bsw/jbe@1309 1705 }
bsw/jbe@1309 1706 }
bsw/jbe@1309 1707
bsw/jbe@1309 1708 return current;
bsw/jbe@1309 1709 },
bsw/jbe@1309 1710
bsw/jbe@1309 1711 remove: function() {
bsw/jbe@1309 1712 var current = this._current, start, end;
bsw/jbe@1309 1713
bsw/jbe@1309 1714 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
bsw/jbe@1309 1715 start = (current === this.sc) ? this.so : 0;
bsw/jbe@1309 1716 end = (current === this.ec) ? this.eo : current.length;
bsw/jbe@1309 1717 if (start != end) {
bsw/jbe@1309 1718 current.deleteData(start, end - start);
bsw/jbe@1309 1719 }
bsw/jbe@1309 1720 } else {
bsw/jbe@1309 1721 if (current.parentNode) {
bsw/jbe@1309 1722 removeNode(current);
bsw/jbe@1309 1723 } else {
bsw/jbe@1309 1724 }
bsw/jbe@1309 1725 }
bsw/jbe@1309 1726 },
bsw/jbe@1309 1727
bsw/jbe@1309 1728 // Checks if the current node is partially selected
bsw/jbe@1309 1729 isPartiallySelectedSubtree: function() {
bsw/jbe@1309 1730 var current = this._current;
bsw/jbe@1309 1731 return isNonTextPartiallySelected(current, this.range);
bsw/jbe@1309 1732 },
bsw/jbe@1309 1733
bsw/jbe@1309 1734 getSubtreeIterator: function() {
bsw/jbe@1309 1735 var subRange;
bsw/jbe@1309 1736 if (this.isSingleCharacterDataNode) {
bsw/jbe@1309 1737 subRange = this.range.cloneRange();
bsw/jbe@1309 1738 subRange.collapse(false);
bsw/jbe@1309 1739 } else {
bsw/jbe@1309 1740 subRange = new Range(getRangeDocument(this.range));
bsw/jbe@1309 1741 var current = this._current;
bsw/jbe@1309 1742 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
bsw/jbe@1309 1743
bsw/jbe@1309 1744 if (isOrIsAncestorOf(current, this.sc)) {
bsw/jbe@1309 1745 startContainer = this.sc;
bsw/jbe@1309 1746 startOffset = this.so;
bsw/jbe@1309 1747 }
bsw/jbe@1309 1748 if (isOrIsAncestorOf(current, this.ec)) {
bsw/jbe@1309 1749 endContainer = this.ec;
bsw/jbe@1309 1750 endOffset = this.eo;
bsw/jbe@1309 1751 }
bsw/jbe@1309 1752
bsw/jbe@1309 1753 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
bsw/jbe@1309 1754 }
bsw/jbe@1309 1755 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
bsw/jbe@1309 1756 },
bsw/jbe@1309 1757
bsw/jbe@1309 1758 detach: function() {
bsw/jbe@1309 1759 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
bsw/jbe@1309 1760 }
bsw/jbe@1309 1761 };
bsw/jbe@1309 1762
bsw/jbe@1309 1763 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1764
bsw/jbe@1309 1765 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
bsw/jbe@1309 1766 var rootContainerNodeTypes = [2, 9, 11];
bsw/jbe@1309 1767 var readonlyNodeTypes = [5, 6, 10, 12];
bsw/jbe@1309 1768 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
bsw/jbe@1309 1769 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
bsw/jbe@1309 1770
bsw/jbe@1309 1771 function createAncestorFinder(nodeTypes) {
bsw/jbe@1309 1772 return function(node, selfIsAncestor) {
bsw/jbe@1309 1773 var t, n = selfIsAncestor ? node : node.parentNode;
bsw/jbe@1309 1774 while (n) {
bsw/jbe@1309 1775 t = n.nodeType;
bsw/jbe@1309 1776 if (arrayContains(nodeTypes, t)) {
bsw/jbe@1309 1777 return n;
bsw/jbe@1309 1778 }
bsw/jbe@1309 1779 n = n.parentNode;
bsw/jbe@1309 1780 }
bsw/jbe@1309 1781 return null;
bsw/jbe@1309 1782 };
bsw/jbe@1309 1783 }
bsw/jbe@1309 1784
bsw/jbe@1309 1785 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
bsw/jbe@1309 1786 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
bsw/jbe@1309 1787 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
bsw/jbe@1309 1788
bsw/jbe@1309 1789 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
bsw/jbe@1309 1790 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
bsw/jbe@1309 1791 throw new DOMException("INVALID_NODE_TYPE_ERR");
bsw/jbe@1309 1792 }
bsw/jbe@1309 1793 }
bsw/jbe@1309 1794
bsw/jbe@1309 1795 function assertValidNodeType(node, invalidTypes) {
bsw/jbe@1309 1796 if (!arrayContains(invalidTypes, node.nodeType)) {
bsw/jbe@1309 1797 throw new DOMException("INVALID_NODE_TYPE_ERR");
bsw/jbe@1309 1798 }
bsw/jbe@1309 1799 }
bsw/jbe@1309 1800
bsw/jbe@1309 1801 function assertValidOffset(node, offset) {
bsw/jbe@1309 1802 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
bsw/jbe@1309 1803 throw new DOMException("INDEX_SIZE_ERR");
bsw/jbe@1309 1804 }
bsw/jbe@1309 1805 }
bsw/jbe@1309 1806
bsw/jbe@1309 1807 function assertSameDocumentOrFragment(node1, node2) {
bsw/jbe@1309 1808 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
bsw/jbe@1309 1809 throw new DOMException("WRONG_DOCUMENT_ERR");
bsw/jbe@1309 1810 }
bsw/jbe@1309 1811 }
bsw/jbe@1309 1812
bsw/jbe@1309 1813 function assertNodeNotReadOnly(node) {
bsw/jbe@1309 1814 if (getReadonlyAncestor(node, true)) {
bsw/jbe@1309 1815 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
bsw/jbe@1309 1816 }
bsw/jbe@1309 1817 }
bsw/jbe@1309 1818
bsw/jbe@1309 1819 function assertNode(node, codeName) {
bsw/jbe@1309 1820 if (!node) {
bsw/jbe@1309 1821 throw new DOMException(codeName);
bsw/jbe@1309 1822 }
bsw/jbe@1309 1823 }
bsw/jbe@1309 1824
bsw/jbe@1309 1825 function isValidOffset(node, offset) {
bsw/jbe@1309 1826 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
bsw/jbe@1309 1827 }
bsw/jbe@1309 1828
bsw/jbe@1309 1829 function isRangeValid(range) {
bsw/jbe@1309 1830 return (!!range.startContainer && !!range.endContainer &&
bsw/jbe@1309 1831 !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
bsw/jbe@1309 1832 getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
bsw/jbe@1309 1833 isValidOffset(range.startContainer, range.startOffset) &&
bsw/jbe@1309 1834 isValidOffset(range.endContainer, range.endOffset));
bsw/jbe@1309 1835 }
bsw/jbe@1309 1836
bsw/jbe@1309 1837 function assertRangeValid(range) {
bsw/jbe@1309 1838 if (!isRangeValid(range)) {
bsw/jbe@1309 1839 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
bsw/jbe@1309 1840 }
bsw/jbe@1309 1841 }
bsw/jbe@1309 1842
bsw/jbe@1309 1843 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1844
bsw/jbe@1309 1845 // Test the browser's innerHTML support to decide how to implement createContextualFragment
bsw/jbe@1309 1846 var styleEl = document.createElement("style");
bsw/jbe@1309 1847 var htmlParsingConforms = false;
bsw/jbe@1309 1848 try {
bsw/jbe@1309 1849 styleEl.innerHTML = "<b>x</b>";
bsw/jbe@1309 1850 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
bsw/jbe@1309 1851 } catch (e) {
bsw/jbe@1309 1852 // IE 6 and 7 throw
bsw/jbe@1309 1853 }
bsw/jbe@1309 1854
bsw/jbe@1309 1855 api.features.htmlParsingConforms = htmlParsingConforms;
bsw/jbe@1309 1856
bsw/jbe@1309 1857 var createContextualFragment = htmlParsingConforms ?
bsw/jbe@1309 1858
bsw/jbe@1309 1859 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
bsw/jbe@1309 1860 // discussion and base code for this implementation at issue 67.
bsw/jbe@1309 1861 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
bsw/jbe@1309 1862 // Thanks to Aleks Williams.
bsw/jbe@1309 1863 function(fragmentStr) {
bsw/jbe@1309 1864 // "Let node the context object's start's node."
bsw/jbe@1309 1865 var node = this.startContainer;
bsw/jbe@1309 1866 var doc = getDocument(node);
bsw/jbe@1309 1867
bsw/jbe@1309 1868 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
bsw/jbe@1309 1869 // exception and abort these steps."
bsw/jbe@1309 1870 if (!node) {
bsw/jbe@1309 1871 throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309 1872 }
bsw/jbe@1309 1873
bsw/jbe@1309 1874 // "Let element be as follows, depending on node's interface:"
bsw/jbe@1309 1875 // Document, Document Fragment: null
bsw/jbe@1309 1876 var el = null;
bsw/jbe@1309 1877
bsw/jbe@1309 1878 // "Element: node"
bsw/jbe@1309 1879 if (node.nodeType == 1) {
bsw/jbe@1309 1880 el = node;
bsw/jbe@1309 1881
bsw/jbe@1309 1882 // "Text, Comment: node's parentElement"
bsw/jbe@1309 1883 } else if (isCharacterDataNode(node)) {
bsw/jbe@1309 1884 el = dom.parentElement(node);
bsw/jbe@1309 1885 }
bsw/jbe@1309 1886
bsw/jbe@1309 1887 // "If either element is null or element's ownerDocument is an HTML document
bsw/jbe@1309 1888 // and element's local name is "html" and element's namespace is the HTML
bsw/jbe@1309 1889 // namespace"
bsw/jbe@1309 1890 if (el === null || (
bsw/jbe@1309 1891 el.nodeName == "HTML" &&
bsw/jbe@1309 1892 dom.isHtmlNamespace(getDocument(el).documentElement) &&
bsw/jbe@1309 1893 dom.isHtmlNamespace(el)
bsw/jbe@1309 1894 )) {
bsw/jbe@1309 1895
bsw/jbe@1309 1896 // "let element be a new Element with "body" as its local name and the HTML
bsw/jbe@1309 1897 // namespace as its namespace.""
bsw/jbe@1309 1898 el = doc.createElement("body");
bsw/jbe@1309 1899 } else {
bsw/jbe@1309 1900 el = el.cloneNode(false);
bsw/jbe@1309 1901 }
bsw/jbe@1309 1902
bsw/jbe@1309 1903 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
bsw/jbe@1309 1904 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
bsw/jbe@1309 1905 // "In either case, the algorithm must be invoked with fragment as the input
bsw/jbe@1309 1906 // and element as the context element."
bsw/jbe@1309 1907 el.innerHTML = fragmentStr;
bsw/jbe@1309 1908
bsw/jbe@1309 1909 // "If this raises an exception, then abort these steps. Otherwise, let new
bsw/jbe@1309 1910 // children be the nodes returned."
bsw/jbe@1309 1911
bsw/jbe@1309 1912 // "Let fragment be a new DocumentFragment."
bsw/jbe@1309 1913 // "Append all new children to fragment."
bsw/jbe@1309 1914 // "Return fragment."
bsw/jbe@1309 1915 return dom.fragmentFromNodeChildren(el);
bsw/jbe@1309 1916 } :
bsw/jbe@1309 1917
bsw/jbe@1309 1918 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
bsw/jbe@1309 1919 // previous versions of Rangy used (with the exception of using a body element rather than a div)
bsw/jbe@1309 1920 function(fragmentStr) {
bsw/jbe@1309 1921 var doc = getRangeDocument(this);
bsw/jbe@1309 1922 var el = doc.createElement("body");
bsw/jbe@1309 1923 el.innerHTML = fragmentStr;
bsw/jbe@1309 1924
bsw/jbe@1309 1925 return dom.fragmentFromNodeChildren(el);
bsw/jbe@1309 1926 };
bsw/jbe@1309 1927
bsw/jbe@1309 1928 function splitRangeBoundaries(range, positionsToPreserve) {
bsw/jbe@1309 1929 assertRangeValid(range);
bsw/jbe@1309 1930
bsw/jbe@1309 1931 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
bsw/jbe@1309 1932 var startEndSame = (sc === ec);
bsw/jbe@1309 1933
bsw/jbe@1309 1934 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
bsw/jbe@1309 1935 splitDataNode(ec, eo, positionsToPreserve);
bsw/jbe@1309 1936 }
bsw/jbe@1309 1937
bsw/jbe@1309 1938 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
bsw/jbe@1309 1939 sc = splitDataNode(sc, so, positionsToPreserve);
bsw/jbe@1309 1940 if (startEndSame) {
bsw/jbe@1309 1941 eo -= so;
bsw/jbe@1309 1942 ec = sc;
bsw/jbe@1309 1943 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
bsw/jbe@1309 1944 eo++;
bsw/jbe@1309 1945 }
bsw/jbe@1309 1946 so = 0;
bsw/jbe@1309 1947 }
bsw/jbe@1309 1948 range.setStartAndEnd(sc, so, ec, eo);
bsw/jbe@1309 1949 }
bsw/jbe@1309 1950
bsw/jbe@1309 1951 function rangeToHtml(range) {
bsw/jbe@1309 1952 assertRangeValid(range);
bsw/jbe@1309 1953 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
bsw/jbe@1309 1954 container.appendChild( range.cloneContents() );
bsw/jbe@1309 1955 return container.innerHTML;
bsw/jbe@1309 1956 }
bsw/jbe@1309 1957
bsw/jbe@1309 1958 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 1959
bsw/jbe@1309 1960 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
bsw/jbe@1309 1961 "commonAncestorContainer"];
bsw/jbe@1309 1962
bsw/jbe@1309 1963 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
bsw/jbe@1309 1964 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
bsw/jbe@1309 1965
bsw/jbe@1309 1966 util.extend(api.rangePrototype, {
bsw/jbe@1309 1967 compareBoundaryPoints: function(how, range) {
bsw/jbe@1309 1968 assertRangeValid(this);
bsw/jbe@1309 1969 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
bsw/jbe@1309 1970
bsw/jbe@1309 1971 var nodeA, offsetA, nodeB, offsetB;
bsw/jbe@1309 1972 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
bsw/jbe@1309 1973 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
bsw/jbe@1309 1974 nodeA = this[prefixA + "Container"];
bsw/jbe@1309 1975 offsetA = this[prefixA + "Offset"];
bsw/jbe@1309 1976 nodeB = range[prefixB + "Container"];
bsw/jbe@1309 1977 offsetB = range[prefixB + "Offset"];
bsw/jbe@1309 1978 return comparePoints(nodeA, offsetA, nodeB, offsetB);
bsw/jbe@1309 1979 },
bsw/jbe@1309 1980
bsw/jbe@1309 1981 insertNode: function(node) {
bsw/jbe@1309 1982 assertRangeValid(this);
bsw/jbe@1309 1983 assertValidNodeType(node, insertableNodeTypes);
bsw/jbe@1309 1984 assertNodeNotReadOnly(this.startContainer);
bsw/jbe@1309 1985
bsw/jbe@1309 1986 if (isOrIsAncestorOf(node, this.startContainer)) {
bsw/jbe@1309 1987 throw new DOMException("HIERARCHY_REQUEST_ERR");
bsw/jbe@1309 1988 }
bsw/jbe@1309 1989
bsw/jbe@1309 1990 // No check for whether the container of the start of the Range is of a type that does not allow
bsw/jbe@1309 1991 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
bsw/jbe@1309 1992 // to add the node
bsw/jbe@1309 1993
bsw/jbe@1309 1994 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
bsw/jbe@1309 1995 this.setStartBefore(firstNodeInserted);
bsw/jbe@1309 1996 },
bsw/jbe@1309 1997
bsw/jbe@1309 1998 cloneContents: function() {
bsw/jbe@1309 1999 assertRangeValid(this);
bsw/jbe@1309 2000
bsw/jbe@1309 2001 var clone, frag;
bsw/jbe@1309 2002 if (this.collapsed) {
bsw/jbe@1309 2003 return getRangeDocument(this).createDocumentFragment();
bsw/jbe@1309 2004 } else {
bsw/jbe@1309 2005 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
bsw/jbe@1309 2006 clone = this.startContainer.cloneNode(true);
bsw/jbe@1309 2007 clone.data = clone.data.slice(this.startOffset, this.endOffset);
bsw/jbe@1309 2008 frag = getRangeDocument(this).createDocumentFragment();
bsw/jbe@1309 2009 frag.appendChild(clone);
bsw/jbe@1309 2010 return frag;
bsw/jbe@1309 2011 } else {
bsw/jbe@1309 2012 var iterator = new RangeIterator(this, true);
bsw/jbe@1309 2013 clone = cloneSubtree(iterator);
bsw/jbe@1309 2014 iterator.detach();
bsw/jbe@1309 2015 }
bsw/jbe@1309 2016 return clone;
bsw/jbe@1309 2017 }
bsw/jbe@1309 2018 },
bsw/jbe@1309 2019
bsw/jbe@1309 2020 canSurroundContents: function() {
bsw/jbe@1309 2021 assertRangeValid(this);
bsw/jbe@1309 2022 assertNodeNotReadOnly(this.startContainer);
bsw/jbe@1309 2023 assertNodeNotReadOnly(this.endContainer);
bsw/jbe@1309 2024
bsw/jbe@1309 2025 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
bsw/jbe@1309 2026 // no non-text nodes.
bsw/jbe@1309 2027 var iterator = new RangeIterator(this, true);
bsw/jbe@1309 2028 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
bsw/jbe@1309 2029 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
bsw/jbe@1309 2030 iterator.detach();
bsw/jbe@1309 2031 return !boundariesInvalid;
bsw/jbe@1309 2032 },
bsw/jbe@1309 2033
bsw/jbe@1309 2034 surroundContents: function(node) {
bsw/jbe@1309 2035 assertValidNodeType(node, surroundNodeTypes);
bsw/jbe@1309 2036
bsw/jbe@1309 2037 if (!this.canSurroundContents()) {
bsw/jbe@1309 2038 throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309 2039 }
bsw/jbe@1309 2040
bsw/jbe@1309 2041 // Extract the contents
bsw/jbe@1309 2042 var content = this.extractContents();
bsw/jbe@1309 2043
bsw/jbe@1309 2044 // Clear the children of the node
bsw/jbe@1309 2045 if (node.hasChildNodes()) {
bsw/jbe@1309 2046 while (node.lastChild) {
bsw/jbe@1309 2047 node.removeChild(node.lastChild);
bsw/jbe@1309 2048 }
bsw/jbe@1309 2049 }
bsw/jbe@1309 2050
bsw/jbe@1309 2051 // Insert the new node and add the extracted contents
bsw/jbe@1309 2052 insertNodeAtPosition(node, this.startContainer, this.startOffset);
bsw/jbe@1309 2053 node.appendChild(content);
bsw/jbe@1309 2054
bsw/jbe@1309 2055 this.selectNode(node);
bsw/jbe@1309 2056 },
bsw/jbe@1309 2057
bsw/jbe@1309 2058 cloneRange: function() {
bsw/jbe@1309 2059 assertRangeValid(this);
bsw/jbe@1309 2060 var range = new Range(getRangeDocument(this));
bsw/jbe@1309 2061 var i = rangeProperties.length, prop;
bsw/jbe@1309 2062 while (i--) {
bsw/jbe@1309 2063 prop = rangeProperties[i];
bsw/jbe@1309 2064 range[prop] = this[prop];
bsw/jbe@1309 2065 }
bsw/jbe@1309 2066 return range;
bsw/jbe@1309 2067 },
bsw/jbe@1309 2068
bsw/jbe@1309 2069 toString: function() {
bsw/jbe@1309 2070 assertRangeValid(this);
bsw/jbe@1309 2071 var sc = this.startContainer;
bsw/jbe@1309 2072 if (sc === this.endContainer && isCharacterDataNode(sc)) {
bsw/jbe@1309 2073 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
bsw/jbe@1309 2074 } else {
bsw/jbe@1309 2075 var textParts = [], iterator = new RangeIterator(this, true);
bsw/jbe@1309 2076 iterateSubtree(iterator, function(node) {
bsw/jbe@1309 2077 // Accept only text or CDATA nodes, not comments
bsw/jbe@1309 2078 if (node.nodeType == 3 || node.nodeType == 4) {
bsw/jbe@1309 2079 textParts.push(node.data);
bsw/jbe@1309 2080 }
bsw/jbe@1309 2081 });
bsw/jbe@1309 2082 iterator.detach();
bsw/jbe@1309 2083 return textParts.join("");
bsw/jbe@1309 2084 }
bsw/jbe@1309 2085 },
bsw/jbe@1309 2086
bsw/jbe@1309 2087 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
bsw/jbe@1309 2088 // been removed from Mozilla.
bsw/jbe@1309 2089
bsw/jbe@1309 2090 compareNode: function(node) {
bsw/jbe@1309 2091 assertRangeValid(this);
bsw/jbe@1309 2092
bsw/jbe@1309 2093 var parent = node.parentNode;
bsw/jbe@1309 2094 var nodeIndex = getNodeIndex(node);
bsw/jbe@1309 2095
bsw/jbe@1309 2096 if (!parent) {
bsw/jbe@1309 2097 throw new DOMException("NOT_FOUND_ERR");
bsw/jbe@1309 2098 }
bsw/jbe@1309 2099
bsw/jbe@1309 2100 var startComparison = this.comparePoint(parent, nodeIndex),
bsw/jbe@1309 2101 endComparison = this.comparePoint(parent, nodeIndex + 1);
bsw/jbe@1309 2102
bsw/jbe@1309 2103 if (startComparison < 0) { // Node starts before
bsw/jbe@1309 2104 return (endComparison > 0) ? n_b_a : n_b;
bsw/jbe@1309 2105 } else {
bsw/jbe@1309 2106 return (endComparison > 0) ? n_a : n_i;
bsw/jbe@1309 2107 }
bsw/jbe@1309 2108 },
bsw/jbe@1309 2109
bsw/jbe@1309 2110 comparePoint: function(node, offset) {
bsw/jbe@1309 2111 assertRangeValid(this);
bsw/jbe@1309 2112 assertNode(node, "HIERARCHY_REQUEST_ERR");
bsw/jbe@1309 2113 assertSameDocumentOrFragment(node, this.startContainer);
bsw/jbe@1309 2114
bsw/jbe@1309 2115 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
bsw/jbe@1309 2116 return -1;
bsw/jbe@1309 2117 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
bsw/jbe@1309 2118 return 1;
bsw/jbe@1309 2119 }
bsw/jbe@1309 2120 return 0;
bsw/jbe@1309 2121 },
bsw/jbe@1309 2122
bsw/jbe@1309 2123 createContextualFragment: createContextualFragment,
bsw/jbe@1309 2124
bsw/jbe@1309 2125 toHtml: function() {
bsw/jbe@1309 2126 return rangeToHtml(this);
bsw/jbe@1309 2127 },
bsw/jbe@1309 2128
bsw/jbe@1309 2129 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
bsw/jbe@1309 2130 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
bsw/jbe@1309 2131 intersectsNode: function(node, touchingIsIntersecting) {
bsw/jbe@1309 2132 assertRangeValid(this);
bsw/jbe@1309 2133 if (getRootContainer(node) != getRangeRoot(this)) {
bsw/jbe@1309 2134 return false;
bsw/jbe@1309 2135 }
bsw/jbe@1309 2136
bsw/jbe@1309 2137 var parent = node.parentNode, offset = getNodeIndex(node);
bsw/jbe@1309 2138 if (!parent) {
bsw/jbe@1309 2139 return true;
bsw/jbe@1309 2140 }
bsw/jbe@1309 2141
bsw/jbe@1309 2142 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
bsw/jbe@1309 2143 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
bsw/jbe@1309 2144
bsw/jbe@1309 2145 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
bsw/jbe@1309 2146 },
bsw/jbe@1309 2147
bsw/jbe@1309 2148 isPointInRange: function(node, offset) {
bsw/jbe@1309 2149 assertRangeValid(this);
bsw/jbe@1309 2150 assertNode(node, "HIERARCHY_REQUEST_ERR");
bsw/jbe@1309 2151 assertSameDocumentOrFragment(node, this.startContainer);
bsw/jbe@1309 2152
bsw/jbe@1309 2153 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
bsw/jbe@1309 2154 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
bsw/jbe@1309 2155 },
bsw/jbe@1309 2156
bsw/jbe@1309 2157 // The methods below are non-standard and invented by me.
bsw/jbe@1309 2158
bsw/jbe@1309 2159 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
bsw/jbe@1309 2160 intersectsRange: function(range) {
bsw/jbe@1309 2161 return rangesIntersect(this, range, false);
bsw/jbe@1309 2162 },
bsw/jbe@1309 2163
bsw/jbe@1309 2164 // Sharing a boundary start-to-end or end-to-start does count as intersection.
bsw/jbe@1309 2165 intersectsOrTouchesRange: function(range) {
bsw/jbe@1309 2166 return rangesIntersect(this, range, true);
bsw/jbe@1309 2167 },
bsw/jbe@1309 2168
bsw/jbe@1309 2169 intersection: function(range) {
bsw/jbe@1309 2170 if (this.intersectsRange(range)) {
bsw/jbe@1309 2171 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
bsw/jbe@1309 2172 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
bsw/jbe@1309 2173
bsw/jbe@1309 2174 var intersectionRange = this.cloneRange();
bsw/jbe@1309 2175 if (startComparison == -1) {
bsw/jbe@1309 2176 intersectionRange.setStart(range.startContainer, range.startOffset);
bsw/jbe@1309 2177 }
bsw/jbe@1309 2178 if (endComparison == 1) {
bsw/jbe@1309 2179 intersectionRange.setEnd(range.endContainer, range.endOffset);
bsw/jbe@1309 2180 }
bsw/jbe@1309 2181 return intersectionRange;
bsw/jbe@1309 2182 }
bsw/jbe@1309 2183 return null;
bsw/jbe@1309 2184 },
bsw/jbe@1309 2185
bsw/jbe@1309 2186 union: function(range) {
bsw/jbe@1309 2187 if (this.intersectsOrTouchesRange(range)) {
bsw/jbe@1309 2188 var unionRange = this.cloneRange();
bsw/jbe@1309 2189 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
bsw/jbe@1309 2190 unionRange.setStart(range.startContainer, range.startOffset);
bsw/jbe@1309 2191 }
bsw/jbe@1309 2192 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
bsw/jbe@1309 2193 unionRange.setEnd(range.endContainer, range.endOffset);
bsw/jbe@1309 2194 }
bsw/jbe@1309 2195 return unionRange;
bsw/jbe@1309 2196 } else {
bsw/jbe@1309 2197 throw new DOMException("Ranges do not intersect");
bsw/jbe@1309 2198 }
bsw/jbe@1309 2199 },
bsw/jbe@1309 2200
bsw/jbe@1309 2201 containsNode: function(node, allowPartial) {
bsw/jbe@1309 2202 if (allowPartial) {
bsw/jbe@1309 2203 return this.intersectsNode(node, false);
bsw/jbe@1309 2204 } else {
bsw/jbe@1309 2205 return this.compareNode(node) == n_i;
bsw/jbe@1309 2206 }
bsw/jbe@1309 2207 },
bsw/jbe@1309 2208
bsw/jbe@1309 2209 containsNodeContents: function(node) {
bsw/jbe@1309 2210 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
bsw/jbe@1309 2211 },
bsw/jbe@1309 2212
bsw/jbe@1309 2213 containsRange: function(range) {
bsw/jbe@1309 2214 var intersection = this.intersection(range);
bsw/jbe@1309 2215 return intersection !== null && range.equals(intersection);
bsw/jbe@1309 2216 },
bsw/jbe@1309 2217
bsw/jbe@1309 2218 containsNodeText: function(node) {
bsw/jbe@1309 2219 var nodeRange = this.cloneRange();
bsw/jbe@1309 2220 nodeRange.selectNode(node);
bsw/jbe@1309 2221 var textNodes = nodeRange.getNodes([3]);
bsw/jbe@1309 2222 if (textNodes.length > 0) {
bsw/jbe@1309 2223 nodeRange.setStart(textNodes[0], 0);
bsw/jbe@1309 2224 var lastTextNode = textNodes.pop();
bsw/jbe@1309 2225 nodeRange.setEnd(lastTextNode, lastTextNode.length);
bsw/jbe@1309 2226 return this.containsRange(nodeRange);
bsw/jbe@1309 2227 } else {
bsw/jbe@1309 2228 return this.containsNodeContents(node);
bsw/jbe@1309 2229 }
bsw/jbe@1309 2230 },
bsw/jbe@1309 2231
bsw/jbe@1309 2232 getNodes: function(nodeTypes, filter) {
bsw/jbe@1309 2233 assertRangeValid(this);
bsw/jbe@1309 2234 return getNodesInRange(this, nodeTypes, filter);
bsw/jbe@1309 2235 },
bsw/jbe@1309 2236
bsw/jbe@1309 2237 getDocument: function() {
bsw/jbe@1309 2238 return getRangeDocument(this);
bsw/jbe@1309 2239 },
bsw/jbe@1309 2240
bsw/jbe@1309 2241 collapseBefore: function(node) {
bsw/jbe@1309 2242 this.setEndBefore(node);
bsw/jbe@1309 2243 this.collapse(false);
bsw/jbe@1309 2244 },
bsw/jbe@1309 2245
bsw/jbe@1309 2246 collapseAfter: function(node) {
bsw/jbe@1309 2247 this.setStartAfter(node);
bsw/jbe@1309 2248 this.collapse(true);
bsw/jbe@1309 2249 },
bsw/jbe@1309 2250
bsw/jbe@1309 2251 getBookmark: function(containerNode) {
bsw/jbe@1309 2252 var doc = getRangeDocument(this);
bsw/jbe@1309 2253 var preSelectionRange = api.createRange(doc);
bsw/jbe@1309 2254 containerNode = containerNode || dom.getBody(doc);
bsw/jbe@1309 2255 preSelectionRange.selectNodeContents(containerNode);
bsw/jbe@1309 2256 var range = this.intersection(preSelectionRange);
bsw/jbe@1309 2257 var start = 0, end = 0;
bsw/jbe@1309 2258 if (range) {
bsw/jbe@1309 2259 preSelectionRange.setEnd(range.startContainer, range.startOffset);
bsw/jbe@1309 2260 start = preSelectionRange.toString().length;
bsw/jbe@1309 2261 end = start + range.toString().length;
bsw/jbe@1309 2262 }
bsw/jbe@1309 2263
bsw/jbe@1309 2264 return {
bsw/jbe@1309 2265 start: start,
bsw/jbe@1309 2266 end: end,
bsw/jbe@1309 2267 containerNode: containerNode
bsw/jbe@1309 2268 };
bsw/jbe@1309 2269 },
bsw/jbe@1309 2270
bsw/jbe@1309 2271 moveToBookmark: function(bookmark) {
bsw/jbe@1309 2272 var containerNode = bookmark.containerNode;
bsw/jbe@1309 2273 var charIndex = 0;
bsw/jbe@1309 2274 this.setStart(containerNode, 0);
bsw/jbe@1309 2275 this.collapse(true);
bsw/jbe@1309 2276 var nodeStack = [containerNode], node, foundStart = false, stop = false;
bsw/jbe@1309 2277 var nextCharIndex, i, childNodes;
bsw/jbe@1309 2278
bsw/jbe@1309 2279 while (!stop && (node = nodeStack.pop())) {
bsw/jbe@1309 2280 if (node.nodeType == 3) {
bsw/jbe@1309 2281 nextCharIndex = charIndex + node.length;
bsw/jbe@1309 2282 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
bsw/jbe@1309 2283 this.setStart(node, bookmark.start - charIndex);
bsw/jbe@1309 2284 foundStart = true;
bsw/jbe@1309 2285 }
bsw/jbe@1309 2286 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
bsw/jbe@1309 2287 this.setEnd(node, bookmark.end - charIndex);
bsw/jbe@1309 2288 stop = true;
bsw/jbe@1309 2289 }
bsw/jbe@1309 2290 charIndex = nextCharIndex;
bsw/jbe@1309 2291 } else {
bsw/jbe@1309 2292 childNodes = node.childNodes;
bsw/jbe@1309 2293 i = childNodes.length;
bsw/jbe@1309 2294 while (i--) {
bsw/jbe@1309 2295 nodeStack.push(childNodes[i]);
bsw/jbe@1309 2296 }
bsw/jbe@1309 2297 }
bsw/jbe@1309 2298 }
bsw/jbe@1309 2299 },
bsw/jbe@1309 2300
bsw/jbe@1309 2301 getName: function() {
bsw/jbe@1309 2302 return "DomRange";
bsw/jbe@1309 2303 },
bsw/jbe@1309 2304
bsw/jbe@1309 2305 equals: function(range) {
bsw/jbe@1309 2306 return Range.rangesEqual(this, range);
bsw/jbe@1309 2307 },
bsw/jbe@1309 2308
bsw/jbe@1309 2309 isValid: function() {
bsw/jbe@1309 2310 return isRangeValid(this);
bsw/jbe@1309 2311 },
bsw/jbe@1309 2312
bsw/jbe@1309 2313 inspect: function() {
bsw/jbe@1309 2314 return inspect(this);
bsw/jbe@1309 2315 },
bsw/jbe@1309 2316
bsw/jbe@1309 2317 detach: function() {
bsw/jbe@1309 2318 // In DOM4, detach() is now a no-op.
bsw/jbe@1309 2319 }
bsw/jbe@1309 2320 });
bsw/jbe@1309 2321
bsw/jbe@1309 2322 function copyComparisonConstantsToObject(obj) {
bsw/jbe@1309 2323 obj.START_TO_START = s2s;
bsw/jbe@1309 2324 obj.START_TO_END = s2e;
bsw/jbe@1309 2325 obj.END_TO_END = e2e;
bsw/jbe@1309 2326 obj.END_TO_START = e2s;
bsw/jbe@1309 2327
bsw/jbe@1309 2328 obj.NODE_BEFORE = n_b;
bsw/jbe@1309 2329 obj.NODE_AFTER = n_a;
bsw/jbe@1309 2330 obj.NODE_BEFORE_AND_AFTER = n_b_a;
bsw/jbe@1309 2331 obj.NODE_INSIDE = n_i;
bsw/jbe@1309 2332 }
bsw/jbe@1309 2333
bsw/jbe@1309 2334 function copyComparisonConstants(constructor) {
bsw/jbe@1309 2335 copyComparisonConstantsToObject(constructor);
bsw/jbe@1309 2336 copyComparisonConstantsToObject(constructor.prototype);
bsw/jbe@1309 2337 }
bsw/jbe@1309 2338
bsw/jbe@1309 2339 function createRangeContentRemover(remover, boundaryUpdater) {
bsw/jbe@1309 2340 return function() {
bsw/jbe@1309 2341 assertRangeValid(this);
bsw/jbe@1309 2342
bsw/jbe@1309 2343 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
bsw/jbe@1309 2344
bsw/jbe@1309 2345 var iterator = new RangeIterator(this, true);
bsw/jbe@1309 2346
bsw/jbe@1309 2347 // Work out where to position the range after content removal
bsw/jbe@1309 2348 var node, boundary;
bsw/jbe@1309 2349 if (sc !== root) {
bsw/jbe@1309 2350 node = getClosestAncestorIn(sc, root, true);
bsw/jbe@1309 2351 boundary = getBoundaryAfterNode(node);
bsw/jbe@1309 2352 sc = boundary.node;
bsw/jbe@1309 2353 so = boundary.offset;
bsw/jbe@1309 2354 }
bsw/jbe@1309 2355
bsw/jbe@1309 2356 // Check none of the range is read-only
bsw/jbe@1309 2357 iterateSubtree(iterator, assertNodeNotReadOnly);
bsw/jbe@1309 2358
bsw/jbe@1309 2359 iterator.reset();
bsw/jbe@1309 2360
bsw/jbe@1309 2361 // Remove the content
bsw/jbe@1309 2362 var returnValue = remover(iterator);
bsw/jbe@1309 2363 iterator.detach();
bsw/jbe@1309 2364
bsw/jbe@1309 2365 // Move to the new position
bsw/jbe@1309 2366 boundaryUpdater(this, sc, so, sc, so);
bsw/jbe@1309 2367
bsw/jbe@1309 2368 return returnValue;
bsw/jbe@1309 2369 };
bsw/jbe@1309 2370 }
bsw/jbe@1309 2371
bsw/jbe@1309 2372 function createPrototypeRange(constructor, boundaryUpdater) {
bsw/jbe@1309 2373 function createBeforeAfterNodeSetter(isBefore, isStart) {
bsw/jbe@1309 2374 return function(node) {
bsw/jbe@1309 2375 assertValidNodeType(node, beforeAfterNodeTypes);
bsw/jbe@1309 2376 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
bsw/jbe@1309 2377
bsw/jbe@1309 2378 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
bsw/jbe@1309 2379 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
bsw/jbe@1309 2380 };
bsw/jbe@1309 2381 }
bsw/jbe@1309 2382
bsw/jbe@1309 2383 function setRangeStart(range, node, offset) {
bsw/jbe@1309 2384 var ec = range.endContainer, eo = range.endOffset;
bsw/jbe@1309 2385 if (node !== range.startContainer || offset !== range.startOffset) {
bsw/jbe@1309 2386 // Check the root containers of the range and the new boundary, and also check whether the new boundary
bsw/jbe@1309 2387 // is after the current end. In either case, collapse the range to the new position
bsw/jbe@1309 2388 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
bsw/jbe@1309 2389 ec = node;
bsw/jbe@1309 2390 eo = offset;
bsw/jbe@1309 2391 }
bsw/jbe@1309 2392 boundaryUpdater(range, node, offset, ec, eo);
bsw/jbe@1309 2393 }
bsw/jbe@1309 2394 }
bsw/jbe@1309 2395
bsw/jbe@1309 2396 function setRangeEnd(range, node, offset) {
bsw/jbe@1309 2397 var sc = range.startContainer, so = range.startOffset;
bsw/jbe@1309 2398 if (node !== range.endContainer || offset !== range.endOffset) {
bsw/jbe@1309 2399 // Check the root containers of the range and the new boundary, and also check whether the new boundary
bsw/jbe@1309 2400 // is after the current end. In either case, collapse the range to the new position
bsw/jbe@1309 2401 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
bsw/jbe@1309 2402 sc = node;
bsw/jbe@1309 2403 so = offset;
bsw/jbe@1309 2404 }
bsw/jbe@1309 2405 boundaryUpdater(range, sc, so, node, offset);
bsw/jbe@1309 2406 }
bsw/jbe@1309 2407 }
bsw/jbe@1309 2408
bsw/jbe@1309 2409 // Set up inheritance
bsw/jbe@1309 2410 var F = function() {};
bsw/jbe@1309 2411 F.prototype = api.rangePrototype;
bsw/jbe@1309 2412 constructor.prototype = new F();
bsw/jbe@1309 2413
bsw/jbe@1309 2414 util.extend(constructor.prototype, {
bsw/jbe@1309 2415 setStart: function(node, offset) {
bsw/jbe@1309 2416 assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309 2417 assertValidOffset(node, offset);
bsw/jbe@1309 2418
bsw/jbe@1309 2419 setRangeStart(this, node, offset);
bsw/jbe@1309 2420 },
bsw/jbe@1309 2421
bsw/jbe@1309 2422 setEnd: function(node, offset) {
bsw/jbe@1309 2423 assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309 2424 assertValidOffset(node, offset);
bsw/jbe@1309 2425
bsw/jbe@1309 2426 setRangeEnd(this, node, offset);
bsw/jbe@1309 2427 },
bsw/jbe@1309 2428
bsw/jbe@1309 2429 /**
bsw/jbe@1309 2430 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
bsw/jbe@1309 2431 * - Two parameters (node, offset) creates a collapsed range at that position
bsw/jbe@1309 2432 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
bsw/jbe@1309 2433 * startOffset and ending at endOffset
bsw/jbe@1309 2434 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
bsw/jbe@1309 2435 * startNode and ending at endOffset in endNode
bsw/jbe@1309 2436 */
bsw/jbe@1309 2437 setStartAndEnd: function() {
bsw/jbe@1309 2438 var args = arguments;
bsw/jbe@1309 2439 var sc = args[0], so = args[1], ec = sc, eo = so;
bsw/jbe@1309 2440
bsw/jbe@1309 2441 switch (args.length) {
bsw/jbe@1309 2442 case 3:
bsw/jbe@1309 2443 eo = args[2];
bsw/jbe@1309 2444 break;
bsw/jbe@1309 2445 case 4:
bsw/jbe@1309 2446 ec = args[2];
bsw/jbe@1309 2447 eo = args[3];
bsw/jbe@1309 2448 break;
bsw/jbe@1309 2449 }
bsw/jbe@1309 2450
bsw/jbe@1309 2451 boundaryUpdater(this, sc, so, ec, eo);
bsw/jbe@1309 2452 },
bsw/jbe@1309 2453
bsw/jbe@1309 2454 setBoundary: function(node, offset, isStart) {
bsw/jbe@1309 2455 this["set" + (isStart ? "Start" : "End")](node, offset);
bsw/jbe@1309 2456 },
bsw/jbe@1309 2457
bsw/jbe@1309 2458 setStartBefore: createBeforeAfterNodeSetter(true, true),
bsw/jbe@1309 2459 setStartAfter: createBeforeAfterNodeSetter(false, true),
bsw/jbe@1309 2460 setEndBefore: createBeforeAfterNodeSetter(true, false),
bsw/jbe@1309 2461 setEndAfter: createBeforeAfterNodeSetter(false, false),
bsw/jbe@1309 2462
bsw/jbe@1309 2463 collapse: function(isStart) {
bsw/jbe@1309 2464 assertRangeValid(this);
bsw/jbe@1309 2465 if (isStart) {
bsw/jbe@1309 2466 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
bsw/jbe@1309 2467 } else {
bsw/jbe@1309 2468 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
bsw/jbe@1309 2469 }
bsw/jbe@1309 2470 },
bsw/jbe@1309 2471
bsw/jbe@1309 2472 selectNodeContents: function(node) {
bsw/jbe@1309 2473 assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309 2474
bsw/jbe@1309 2475 boundaryUpdater(this, node, 0, node, getNodeLength(node));
bsw/jbe@1309 2476 },
bsw/jbe@1309 2477
bsw/jbe@1309 2478 selectNode: function(node) {
bsw/jbe@1309 2479 assertNoDocTypeNotationEntityAncestor(node, false);
bsw/jbe@1309 2480 assertValidNodeType(node, beforeAfterNodeTypes);
bsw/jbe@1309 2481
bsw/jbe@1309 2482 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
bsw/jbe@1309 2483 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
bsw/jbe@1309 2484 },
bsw/jbe@1309 2485
bsw/jbe@1309 2486 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
bsw/jbe@1309 2487
bsw/jbe@1309 2488 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
bsw/jbe@1309 2489
bsw/jbe@1309 2490 canSurroundContents: function() {
bsw/jbe@1309 2491 assertRangeValid(this);
bsw/jbe@1309 2492 assertNodeNotReadOnly(this.startContainer);
bsw/jbe@1309 2493 assertNodeNotReadOnly(this.endContainer);
bsw/jbe@1309 2494
bsw/jbe@1309 2495 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
bsw/jbe@1309 2496 // no non-text nodes.
bsw/jbe@1309 2497 var iterator = new RangeIterator(this, true);
bsw/jbe@1309 2498 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
bsw/jbe@1309 2499 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
bsw/jbe@1309 2500 iterator.detach();
bsw/jbe@1309 2501 return !boundariesInvalid;
bsw/jbe@1309 2502 },
bsw/jbe@1309 2503
bsw/jbe@1309 2504 splitBoundaries: function() {
bsw/jbe@1309 2505 splitRangeBoundaries(this);
bsw/jbe@1309 2506 },
bsw/jbe@1309 2507
bsw/jbe@1309 2508 splitBoundariesPreservingPositions: function(positionsToPreserve) {
bsw/jbe@1309 2509 splitRangeBoundaries(this, positionsToPreserve);
bsw/jbe@1309 2510 },
bsw/jbe@1309 2511
bsw/jbe@1309 2512 normalizeBoundaries: function() {
bsw/jbe@1309 2513 assertRangeValid(this);
bsw/jbe@1309 2514
bsw/jbe@1309 2515 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
bsw/jbe@1309 2516
bsw/jbe@1309 2517 var mergeForward = function(node) {
bsw/jbe@1309 2518 var sibling = node.nextSibling;
bsw/jbe@1309 2519 if (sibling && sibling.nodeType == node.nodeType) {
bsw/jbe@1309 2520 ec = node;
bsw/jbe@1309 2521 eo = node.length;
bsw/jbe@1309 2522 node.appendData(sibling.data);
bsw/jbe@1309 2523 removeNode(sibling);
bsw/jbe@1309 2524 }
bsw/jbe@1309 2525 };
bsw/jbe@1309 2526
bsw/jbe@1309 2527 var mergeBackward = function(node) {
bsw/jbe@1309 2528 var sibling = node.previousSibling;
bsw/jbe@1309 2529 if (sibling && sibling.nodeType == node.nodeType) {
bsw/jbe@1309 2530 sc = node;
bsw/jbe@1309 2531 var nodeLength = node.length;
bsw/jbe@1309 2532 so = sibling.length;
bsw/jbe@1309 2533 node.insertData(0, sibling.data);
bsw/jbe@1309 2534 removeNode(sibling);
bsw/jbe@1309 2535 if (sc == ec) {
bsw/jbe@1309 2536 eo += so;
bsw/jbe@1309 2537 ec = sc;
bsw/jbe@1309 2538 } else if (ec == node.parentNode) {
bsw/jbe@1309 2539 var nodeIndex = getNodeIndex(node);
bsw/jbe@1309 2540 if (eo == nodeIndex) {
bsw/jbe@1309 2541 ec = node;
bsw/jbe@1309 2542 eo = nodeLength;
bsw/jbe@1309 2543 } else if (eo > nodeIndex) {
bsw/jbe@1309 2544 eo--;
bsw/jbe@1309 2545 }
bsw/jbe@1309 2546 }
bsw/jbe@1309 2547 }
bsw/jbe@1309 2548 };
bsw/jbe@1309 2549
bsw/jbe@1309 2550 var normalizeStart = true;
bsw/jbe@1309 2551 var sibling;
bsw/jbe@1309 2552
bsw/jbe@1309 2553 if (isCharacterDataNode(ec)) {
bsw/jbe@1309 2554 if (eo == ec.length) {
bsw/jbe@1309 2555 mergeForward(ec);
bsw/jbe@1309 2556 } else if (eo == 0) {
bsw/jbe@1309 2557 sibling = ec.previousSibling;
bsw/jbe@1309 2558 if (sibling && sibling.nodeType == ec.nodeType) {
bsw/jbe@1309 2559 eo = sibling.length;
bsw/jbe@1309 2560 if (sc == ec) {
bsw/jbe@1309 2561 normalizeStart = false;
bsw/jbe@1309 2562 }
bsw/jbe@1309 2563 sibling.appendData(ec.data);
bsw/jbe@1309 2564 removeNode(ec);
bsw/jbe@1309 2565 ec = sibling;
bsw/jbe@1309 2566 }
bsw/jbe@1309 2567 }
bsw/jbe@1309 2568 } else {
bsw/jbe@1309 2569 if (eo > 0) {
bsw/jbe@1309 2570 var endNode = ec.childNodes[eo - 1];
bsw/jbe@1309 2571 if (endNode && isCharacterDataNode(endNode)) {
bsw/jbe@1309 2572 mergeForward(endNode);
bsw/jbe@1309 2573 }
bsw/jbe@1309 2574 }
bsw/jbe@1309 2575 normalizeStart = !this.collapsed;
bsw/jbe@1309 2576 }
bsw/jbe@1309 2577
bsw/jbe@1309 2578 if (normalizeStart) {
bsw/jbe@1309 2579 if (isCharacterDataNode(sc)) {
bsw/jbe@1309 2580 if (so == 0) {
bsw/jbe@1309 2581 mergeBackward(sc);
bsw/jbe@1309 2582 } else if (so == sc.length) {
bsw/jbe@1309 2583 sibling = sc.nextSibling;
bsw/jbe@1309 2584 if (sibling && sibling.nodeType == sc.nodeType) {
bsw/jbe@1309 2585 if (ec == sibling) {
bsw/jbe@1309 2586 ec = sc;
bsw/jbe@1309 2587 eo += sc.length;
bsw/jbe@1309 2588 }
bsw/jbe@1309 2589 sc.appendData(sibling.data);
bsw/jbe@1309 2590 removeNode(sibling);
bsw/jbe@1309 2591 }
bsw/jbe@1309 2592 }
bsw/jbe@1309 2593 } else {
bsw/jbe@1309 2594 if (so < sc.childNodes.length) {
bsw/jbe@1309 2595 var startNode = sc.childNodes[so];
bsw/jbe@1309 2596 if (startNode && isCharacterDataNode(startNode)) {
bsw/jbe@1309 2597 mergeBackward(startNode);
bsw/jbe@1309 2598 }
bsw/jbe@1309 2599 }
bsw/jbe@1309 2600 }
bsw/jbe@1309 2601 } else {
bsw/jbe@1309 2602 sc = ec;
bsw/jbe@1309 2603 so = eo;
bsw/jbe@1309 2604 }
bsw/jbe@1309 2605
bsw/jbe@1309 2606 boundaryUpdater(this, sc, so, ec, eo);
bsw/jbe@1309 2607 },
bsw/jbe@1309 2608
bsw/jbe@1309 2609 collapseToPoint: function(node, offset) {
bsw/jbe@1309 2610 assertNoDocTypeNotationEntityAncestor(node, true);
bsw/jbe@1309 2611 assertValidOffset(node, offset);
bsw/jbe@1309 2612 this.setStartAndEnd(node, offset);
bsw/jbe@1309 2613 }
bsw/jbe@1309 2614 });
bsw/jbe@1309 2615
bsw/jbe@1309 2616 copyComparisonConstants(constructor);
bsw/jbe@1309 2617 }
bsw/jbe@1309 2618
bsw/jbe@1309 2619 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2620
bsw/jbe@1309 2621 // Updates commonAncestorContainer and collapsed after boundary change
bsw/jbe@1309 2622 function updateCollapsedAndCommonAncestor(range) {
bsw/jbe@1309 2623 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
bsw/jbe@1309 2624 range.commonAncestorContainer = range.collapsed ?
bsw/jbe@1309 2625 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
bsw/jbe@1309 2626 }
bsw/jbe@1309 2627
bsw/jbe@1309 2628 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
bsw/jbe@1309 2629 range.startContainer = startContainer;
bsw/jbe@1309 2630 range.startOffset = startOffset;
bsw/jbe@1309 2631 range.endContainer = endContainer;
bsw/jbe@1309 2632 range.endOffset = endOffset;
bsw/jbe@1309 2633 range.document = dom.getDocument(startContainer);
bsw/jbe@1309 2634
bsw/jbe@1309 2635 updateCollapsedAndCommonAncestor(range);
bsw/jbe@1309 2636 }
bsw/jbe@1309 2637
bsw/jbe@1309 2638 function Range(doc) {
bsw/jbe@1309 2639 this.startContainer = doc;
bsw/jbe@1309 2640 this.startOffset = 0;
bsw/jbe@1309 2641 this.endContainer = doc;
bsw/jbe@1309 2642 this.endOffset = 0;
bsw/jbe@1309 2643 this.document = doc;
bsw/jbe@1309 2644 updateCollapsedAndCommonAncestor(this);
bsw/jbe@1309 2645 }
bsw/jbe@1309 2646
bsw/jbe@1309 2647 createPrototypeRange(Range, updateBoundaries);
bsw/jbe@1309 2648
bsw/jbe@1309 2649 util.extend(Range, {
bsw/jbe@1309 2650 rangeProperties: rangeProperties,
bsw/jbe@1309 2651 RangeIterator: RangeIterator,
bsw/jbe@1309 2652 copyComparisonConstants: copyComparisonConstants,
bsw/jbe@1309 2653 createPrototypeRange: createPrototypeRange,
bsw/jbe@1309 2654 inspect: inspect,
bsw/jbe@1309 2655 toHtml: rangeToHtml,
bsw/jbe@1309 2656 getRangeDocument: getRangeDocument,
bsw/jbe@1309 2657 rangesEqual: function(r1, r2) {
bsw/jbe@1309 2658 return r1.startContainer === r2.startContainer &&
bsw/jbe@1309 2659 r1.startOffset === r2.startOffset &&
bsw/jbe@1309 2660 r1.endContainer === r2.endContainer &&
bsw/jbe@1309 2661 r1.endOffset === r2.endOffset;
bsw/jbe@1309 2662 }
bsw/jbe@1309 2663 });
bsw/jbe@1309 2664
bsw/jbe@1309 2665 api.DomRange = Range;
bsw/jbe@1309 2666 });
bsw/jbe@1309 2667
bsw/jbe@1309 2668 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2669
bsw/jbe@1309 2670 // Wrappers for the browser's native DOM Range and/or TextRange implementation
bsw/jbe@1309 2671 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
bsw/jbe@1309 2672 var WrappedRange, WrappedTextRange;
bsw/jbe@1309 2673 var dom = api.dom;
bsw/jbe@1309 2674 var util = api.util;
bsw/jbe@1309 2675 var DomPosition = dom.DomPosition;
bsw/jbe@1309 2676 var DomRange = api.DomRange;
bsw/jbe@1309 2677 var getBody = dom.getBody;
bsw/jbe@1309 2678 var getContentDocument = dom.getContentDocument;
bsw/jbe@1309 2679 var isCharacterDataNode = dom.isCharacterDataNode;
bsw/jbe@1309 2680
bsw/jbe@1309 2681
bsw/jbe@1309 2682 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2683
bsw/jbe@1309 2684 if (api.features.implementsDomRange) {
bsw/jbe@1309 2685 // This is a wrapper around the browser's native DOM Range. It has two aims:
bsw/jbe@1309 2686 // - Provide workarounds for specific browser bugs
bsw/jbe@1309 2687 // - provide convenient extensions, which are inherited from Rangy's DomRange
bsw/jbe@1309 2688
bsw/jbe@1309 2689 (function() {
bsw/jbe@1309 2690 var rangeProto;
bsw/jbe@1309 2691 var rangeProperties = DomRange.rangeProperties;
bsw/jbe@1309 2692
bsw/jbe@1309 2693 function updateRangeProperties(range) {
bsw/jbe@1309 2694 var i = rangeProperties.length, prop;
bsw/jbe@1309 2695 while (i--) {
bsw/jbe@1309 2696 prop = rangeProperties[i];
bsw/jbe@1309 2697 range[prop] = range.nativeRange[prop];
bsw/jbe@1309 2698 }
bsw/jbe@1309 2699 // Fix for broken collapsed property in IE 9.
bsw/jbe@1309 2700 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
bsw/jbe@1309 2701 }
bsw/jbe@1309 2702
bsw/jbe@1309 2703 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
bsw/jbe@1309 2704 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
bsw/jbe@1309 2705 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
bsw/jbe@1309 2706 var nativeRangeDifferent = !range.equals(range.nativeRange);
bsw/jbe@1309 2707
bsw/jbe@1309 2708 // Always set both boundaries for the benefit of IE9 (see issue 35)
bsw/jbe@1309 2709 if (startMoved || endMoved || nativeRangeDifferent) {
bsw/jbe@1309 2710 range.setEnd(endContainer, endOffset);
bsw/jbe@1309 2711 range.setStart(startContainer, startOffset);
bsw/jbe@1309 2712 }
bsw/jbe@1309 2713 }
bsw/jbe@1309 2714
bsw/jbe@1309 2715 var createBeforeAfterNodeSetter;
bsw/jbe@1309 2716
bsw/jbe@1309 2717 WrappedRange = function(range) {
bsw/jbe@1309 2718 if (!range) {
bsw/jbe@1309 2719 throw module.createError("WrappedRange: Range must be specified");
bsw/jbe@1309 2720 }
bsw/jbe@1309 2721 this.nativeRange = range;
bsw/jbe@1309 2722 updateRangeProperties(this);
bsw/jbe@1309 2723 };
bsw/jbe@1309 2724
bsw/jbe@1309 2725 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
bsw/jbe@1309 2726
bsw/jbe@1309 2727 rangeProto = WrappedRange.prototype;
bsw/jbe@1309 2728
bsw/jbe@1309 2729 rangeProto.selectNode = function(node) {
bsw/jbe@1309 2730 this.nativeRange.selectNode(node);
bsw/jbe@1309 2731 updateRangeProperties(this);
bsw/jbe@1309 2732 };
bsw/jbe@1309 2733
bsw/jbe@1309 2734 rangeProto.cloneContents = function() {
bsw/jbe@1309 2735 return this.nativeRange.cloneContents();
bsw/jbe@1309 2736 };
bsw/jbe@1309 2737
bsw/jbe@1309 2738 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
bsw/jbe@1309 2739 // insertNode() is never delegated to the native range.
bsw/jbe@1309 2740
bsw/jbe@1309 2741 rangeProto.surroundContents = function(node) {
bsw/jbe@1309 2742 this.nativeRange.surroundContents(node);
bsw/jbe@1309 2743 updateRangeProperties(this);
bsw/jbe@1309 2744 };
bsw/jbe@1309 2745
bsw/jbe@1309 2746 rangeProto.collapse = function(isStart) {
bsw/jbe@1309 2747 this.nativeRange.collapse(isStart);
bsw/jbe@1309 2748 updateRangeProperties(this);
bsw/jbe@1309 2749 };
bsw/jbe@1309 2750
bsw/jbe@1309 2751 rangeProto.cloneRange = function() {
bsw/jbe@1309 2752 return new WrappedRange(this.nativeRange.cloneRange());
bsw/jbe@1309 2753 };
bsw/jbe@1309 2754
bsw/jbe@1309 2755 rangeProto.refresh = function() {
bsw/jbe@1309 2756 updateRangeProperties(this);
bsw/jbe@1309 2757 };
bsw/jbe@1309 2758
bsw/jbe@1309 2759 rangeProto.toString = function() {
bsw/jbe@1309 2760 return this.nativeRange.toString();
bsw/jbe@1309 2761 };
bsw/jbe@1309 2762
bsw/jbe@1309 2763 // Create test range and node for feature detection
bsw/jbe@1309 2764
bsw/jbe@1309 2765 var testTextNode = document.createTextNode("test");
bsw/jbe@1309 2766 getBody(document).appendChild(testTextNode);
bsw/jbe@1309 2767 var range = document.createRange();
bsw/jbe@1309 2768
bsw/jbe@1309 2769 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2770
bsw/jbe@1309 2771 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
bsw/jbe@1309 2772 // correct for it
bsw/jbe@1309 2773
bsw/jbe@1309 2774 range.setStart(testTextNode, 0);
bsw/jbe@1309 2775 range.setEnd(testTextNode, 0);
bsw/jbe@1309 2776
bsw/jbe@1309 2777 try {
bsw/jbe@1309 2778 range.setStart(testTextNode, 1);
bsw/jbe@1309 2779
bsw/jbe@1309 2780 rangeProto.setStart = function(node, offset) {
bsw/jbe@1309 2781 this.nativeRange.setStart(node, offset);
bsw/jbe@1309 2782 updateRangeProperties(this);
bsw/jbe@1309 2783 };
bsw/jbe@1309 2784
bsw/jbe@1309 2785 rangeProto.setEnd = function(node, offset) {
bsw/jbe@1309 2786 this.nativeRange.setEnd(node, offset);
bsw/jbe@1309 2787 updateRangeProperties(this);
bsw/jbe@1309 2788 };
bsw/jbe@1309 2789
bsw/jbe@1309 2790 createBeforeAfterNodeSetter = function(name) {
bsw/jbe@1309 2791 return function(node) {
bsw/jbe@1309 2792 this.nativeRange[name](node);
bsw/jbe@1309 2793 updateRangeProperties(this);
bsw/jbe@1309 2794 };
bsw/jbe@1309 2795 };
bsw/jbe@1309 2796
bsw/jbe@1309 2797 } catch(ex) {
bsw/jbe@1309 2798
bsw/jbe@1309 2799 rangeProto.setStart = function(node, offset) {
bsw/jbe@1309 2800 try {
bsw/jbe@1309 2801 this.nativeRange.setStart(node, offset);
bsw/jbe@1309 2802 } catch (ex) {
bsw/jbe@1309 2803 this.nativeRange.setEnd(node, offset);
bsw/jbe@1309 2804 this.nativeRange.setStart(node, offset);
bsw/jbe@1309 2805 }
bsw/jbe@1309 2806 updateRangeProperties(this);
bsw/jbe@1309 2807 };
bsw/jbe@1309 2808
bsw/jbe@1309 2809 rangeProto.setEnd = function(node, offset) {
bsw/jbe@1309 2810 try {
bsw/jbe@1309 2811 this.nativeRange.setEnd(node, offset);
bsw/jbe@1309 2812 } catch (ex) {
bsw/jbe@1309 2813 this.nativeRange.setStart(node, offset);
bsw/jbe@1309 2814 this.nativeRange.setEnd(node, offset);
bsw/jbe@1309 2815 }
bsw/jbe@1309 2816 updateRangeProperties(this);
bsw/jbe@1309 2817 };
bsw/jbe@1309 2818
bsw/jbe@1309 2819 createBeforeAfterNodeSetter = function(name, oppositeName) {
bsw/jbe@1309 2820 return function(node) {
bsw/jbe@1309 2821 try {
bsw/jbe@1309 2822 this.nativeRange[name](node);
bsw/jbe@1309 2823 } catch (ex) {
bsw/jbe@1309 2824 this.nativeRange[oppositeName](node);
bsw/jbe@1309 2825 this.nativeRange[name](node);
bsw/jbe@1309 2826 }
bsw/jbe@1309 2827 updateRangeProperties(this);
bsw/jbe@1309 2828 };
bsw/jbe@1309 2829 };
bsw/jbe@1309 2830 }
bsw/jbe@1309 2831
bsw/jbe@1309 2832 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
bsw/jbe@1309 2833 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
bsw/jbe@1309 2834 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
bsw/jbe@1309 2835 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
bsw/jbe@1309 2836
bsw/jbe@1309 2837 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2838
bsw/jbe@1309 2839 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
bsw/jbe@1309 2840 // whether the native implementation can be trusted
bsw/jbe@1309 2841 rangeProto.selectNodeContents = function(node) {
bsw/jbe@1309 2842 this.setStartAndEnd(node, 0, dom.getNodeLength(node));
bsw/jbe@1309 2843 };
bsw/jbe@1309 2844
bsw/jbe@1309 2845 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2846
bsw/jbe@1309 2847 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
bsw/jbe@1309 2848 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
bsw/jbe@1309 2849
bsw/jbe@1309 2850 range.selectNodeContents(testTextNode);
bsw/jbe@1309 2851 range.setEnd(testTextNode, 3);
bsw/jbe@1309 2852
bsw/jbe@1309 2853 var range2 = document.createRange();
bsw/jbe@1309 2854 range2.selectNodeContents(testTextNode);
bsw/jbe@1309 2855 range2.setEnd(testTextNode, 4);
bsw/jbe@1309 2856 range2.setStart(testTextNode, 2);
bsw/jbe@1309 2857
bsw/jbe@1309 2858 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
bsw/jbe@1309 2859 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
bsw/jbe@1309 2860 // This is the wrong way round, so correct for it
bsw/jbe@1309 2861
bsw/jbe@1309 2862 rangeProto.compareBoundaryPoints = function(type, range) {
bsw/jbe@1309 2863 range = range.nativeRange || range;
bsw/jbe@1309 2864 if (type == range.START_TO_END) {
bsw/jbe@1309 2865 type = range.END_TO_START;
bsw/jbe@1309 2866 } else if (type == range.END_TO_START) {
bsw/jbe@1309 2867 type = range.START_TO_END;
bsw/jbe@1309 2868 }
bsw/jbe@1309 2869 return this.nativeRange.compareBoundaryPoints(type, range);
bsw/jbe@1309 2870 };
bsw/jbe@1309 2871 } else {
bsw/jbe@1309 2872 rangeProto.compareBoundaryPoints = function(type, range) {
bsw/jbe@1309 2873 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
bsw/jbe@1309 2874 };
bsw/jbe@1309 2875 }
bsw/jbe@1309 2876
bsw/jbe@1309 2877 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2878
bsw/jbe@1309 2879 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
bsw/jbe@1309 2880
bsw/jbe@1309 2881 var el = document.createElement("div");
bsw/jbe@1309 2882 el.innerHTML = "123";
bsw/jbe@1309 2883 var textNode = el.firstChild;
bsw/jbe@1309 2884 var body = getBody(document);
bsw/jbe@1309 2885 body.appendChild(el);
bsw/jbe@1309 2886
bsw/jbe@1309 2887 range.setStart(textNode, 1);
bsw/jbe@1309 2888 range.setEnd(textNode, 2);
bsw/jbe@1309 2889 range.deleteContents();
bsw/jbe@1309 2890
bsw/jbe@1309 2891 if (textNode.data == "13") {
bsw/jbe@1309 2892 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
bsw/jbe@1309 2893 // extractContents()
bsw/jbe@1309 2894 rangeProto.deleteContents = function() {
bsw/jbe@1309 2895 this.nativeRange.deleteContents();
bsw/jbe@1309 2896 updateRangeProperties(this);
bsw/jbe@1309 2897 };
bsw/jbe@1309 2898
bsw/jbe@1309 2899 rangeProto.extractContents = function() {
bsw/jbe@1309 2900 var frag = this.nativeRange.extractContents();
bsw/jbe@1309 2901 updateRangeProperties(this);
bsw/jbe@1309 2902 return frag;
bsw/jbe@1309 2903 };
bsw/jbe@1309 2904 } else {
bsw/jbe@1309 2905 }
bsw/jbe@1309 2906
bsw/jbe@1309 2907 body.removeChild(el);
bsw/jbe@1309 2908 body = null;
bsw/jbe@1309 2909
bsw/jbe@1309 2910 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2911
bsw/jbe@1309 2912 // Test for existence of createContextualFragment and delegate to it if it exists
bsw/jbe@1309 2913 if (util.isHostMethod(range, "createContextualFragment")) {
bsw/jbe@1309 2914 rangeProto.createContextualFragment = function(fragmentStr) {
bsw/jbe@1309 2915 return this.nativeRange.createContextualFragment(fragmentStr);
bsw/jbe@1309 2916 };
bsw/jbe@1309 2917 }
bsw/jbe@1309 2918
bsw/jbe@1309 2919 /*--------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 2920
bsw/jbe@1309 2921 // Clean up
bsw/jbe@1309 2922 getBody(document).removeChild(testTextNode);
bsw/jbe@1309 2923
bsw/jbe@1309 2924 rangeProto.getName = function() {
bsw/jbe@1309 2925 return "WrappedRange";
bsw/jbe@1309 2926 };
bsw/jbe@1309 2927
bsw/jbe@1309 2928 api.WrappedRange = WrappedRange;
bsw/jbe@1309 2929
bsw/jbe@1309 2930 api.createNativeRange = function(doc) {
bsw/jbe@1309 2931 doc = getContentDocument(doc, module, "createNativeRange");
bsw/jbe@1309 2932 return doc.createRange();
bsw/jbe@1309 2933 };
bsw/jbe@1309 2934 })();
bsw/jbe@1309 2935 }
bsw/jbe@1309 2936
bsw/jbe@1309 2937 if (api.features.implementsTextRange) {
bsw/jbe@1309 2938 /*
bsw/jbe@1309 2939 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
bsw/jbe@1309 2940 method. For example, in the following (where pipes denote the selection boundaries):
bsw/jbe@1309 2941
bsw/jbe@1309 2942 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
bsw/jbe@1309 2943
bsw/jbe@1309 2944 var range = document.selection.createRange();
bsw/jbe@1309 2945 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
bsw/jbe@1309 2946
bsw/jbe@1309 2947 This method returns the common ancestor node of the following:
bsw/jbe@1309 2948 - the parentElement() of the textRange
bsw/jbe@1309 2949 - the parentElement() of the textRange after calling collapse(true)
bsw/jbe@1309 2950 - the parentElement() of the textRange after calling collapse(false)
bsw/jbe@1309 2951 */
bsw/jbe@1309 2952 var getTextRangeContainerElement = function(textRange) {
bsw/jbe@1309 2953 var parentEl = textRange.parentElement();
bsw/jbe@1309 2954 var range = textRange.duplicate();
bsw/jbe@1309 2955 range.collapse(true);
bsw/jbe@1309 2956 var startEl = range.parentElement();
bsw/jbe@1309 2957 range = textRange.duplicate();
bsw/jbe@1309 2958 range.collapse(false);
bsw/jbe@1309 2959 var endEl = range.parentElement();
bsw/jbe@1309 2960 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
bsw/jbe@1309 2961
bsw/jbe@1309 2962 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
bsw/jbe@1309 2963 };
bsw/jbe@1309 2964
bsw/jbe@1309 2965 var textRangeIsCollapsed = function(textRange) {
bsw/jbe@1309 2966 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
bsw/jbe@1309 2967 };
bsw/jbe@1309 2968
bsw/jbe@1309 2969 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
bsw/jbe@1309 2970 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
bsw/jbe@1309 2971 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
bsw/jbe@1309 2972 // bugs, handling for inputs and images, plus optimizations.
bsw/jbe@1309 2973 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
bsw/jbe@1309 2974 var workingRange = textRange.duplicate();
bsw/jbe@1309 2975 workingRange.collapse(isStart);
bsw/jbe@1309 2976 var containerElement = workingRange.parentElement();
bsw/jbe@1309 2977
bsw/jbe@1309 2978 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
bsw/jbe@1309 2979 // check for that
bsw/jbe@1309 2980 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
bsw/jbe@1309 2981 containerElement = wholeRangeContainerElement;
bsw/jbe@1309 2982 }
bsw/jbe@1309 2983
bsw/jbe@1309 2984
bsw/jbe@1309 2985 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
bsw/jbe@1309 2986 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
bsw/jbe@1309 2987 if (!containerElement.canHaveHTML) {
bsw/jbe@1309 2988 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
bsw/jbe@1309 2989 return {
bsw/jbe@1309 2990 boundaryPosition: pos,
bsw/jbe@1309 2991 nodeInfo: {
bsw/jbe@1309 2992 nodeIndex: pos.offset,
bsw/jbe@1309 2993 containerElement: pos.node
bsw/jbe@1309 2994 }
bsw/jbe@1309 2995 };
bsw/jbe@1309 2996 }
bsw/jbe@1309 2997
bsw/jbe@1309 2998 var workingNode = dom.getDocument(containerElement).createElement("span");
bsw/jbe@1309 2999
bsw/jbe@1309 3000 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
bsw/jbe@1309 3001 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
bsw/jbe@1309 3002 if (workingNode.parentNode) {
bsw/jbe@1309 3003 dom.removeNode(workingNode);
bsw/jbe@1309 3004 }
bsw/jbe@1309 3005
bsw/jbe@1309 3006 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
bsw/jbe@1309 3007 var previousNode, nextNode, boundaryPosition, boundaryNode;
bsw/jbe@1309 3008 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
bsw/jbe@1309 3009 var childNodeCount = containerElement.childNodes.length;
bsw/jbe@1309 3010 var end = childNodeCount;
bsw/jbe@1309 3011
bsw/jbe@1309 3012 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
bsw/jbe@1309 3013 // after the range boundary.
bsw/jbe@1309 3014 var nodeIndex = end;
bsw/jbe@1309 3015
bsw/jbe@1309 3016 while (true) {
bsw/jbe@1309 3017 if (nodeIndex == childNodeCount) {
bsw/jbe@1309 3018 containerElement.appendChild(workingNode);
bsw/jbe@1309 3019 } else {
bsw/jbe@1309 3020 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
bsw/jbe@1309 3021 }
bsw/jbe@1309 3022 workingRange.moveToElementText(workingNode);
bsw/jbe@1309 3023 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
bsw/jbe@1309 3024 if (comparison == 0 || start == end) {
bsw/jbe@1309 3025 break;
bsw/jbe@1309 3026 } else if (comparison == -1) {
bsw/jbe@1309 3027 if (end == start + 1) {
bsw/jbe@1309 3028 // We know the endth child node is after the range boundary, so we must be done.
bsw/jbe@1309 3029 break;
bsw/jbe@1309 3030 } else {
bsw/jbe@1309 3031 start = nodeIndex;
bsw/jbe@1309 3032 }
bsw/jbe@1309 3033 } else {
bsw/jbe@1309 3034 end = (end == start + 1) ? start : nodeIndex;
bsw/jbe@1309 3035 }
bsw/jbe@1309 3036 nodeIndex = Math.floor((start + end) / 2);
bsw/jbe@1309 3037 containerElement.removeChild(workingNode);
bsw/jbe@1309 3038 }
bsw/jbe@1309 3039
bsw/jbe@1309 3040
bsw/jbe@1309 3041 // We've now reached or gone past the boundary of the text range we're interested in
bsw/jbe@1309 3042 // so have identified the node we want
bsw/jbe@1309 3043 boundaryNode = workingNode.nextSibling;
bsw/jbe@1309 3044
bsw/jbe@1309 3045 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
bsw/jbe@1309 3046 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
bsw/jbe@1309 3047 // the node containing the text range's boundary, so we move the end of the working range to the
bsw/jbe@1309 3048 // boundary point and measure the length of its text to get the boundary's offset within the node.
bsw/jbe@1309 3049 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
bsw/jbe@1309 3050
bsw/jbe@1309 3051 var offset;
bsw/jbe@1309 3052
bsw/jbe@1309 3053 if (/[\r\n]/.test(boundaryNode.data)) {
bsw/jbe@1309 3054 /*
bsw/jbe@1309 3055 For the particular case of a boundary within a text node containing rendered line breaks (within a
bsw/jbe@1309 3056 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
bsw/jbe@1309 3057 IE. The facts:
bsw/jbe@1309 3058
bsw/jbe@1309 3059 - Each line break is represented as \r in the text node's data/nodeValue properties
bsw/jbe@1309 3060 - Each line break is represented as \r\n in the TextRange's 'text' property
bsw/jbe@1309 3061 - The 'text' property of the TextRange does not contain trailing line breaks
bsw/jbe@1309 3062
bsw/jbe@1309 3063 To get round the problem presented by the final fact above, we can use the fact that TextRange's
bsw/jbe@1309 3064 moveStart() and moveEnd() methods return the actual number of characters moved, which is not
bsw/jbe@1309 3065 necessarily the same as the number of characters it was instructed to move. The simplest approach is
bsw/jbe@1309 3066 to use this to store the characters moved when moving both the start and end of the range to the
bsw/jbe@1309 3067 start of the document body and subtracting the start offset from the end offset (the
bsw/jbe@1309 3068 "move-negative-gazillion" method). However, this is extremely slow when the document is large and
bsw/jbe@1309 3069 the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
bsw/jbe@1309 3070 the end of the document) has the same problem.
bsw/jbe@1309 3071
bsw/jbe@1309 3072 Another approach that works is to use moveStart() to move the start boundary of the range up to the
bsw/jbe@1309 3073 end boundary one character at a time and incrementing a counter with the value returned by the
bsw/jbe@1309 3074 moveStart() call. However, the check for whether the start boundary has reached the end boundary is
bsw/jbe@1309 3075 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
bsw/jbe@1309 3076 by the location of the range within the document).
bsw/jbe@1309 3077
bsw/jbe@1309 3078 The approach used below is a hybrid of the two methods above. It uses the fact that a string
bsw/jbe@1309 3079 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
bsw/jbe@1309 3080 be longer than the text of the TextRange, so the start of the range is moved that length initially
bsw/jbe@1309 3081 and then a character at a time to make up for any trailing line breaks not contained in the 'text'
bsw/jbe@1309 3082 property. This has good performance in most situations compared to the previous two methods.
bsw/jbe@1309 3083 */
bsw/jbe@1309 3084 var tempRange = workingRange.duplicate();
bsw/jbe@1309 3085 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
bsw/jbe@1309 3086
bsw/jbe@1309 3087 offset = tempRange.moveStart("character", rangeLength);
bsw/jbe@1309 3088 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
bsw/jbe@1309 3089 offset++;
bsw/jbe@1309 3090 tempRange.moveStart("character", 1);
bsw/jbe@1309 3091 }
bsw/jbe@1309 3092 } else {
bsw/jbe@1309 3093 offset = workingRange.text.length;
bsw/jbe@1309 3094 }
bsw/jbe@1309 3095 boundaryPosition = new DomPosition(boundaryNode, offset);
bsw/jbe@1309 3096 } else {
bsw/jbe@1309 3097
bsw/jbe@1309 3098 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
bsw/jbe@1309 3099 // a position within that, and likewise for a start boundary preceding a character data node
bsw/jbe@1309 3100 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
bsw/jbe@1309 3101 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
bsw/jbe@1309 3102 if (nextNode && isCharacterDataNode(nextNode)) {
bsw/jbe@1309 3103 boundaryPosition = new DomPosition(nextNode, 0);
bsw/jbe@1309 3104 } else if (previousNode && isCharacterDataNode(previousNode)) {
bsw/jbe@1309 3105 boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
bsw/jbe@1309 3106 } else {
bsw/jbe@1309 3107 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
bsw/jbe@1309 3108 }
bsw/jbe@1309 3109 }
bsw/jbe@1309 3110
bsw/jbe@1309 3111 // Clean up
bsw/jbe@1309 3112 dom.removeNode(workingNode);
bsw/jbe@1309 3113
bsw/jbe@1309 3114 return {
bsw/jbe@1309 3115 boundaryPosition: boundaryPosition,
bsw/jbe@1309 3116 nodeInfo: {
bsw/jbe@1309 3117 nodeIndex: nodeIndex,
bsw/jbe@1309 3118 containerElement: containerElement
bsw/jbe@1309 3119 }
bsw/jbe@1309 3120 };
bsw/jbe@1309 3121 };
bsw/jbe@1309 3122
bsw/jbe@1309 3123 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
bsw/jbe@1309 3124 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
bsw/jbe@1309 3125 // (http://code.google.com/p/ierange/)
bsw/jbe@1309 3126 var createBoundaryTextRange = function(boundaryPosition, isStart) {
bsw/jbe@1309 3127 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
bsw/jbe@1309 3128 var doc = dom.getDocument(boundaryPosition.node);
bsw/jbe@1309 3129 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
bsw/jbe@1309 3130 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
bsw/jbe@1309 3131
bsw/jbe@1309 3132 if (nodeIsDataNode) {
bsw/jbe@1309 3133 boundaryNode = boundaryPosition.node;
bsw/jbe@1309 3134 boundaryParent = boundaryNode.parentNode;
bsw/jbe@1309 3135 } else {
bsw/jbe@1309 3136 childNodes = boundaryPosition.node.childNodes;
bsw/jbe@1309 3137 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
bsw/jbe@1309 3138 boundaryParent = boundaryPosition.node;
bsw/jbe@1309 3139 }
bsw/jbe@1309 3140
bsw/jbe@1309 3141 // Position the range immediately before the node containing the boundary
bsw/jbe@1309 3142 workingNode = doc.createElement("span");
bsw/jbe@1309 3143
bsw/jbe@1309 3144 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
bsw/jbe@1309 3145 // the element rather than immediately before or after it
bsw/jbe@1309 3146 workingNode.innerHTML = "&#feff;";
bsw/jbe@1309 3147
bsw/jbe@1309 3148 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
bsw/jbe@1309 3149 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
bsw/jbe@1309 3150 if (boundaryNode) {
bsw/jbe@1309 3151 boundaryParent.insertBefore(workingNode, boundaryNode);
bsw/jbe@1309 3152 } else {
bsw/jbe@1309 3153 boundaryParent.appendChild(workingNode);
bsw/jbe@1309 3154 }
bsw/jbe@1309 3155
bsw/jbe@1309 3156 workingRange.moveToElementText(workingNode);
bsw/jbe@1309 3157 workingRange.collapse(!isStart);
bsw/jbe@1309 3158
bsw/jbe@1309 3159 // Clean up
bsw/jbe@1309 3160 boundaryParent.removeChild(workingNode);
bsw/jbe@1309 3161
bsw/jbe@1309 3162 // Move the working range to the text offset, if required
bsw/jbe@1309 3163 if (nodeIsDataNode) {
bsw/jbe@1309 3164 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
bsw/jbe@1309 3165 }
bsw/jbe@1309 3166
bsw/jbe@1309 3167 return workingRange;
bsw/jbe@1309 3168 };
bsw/jbe@1309 3169
bsw/jbe@1309 3170 /*------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 3171
bsw/jbe@1309 3172 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
bsw/jbe@1309 3173 // prototype
bsw/jbe@1309 3174
bsw/jbe@1309 3175 WrappedTextRange = function(textRange) {
bsw/jbe@1309 3176 this.textRange = textRange;
bsw/jbe@1309 3177 this.refresh();
bsw/jbe@1309 3178 };
bsw/jbe@1309 3179
bsw/jbe@1309 3180 WrappedTextRange.prototype = new DomRange(document);
bsw/jbe@1309 3181
bsw/jbe@1309 3182 WrappedTextRange.prototype.refresh = function() {
bsw/jbe@1309 3183 var start, end, startBoundary;
bsw/jbe@1309 3184
bsw/jbe@1309 3185 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
bsw/jbe@1309 3186 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
bsw/jbe@1309 3187
bsw/jbe@1309 3188 if (textRangeIsCollapsed(this.textRange)) {
bsw/jbe@1309 3189 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
bsw/jbe@1309 3190 true).boundaryPosition;
bsw/jbe@1309 3191 } else {
bsw/jbe@1309 3192 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
bsw/jbe@1309 3193 start = startBoundary.boundaryPosition;
bsw/jbe@1309 3194
bsw/jbe@1309 3195 // An optimization used here is that if the start and end boundaries have the same parent element, the
bsw/jbe@1309 3196 // search scope for the end boundary can be limited to exclude the portion of the element that precedes
bsw/jbe@1309 3197 // the start boundary
bsw/jbe@1309 3198 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
bsw/jbe@1309 3199 startBoundary.nodeInfo).boundaryPosition;
bsw/jbe@1309 3200 }
bsw/jbe@1309 3201
bsw/jbe@1309 3202 this.setStart(start.node, start.offset);
bsw/jbe@1309 3203 this.setEnd(end.node, end.offset);
bsw/jbe@1309 3204 };
bsw/jbe@1309 3205
bsw/jbe@1309 3206 WrappedTextRange.prototype.getName = function() {
bsw/jbe@1309 3207 return "WrappedTextRange";
bsw/jbe@1309 3208 };
bsw/jbe@1309 3209
bsw/jbe@1309 3210 DomRange.copyComparisonConstants(WrappedTextRange);
bsw/jbe@1309 3211
bsw/jbe@1309 3212 var rangeToTextRange = function(range) {
bsw/jbe@1309 3213 if (range.collapsed) {
bsw/jbe@1309 3214 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
bsw/jbe@1309 3215 } else {
bsw/jbe@1309 3216 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
bsw/jbe@1309 3217 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
bsw/jbe@1309 3218 var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
bsw/jbe@1309 3219 textRange.setEndPoint("StartToStart", startRange);
bsw/jbe@1309 3220 textRange.setEndPoint("EndToEnd", endRange);
bsw/jbe@1309 3221 return textRange;
bsw/jbe@1309 3222 }
bsw/jbe@1309 3223 };
bsw/jbe@1309 3224
bsw/jbe@1309 3225 WrappedTextRange.rangeToTextRange = rangeToTextRange;
bsw/jbe@1309 3226
bsw/jbe@1309 3227 WrappedTextRange.prototype.toTextRange = function() {
bsw/jbe@1309 3228 return rangeToTextRange(this);
bsw/jbe@1309 3229 };
bsw/jbe@1309 3230
bsw/jbe@1309 3231 api.WrappedTextRange = WrappedTextRange;
bsw/jbe@1309 3232
bsw/jbe@1309 3233 // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
bsw/jbe@1309 3234 // implementation to use by default.
bsw/jbe@1309 3235 if (!api.features.implementsDomRange || api.config.preferTextRange) {
bsw/jbe@1309 3236 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
bsw/jbe@1309 3237 var globalObj = (function(f) { return f("return this;")(); })(Function);
bsw/jbe@1309 3238 if (typeof globalObj.Range == "undefined") {
bsw/jbe@1309 3239 globalObj.Range = WrappedTextRange;
bsw/jbe@1309 3240 }
bsw/jbe@1309 3241
bsw/jbe@1309 3242 api.createNativeRange = function(doc) {
bsw/jbe@1309 3243 doc = getContentDocument(doc, module, "createNativeRange");
bsw/jbe@1309 3244 return getBody(doc).createTextRange();
bsw/jbe@1309 3245 };
bsw/jbe@1309 3246
bsw/jbe@1309 3247 api.WrappedRange = WrappedTextRange;
bsw/jbe@1309 3248 }
bsw/jbe@1309 3249 }
bsw/jbe@1309 3250
bsw/jbe@1309 3251 api.createRange = function(doc) {
bsw/jbe@1309 3252 doc = getContentDocument(doc, module, "createRange");
bsw/jbe@1309 3253 return new api.WrappedRange(api.createNativeRange(doc));
bsw/jbe@1309 3254 };
bsw/jbe@1309 3255
bsw/jbe@1309 3256 api.createRangyRange = function(doc) {
bsw/jbe@1309 3257 doc = getContentDocument(doc, module, "createRangyRange");
bsw/jbe@1309 3258 return new DomRange(doc);
bsw/jbe@1309 3259 };
bsw/jbe@1309 3260
bsw/jbe@1309 3261 util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
bsw/jbe@1309 3262 util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
bsw/jbe@1309 3263
bsw/jbe@1309 3264 api.addShimListener(function(win) {
bsw/jbe@1309 3265 var doc = win.document;
bsw/jbe@1309 3266 if (typeof doc.createRange == "undefined") {
bsw/jbe@1309 3267 doc.createRange = function() {
bsw/jbe@1309 3268 return api.createRange(doc);
bsw/jbe@1309 3269 };
bsw/jbe@1309 3270 }
bsw/jbe@1309 3271 doc = win = null;
bsw/jbe@1309 3272 });
bsw/jbe@1309 3273 });
bsw/jbe@1309 3274
bsw/jbe@1309 3275 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 3276
bsw/jbe@1309 3277 // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
bsw/jbe@1309 3278 // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
bsw/jbe@1309 3279 api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
bsw/jbe@1309 3280 api.config.checkSelectionRanges = true;
bsw/jbe@1309 3281
bsw/jbe@1309 3282 var BOOLEAN = "boolean";
bsw/jbe@1309 3283 var NUMBER = "number";
bsw/jbe@1309 3284 var dom = api.dom;
bsw/jbe@1309 3285 var util = api.util;
bsw/jbe@1309 3286 var isHostMethod = util.isHostMethod;
bsw/jbe@1309 3287 var DomRange = api.DomRange;
bsw/jbe@1309 3288 var WrappedRange = api.WrappedRange;
bsw/jbe@1309 3289 var DOMException = api.DOMException;
bsw/jbe@1309 3290 var DomPosition = dom.DomPosition;
bsw/jbe@1309 3291 var getNativeSelection;
bsw/jbe@1309 3292 var selectionIsCollapsed;
bsw/jbe@1309 3293 var features = api.features;
bsw/jbe@1309 3294 var CONTROL = "Control";
bsw/jbe@1309 3295 var getDocument = dom.getDocument;
bsw/jbe@1309 3296 var getBody = dom.getBody;
bsw/jbe@1309 3297 var rangesEqual = DomRange.rangesEqual;
bsw/jbe@1309 3298
bsw/jbe@1309 3299
bsw/jbe@1309 3300 // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
bsw/jbe@1309 3301 // "forward" or "forwards") or a Boolean (true for backwards).
bsw/jbe@1309 3302 function isDirectionBackward(dir) {
bsw/jbe@1309 3303 return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
bsw/jbe@1309 3304 }
bsw/jbe@1309 3305
bsw/jbe@1309 3306 function getWindow(win, methodName) {
bsw/jbe@1309 3307 if (!win) {
bsw/jbe@1309 3308 return window;
bsw/jbe@1309 3309 } else if (dom.isWindow(win)) {
bsw/jbe@1309 3310 return win;
bsw/jbe@1309 3311 } else if (win instanceof WrappedSelection) {
bsw/jbe@1309 3312 return win.win;
bsw/jbe@1309 3313 } else {
bsw/jbe@1309 3314 var doc = dom.getContentDocument(win, module, methodName);
bsw/jbe@1309 3315 return dom.getWindow(doc);
bsw/jbe@1309 3316 }
bsw/jbe@1309 3317 }
bsw/jbe@1309 3318
bsw/jbe@1309 3319 function getWinSelection(winParam) {
bsw/jbe@1309 3320 return getWindow(winParam, "getWinSelection").getSelection();
bsw/jbe@1309 3321 }
bsw/jbe@1309 3322
bsw/jbe@1309 3323 function getDocSelection(winParam) {
bsw/jbe@1309 3324 return getWindow(winParam, "getDocSelection").document.selection;
bsw/jbe@1309 3325 }
bsw/jbe@1309 3326
bsw/jbe@1309 3327 function winSelectionIsBackward(sel) {
bsw/jbe@1309 3328 var backward = false;
bsw/jbe@1309 3329 if (sel.anchorNode) {
bsw/jbe@1309 3330 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
bsw/jbe@1309 3331 }
bsw/jbe@1309 3332 return backward;
bsw/jbe@1309 3333 }
bsw/jbe@1309 3334
bsw/jbe@1309 3335 // Test for the Range/TextRange and Selection features required
bsw/jbe@1309 3336 // Test for ability to retrieve selection
bsw/jbe@1309 3337 var implementsWinGetSelection = isHostMethod(window, "getSelection"),
bsw/jbe@1309 3338 implementsDocSelection = util.isHostObject(document, "selection");
bsw/jbe@1309 3339
bsw/jbe@1309 3340 features.implementsWinGetSelection = implementsWinGetSelection;
bsw/jbe@1309 3341 features.implementsDocSelection = implementsDocSelection;
bsw/jbe@1309 3342
bsw/jbe@1309 3343 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
bsw/jbe@1309 3344
bsw/jbe@1309 3345 if (useDocumentSelection) {
bsw/jbe@1309 3346 getNativeSelection = getDocSelection;
bsw/jbe@1309 3347 api.isSelectionValid = function(winParam) {
bsw/jbe@1309 3348 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
bsw/jbe@1309 3349
bsw/jbe@1309 3350 // Check whether the selection TextRange is actually contained within the correct document
bsw/jbe@1309 3351 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
bsw/jbe@1309 3352 };
bsw/jbe@1309 3353 } else if (implementsWinGetSelection) {
bsw/jbe@1309 3354 getNativeSelection = getWinSelection;
bsw/jbe@1309 3355 api.isSelectionValid = function() {
bsw/jbe@1309 3356 return true;
bsw/jbe@1309 3357 };
bsw/jbe@1309 3358 } else {
bsw/jbe@1309 3359 module.fail("Neither document.selection or window.getSelection() detected.");
bsw/jbe@1309 3360 return false;
bsw/jbe@1309 3361 }
bsw/jbe@1309 3362
bsw/jbe@1309 3363 api.getNativeSelection = getNativeSelection;
bsw/jbe@1309 3364
bsw/jbe@1309 3365 var testSelection = getNativeSelection();
bsw/jbe@1309 3366
bsw/jbe@1309 3367 // In Firefox, the selection is null in an iframe with display: none. See issue #138.
bsw/jbe@1309 3368 if (!testSelection) {
bsw/jbe@1309 3369 module.fail("Native selection was null (possibly issue 138?)");
bsw/jbe@1309 3370 return false;
bsw/jbe@1309 3371 }
bsw/jbe@1309 3372
bsw/jbe@1309 3373 var testRange = api.createNativeRange(document);
bsw/jbe@1309 3374 var body = getBody(document);
bsw/jbe@1309 3375
bsw/jbe@1309 3376 // Obtaining a range from a selection
bsw/jbe@1309 3377 var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
bsw/jbe@1309 3378 ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
bsw/jbe@1309 3379
bsw/jbe@1309 3380 features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
bsw/jbe@1309 3381
bsw/jbe@1309 3382 // Test for existence of native selection extend() method
bsw/jbe@1309 3383 var selectionHasExtend = isHostMethod(testSelection, "extend");
bsw/jbe@1309 3384 features.selectionHasExtend = selectionHasExtend;
bsw/jbe@1309 3385
bsw/jbe@1309 3386 // Test if rangeCount exists
bsw/jbe@1309 3387 var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
bsw/jbe@1309 3388 features.selectionHasRangeCount = selectionHasRangeCount;
bsw/jbe@1309 3389
bsw/jbe@1309 3390 var selectionSupportsMultipleRanges = false;
bsw/jbe@1309 3391 var collapsedNonEditableSelectionsSupported = true;
bsw/jbe@1309 3392
bsw/jbe@1309 3393 var addRangeBackwardToNative = selectionHasExtend ?
bsw/jbe@1309 3394 function(nativeSelection, range) {
bsw/jbe@1309 3395 var doc = DomRange.getRangeDocument(range);
bsw/jbe@1309 3396 var endRange = api.createRange(doc);
bsw/jbe@1309 3397 endRange.collapseToPoint(range.endContainer, range.endOffset);
bsw/jbe@1309 3398 nativeSelection.addRange(getNativeRange(endRange));
bsw/jbe@1309 3399 nativeSelection.extend(range.startContainer, range.startOffset);
bsw/jbe@1309 3400 } : null;
bsw/jbe@1309 3401
bsw/jbe@1309 3402 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
bsw/jbe@1309 3403 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
bsw/jbe@1309 3404
bsw/jbe@1309 3405 (function() {
bsw/jbe@1309 3406 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
bsw/jbe@1309 3407 // performed on the current document's selection. See issue 109.
bsw/jbe@1309 3408
bsw/jbe@1309 3409 // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
bsw/jbe@1309 3410 // will result in the selection direction begin reversed if the original selection was backwards and the
bsw/jbe@1309 3411 // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
bsw/jbe@1309 3412 var sel = window.getSelection();
bsw/jbe@1309 3413 if (sel) {
bsw/jbe@1309 3414 // Store the current selection
bsw/jbe@1309 3415 var originalSelectionRangeCount = sel.rangeCount;
bsw/jbe@1309 3416 var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
bsw/jbe@1309 3417 var originalSelectionRanges = [];
bsw/jbe@1309 3418 var originalSelectionBackward = winSelectionIsBackward(sel);
bsw/jbe@1309 3419 for (var i = 0; i < originalSelectionRangeCount; ++i) {
bsw/jbe@1309 3420 originalSelectionRanges[i] = sel.getRangeAt(i);
bsw/jbe@1309 3421 }
bsw/jbe@1309 3422
bsw/jbe@1309 3423 // Create some test elements
bsw/jbe@1309 3424 var testEl = dom.createTestElement(document, "", false);
bsw/jbe@1309 3425 var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
bsw/jbe@1309 3426
bsw/jbe@1309 3427 // Test whether the native selection will allow a collapsed selection within a non-editable element
bsw/jbe@1309 3428 var r1 = document.createRange();
bsw/jbe@1309 3429
bsw/jbe@1309 3430 r1.setStart(textNode, 1);
bsw/jbe@1309 3431 r1.collapse(true);
bsw/jbe@1309 3432 sel.removeAllRanges();
bsw/jbe@1309 3433 sel.addRange(r1);
bsw/jbe@1309 3434 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
bsw/jbe@1309 3435 sel.removeAllRanges();
bsw/jbe@1309 3436
bsw/jbe@1309 3437 // Test whether the native selection is capable of supporting multiple ranges.
bsw/jbe@1309 3438 if (!selectionHasMultipleRanges) {
bsw/jbe@1309 3439 // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
bsw/jbe@1309 3440 // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
bsw/jbe@1309 3441 // nothing we can do about this while retaining the feature test so we have to resort to a browser
bsw/jbe@1309 3442 // sniff. I'm not happy about it. See
bsw/jbe@1309 3443 // https://code.google.com/p/chromium/issues/detail?id=399791
bsw/jbe@1309 3444 var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
bsw/jbe@1309 3445 if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
bsw/jbe@1309 3446 selectionSupportsMultipleRanges = false;
bsw/jbe@1309 3447 } else {
bsw/jbe@1309 3448 var r2 = r1.cloneRange();
bsw/jbe@1309 3449 r1.setStart(textNode, 0);
bsw/jbe@1309 3450 r2.setEnd(textNode, 3);
bsw/jbe@1309 3451 r2.setStart(textNode, 2);
bsw/jbe@1309 3452 sel.addRange(r1);
bsw/jbe@1309 3453 sel.addRange(r2);
bsw/jbe@1309 3454 selectionSupportsMultipleRanges = (sel.rangeCount == 2);
bsw/jbe@1309 3455 }
bsw/jbe@1309 3456 }
bsw/jbe@1309 3457
bsw/jbe@1309 3458 // Clean up
bsw/jbe@1309 3459 dom.removeNode(testEl);
bsw/jbe@1309 3460 sel.removeAllRanges();
bsw/jbe@1309 3461
bsw/jbe@1309 3462 for (i = 0; i < originalSelectionRangeCount; ++i) {
bsw/jbe@1309 3463 if (i == 0 && originalSelectionBackward) {
bsw/jbe@1309 3464 if (addRangeBackwardToNative) {
bsw/jbe@1309 3465 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
bsw/jbe@1309 3466 } else {
bsw/jbe@1309 3467 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
bsw/jbe@1309 3468 sel.addRange(originalSelectionRanges[i]);
bsw/jbe@1309 3469 }
bsw/jbe@1309 3470 } else {
bsw/jbe@1309 3471 sel.addRange(originalSelectionRanges[i]);
bsw/jbe@1309 3472 }
bsw/jbe@1309 3473 }
bsw/jbe@1309 3474 }
bsw/jbe@1309 3475 })();
bsw/jbe@1309 3476 }
bsw/jbe@1309 3477
bsw/jbe@1309 3478 features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
bsw/jbe@1309 3479 features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
bsw/jbe@1309 3480
bsw/jbe@1309 3481 // ControlRanges
bsw/jbe@1309 3482 var implementsControlRange = false, testControlRange;
bsw/jbe@1309 3483
bsw/jbe@1309 3484 if (body && isHostMethod(body, "createControlRange")) {
bsw/jbe@1309 3485 testControlRange = body.createControlRange();
bsw/jbe@1309 3486 if (util.areHostProperties(testControlRange, ["item", "add"])) {
bsw/jbe@1309 3487 implementsControlRange = true;
bsw/jbe@1309 3488 }
bsw/jbe@1309 3489 }
bsw/jbe@1309 3490 features.implementsControlRange = implementsControlRange;
bsw/jbe@1309 3491
bsw/jbe@1309 3492 // Selection collapsedness
bsw/jbe@1309 3493 if (selectionHasAnchorAndFocus) {
bsw/jbe@1309 3494 selectionIsCollapsed = function(sel) {
bsw/jbe@1309 3495 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
bsw/jbe@1309 3496 };
bsw/jbe@1309 3497 } else {
bsw/jbe@1309 3498 selectionIsCollapsed = function(sel) {
bsw/jbe@1309 3499 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
bsw/jbe@1309 3500 };
bsw/jbe@1309 3501 }
bsw/jbe@1309 3502
bsw/jbe@1309 3503 function updateAnchorAndFocusFromRange(sel, range, backward) {
bsw/jbe@1309 3504 var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
bsw/jbe@1309 3505 sel.anchorNode = range[anchorPrefix + "Container"];
bsw/jbe@1309 3506 sel.anchorOffset = range[anchorPrefix + "Offset"];
bsw/jbe@1309 3507 sel.focusNode = range[focusPrefix + "Container"];
bsw/jbe@1309 3508 sel.focusOffset = range[focusPrefix + "Offset"];
bsw/jbe@1309 3509 }
bsw/jbe@1309 3510
bsw/jbe@1309 3511 function updateAnchorAndFocusFromNativeSelection(sel) {
bsw/jbe@1309 3512 var nativeSel = sel.nativeSelection;
bsw/jbe@1309 3513 sel.anchorNode = nativeSel.anchorNode;
bsw/jbe@1309 3514 sel.anchorOffset = nativeSel.anchorOffset;
bsw/jbe@1309 3515 sel.focusNode = nativeSel.focusNode;
bsw/jbe@1309 3516 sel.focusOffset = nativeSel.focusOffset;
bsw/jbe@1309 3517 }
bsw/jbe@1309 3518
bsw/jbe@1309 3519 function updateEmptySelection(sel) {
bsw/jbe@1309 3520 sel.anchorNode = sel.focusNode = null;
bsw/jbe@1309 3521 sel.anchorOffset = sel.focusOffset = 0;
bsw/jbe@1309 3522 sel.rangeCount = 0;
bsw/jbe@1309 3523 sel.isCollapsed = true;
bsw/jbe@1309 3524 sel._ranges.length = 0;
bsw/jbe@1309 3525 }
bsw/jbe@1309 3526
bsw/jbe@1309 3527 function getNativeRange(range) {
bsw/jbe@1309 3528 var nativeRange;
bsw/jbe@1309 3529 if (range instanceof DomRange) {
bsw/jbe@1309 3530 nativeRange = api.createNativeRange(range.getDocument());
bsw/jbe@1309 3531 nativeRange.setEnd(range.endContainer, range.endOffset);
bsw/jbe@1309 3532 nativeRange.setStart(range.startContainer, range.startOffset);
bsw/jbe@1309 3533 } else if (range instanceof WrappedRange) {
bsw/jbe@1309 3534 nativeRange = range.nativeRange;
bsw/jbe@1309 3535 } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
bsw/jbe@1309 3536 nativeRange = range;
bsw/jbe@1309 3537 }
bsw/jbe@1309 3538 return nativeRange;
bsw/jbe@1309 3539 }
bsw/jbe@1309 3540
bsw/jbe@1309 3541 function rangeContainsSingleElement(rangeNodes) {
bsw/jbe@1309 3542 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
bsw/jbe@1309 3543 return false;
bsw/jbe@1309 3544 }
bsw/jbe@1309 3545 for (var i = 1, len = rangeNodes.length; i < len; ++i) {
bsw/jbe@1309 3546 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
bsw/jbe@1309 3547 return false;
bsw/jbe@1309 3548 }
bsw/jbe@1309 3549 }
bsw/jbe@1309 3550 return true;
bsw/jbe@1309 3551 }
bsw/jbe@1309 3552
bsw/jbe@1309 3553 function getSingleElementFromRange(range) {
bsw/jbe@1309 3554 var nodes = range.getNodes();
bsw/jbe@1309 3555 if (!rangeContainsSingleElement(nodes)) {
bsw/jbe@1309 3556 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
bsw/jbe@1309 3557 }
bsw/jbe@1309 3558 return nodes[0];
bsw/jbe@1309 3559 }
bsw/jbe@1309 3560
bsw/jbe@1309 3561 // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
bsw/jbe@1309 3562 function isTextRange(range) {
bsw/jbe@1309 3563 return !!range && typeof range.text != "undefined";
bsw/jbe@1309 3564 }
bsw/jbe@1309 3565
bsw/jbe@1309 3566 function updateFromTextRange(sel, range) {
bsw/jbe@1309 3567 // Create a Range from the selected TextRange
bsw/jbe@1309 3568 var wrappedRange = new WrappedRange(range);
bsw/jbe@1309 3569 sel._ranges = [wrappedRange];
bsw/jbe@1309 3570
bsw/jbe@1309 3571 updateAnchorAndFocusFromRange(sel, wrappedRange, false);
bsw/jbe@1309 3572 sel.rangeCount = 1;
bsw/jbe@1309 3573 sel.isCollapsed = wrappedRange.collapsed;
bsw/jbe@1309 3574 }
bsw/jbe@1309 3575
bsw/jbe@1309 3576 function updateControlSelection(sel) {
bsw/jbe@1309 3577 // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309 3578 sel._ranges.length = 0;
bsw/jbe@1309 3579 if (sel.docSelection.type == "None") {
bsw/jbe@1309 3580 updateEmptySelection(sel);
bsw/jbe@1309 3581 } else {
bsw/jbe@1309 3582 var controlRange = sel.docSelection.createRange();
bsw/jbe@1309 3583 if (isTextRange(controlRange)) {
bsw/jbe@1309 3584 // This case (where the selection type is "Control" and calling createRange() on the selection returns
bsw/jbe@1309 3585 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
bsw/jbe@1309 3586 // ControlRange have been removed from the ControlRange and removed from the document.
bsw/jbe@1309 3587 updateFromTextRange(sel, controlRange);
bsw/jbe@1309 3588 } else {
bsw/jbe@1309 3589 sel.rangeCount = controlRange.length;
bsw/jbe@1309 3590 var range, doc = getDocument(controlRange.item(0));
bsw/jbe@1309 3591 for (var i = 0; i < sel.rangeCount; ++i) {
bsw/jbe@1309 3592 range = api.createRange(doc);
bsw/jbe@1309 3593 range.selectNode(controlRange.item(i));
bsw/jbe@1309 3594 sel._ranges.push(range);
bsw/jbe@1309 3595 }
bsw/jbe@1309 3596 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
bsw/jbe@1309 3597 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
bsw/jbe@1309 3598 }
bsw/jbe@1309 3599 }
bsw/jbe@1309 3600 }
bsw/jbe@1309 3601
bsw/jbe@1309 3602 function addRangeToControlSelection(sel, range) {
bsw/jbe@1309 3603 var controlRange = sel.docSelection.createRange();
bsw/jbe@1309 3604 var rangeElement = getSingleElementFromRange(range);
bsw/jbe@1309 3605
bsw/jbe@1309 3606 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
bsw/jbe@1309 3607 // contained by the supplied range
bsw/jbe@1309 3608 var doc = getDocument(controlRange.item(0));
bsw/jbe@1309 3609 var newControlRange = getBody(doc).createControlRange();
bsw/jbe@1309 3610 for (var i = 0, len = controlRange.length; i < len; ++i) {
bsw/jbe@1309 3611 newControlRange.add(controlRange.item(i));
bsw/jbe@1309 3612 }
bsw/jbe@1309 3613 try {
bsw/jbe@1309 3614 newControlRange.add(rangeElement);
bsw/jbe@1309 3615 } catch (ex) {
bsw/jbe@1309 3616 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
bsw/jbe@1309 3617 }
bsw/jbe@1309 3618 newControlRange.select();
bsw/jbe@1309 3619
bsw/jbe@1309 3620 // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309 3621 updateControlSelection(sel);
bsw/jbe@1309 3622 }
bsw/jbe@1309 3623
bsw/jbe@1309 3624 var getSelectionRangeAt;
bsw/jbe@1309 3625
bsw/jbe@1309 3626 if (isHostMethod(testSelection, "getRangeAt")) {
bsw/jbe@1309 3627 // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
bsw/jbe@1309 3628 // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
bsw/jbe@1309 3629 // lesson to us all, especially me.
bsw/jbe@1309 3630 getSelectionRangeAt = function(sel, index) {
bsw/jbe@1309 3631 try {
bsw/jbe@1309 3632 return sel.getRangeAt(index);
bsw/jbe@1309 3633 } catch (ex) {
bsw/jbe@1309 3634 return null;
bsw/jbe@1309 3635 }
bsw/jbe@1309 3636 };
bsw/jbe@1309 3637 } else if (selectionHasAnchorAndFocus) {
bsw/jbe@1309 3638 getSelectionRangeAt = function(sel) {
bsw/jbe@1309 3639 var doc = getDocument(sel.anchorNode);
bsw/jbe@1309 3640 var range = api.createRange(doc);
bsw/jbe@1309 3641 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
bsw/jbe@1309 3642
bsw/jbe@1309 3643 // Handle the case when the selection was selected backwards (from the end to the start in the
bsw/jbe@1309 3644 // document)
bsw/jbe@1309 3645 if (range.collapsed !== this.isCollapsed) {
bsw/jbe@1309 3646 range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
bsw/jbe@1309 3647 }
bsw/jbe@1309 3648
bsw/jbe@1309 3649 return range;
bsw/jbe@1309 3650 };
bsw/jbe@1309 3651 }
bsw/jbe@1309 3652
bsw/jbe@1309 3653 function WrappedSelection(selection, docSelection, win) {
bsw/jbe@1309 3654 this.nativeSelection = selection;
bsw/jbe@1309 3655 this.docSelection = docSelection;
bsw/jbe@1309 3656 this._ranges = [];
bsw/jbe@1309 3657 this.win = win;
bsw/jbe@1309 3658 this.refresh();
bsw/jbe@1309 3659 }
bsw/jbe@1309 3660
bsw/jbe@1309 3661 WrappedSelection.prototype = api.selectionPrototype;
bsw/jbe@1309 3662
bsw/jbe@1309 3663 function deleteProperties(sel) {
bsw/jbe@1309 3664 sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
bsw/jbe@1309 3665 sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
bsw/jbe@1309 3666 sel.detached = true;
bsw/jbe@1309 3667 }
bsw/jbe@1309 3668
bsw/jbe@1309 3669 var cachedRangySelections = [];
bsw/jbe@1309 3670
bsw/jbe@1309 3671 function actOnCachedSelection(win, action) {
bsw/jbe@1309 3672 var i = cachedRangySelections.length, cached, sel;
bsw/jbe@1309 3673 while (i--) {
bsw/jbe@1309 3674 cached = cachedRangySelections[i];
bsw/jbe@1309 3675 sel = cached.selection;
bsw/jbe@1309 3676 if (action == "deleteAll") {
bsw/jbe@1309 3677 deleteProperties(sel);
bsw/jbe@1309 3678 } else if (cached.win == win) {
bsw/jbe@1309 3679 if (action == "delete") {
bsw/jbe@1309 3680 cachedRangySelections.splice(i, 1);
bsw/jbe@1309 3681 return true;
bsw/jbe@1309 3682 } else {
bsw/jbe@1309 3683 return sel;
bsw/jbe@1309 3684 }
bsw/jbe@1309 3685 }
bsw/jbe@1309 3686 }
bsw/jbe@1309 3687 if (action == "deleteAll") {
bsw/jbe@1309 3688 cachedRangySelections.length = 0;
bsw/jbe@1309 3689 }
bsw/jbe@1309 3690 return null;
bsw/jbe@1309 3691 }
bsw/jbe@1309 3692
bsw/jbe@1309 3693 var getSelection = function(win) {
bsw/jbe@1309 3694 // Check if the parameter is a Rangy Selection object
bsw/jbe@1309 3695 if (win && win instanceof WrappedSelection) {
bsw/jbe@1309 3696 win.refresh();
bsw/jbe@1309 3697 return win;
bsw/jbe@1309 3698 }
bsw/jbe@1309 3699
bsw/jbe@1309 3700 win = getWindow(win, "getNativeSelection");
bsw/jbe@1309 3701
bsw/jbe@1309 3702 var sel = actOnCachedSelection(win);
bsw/jbe@1309 3703 var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
bsw/jbe@1309 3704 if (sel) {
bsw/jbe@1309 3705 sel.nativeSelection = nativeSel;
bsw/jbe@1309 3706 sel.docSelection = docSel;
bsw/jbe@1309 3707 sel.refresh();
bsw/jbe@1309 3708 } else {
bsw/jbe@1309 3709 sel = new WrappedSelection(nativeSel, docSel, win);
bsw/jbe@1309 3710 cachedRangySelections.push( { win: win, selection: sel } );
bsw/jbe@1309 3711 }
bsw/jbe@1309 3712 return sel;
bsw/jbe@1309 3713 };
bsw/jbe@1309 3714
bsw/jbe@1309 3715 api.getSelection = getSelection;
bsw/jbe@1309 3716
bsw/jbe@1309 3717 util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
bsw/jbe@1309 3718
bsw/jbe@1309 3719 var selProto = WrappedSelection.prototype;
bsw/jbe@1309 3720
bsw/jbe@1309 3721 function createControlSelection(sel, ranges) {
bsw/jbe@1309 3722 // Ensure that the selection becomes of type "Control"
bsw/jbe@1309 3723 var doc = getDocument(ranges[0].startContainer);
bsw/jbe@1309 3724 var controlRange = getBody(doc).createControlRange();
bsw/jbe@1309 3725 for (var i = 0, el, len = ranges.length; i < len; ++i) {
bsw/jbe@1309 3726 el = getSingleElementFromRange(ranges[i]);
bsw/jbe@1309 3727 try {
bsw/jbe@1309 3728 controlRange.add(el);
bsw/jbe@1309 3729 } catch (ex) {
bsw/jbe@1309 3730 throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
bsw/jbe@1309 3731 }
bsw/jbe@1309 3732 }
bsw/jbe@1309 3733 controlRange.select();
bsw/jbe@1309 3734
bsw/jbe@1309 3735 // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309 3736 updateControlSelection(sel);
bsw/jbe@1309 3737 }
bsw/jbe@1309 3738
bsw/jbe@1309 3739 // Selecting a range
bsw/jbe@1309 3740 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
bsw/jbe@1309 3741 selProto.removeAllRanges = function() {
bsw/jbe@1309 3742 this.nativeSelection.removeAllRanges();
bsw/jbe@1309 3743 updateEmptySelection(this);
bsw/jbe@1309 3744 };
bsw/jbe@1309 3745
bsw/jbe@1309 3746 var addRangeBackward = function(sel, range) {
bsw/jbe@1309 3747 addRangeBackwardToNative(sel.nativeSelection, range);
bsw/jbe@1309 3748 sel.refresh();
bsw/jbe@1309 3749 };
bsw/jbe@1309 3750
bsw/jbe@1309 3751 if (selectionHasRangeCount) {
bsw/jbe@1309 3752 selProto.addRange = function(range, direction) {
bsw/jbe@1309 3753 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
bsw/jbe@1309 3754 addRangeToControlSelection(this, range);
bsw/jbe@1309 3755 } else {
bsw/jbe@1309 3756 if (isDirectionBackward(direction) && selectionHasExtend) {
bsw/jbe@1309 3757 addRangeBackward(this, range);
bsw/jbe@1309 3758 } else {
bsw/jbe@1309 3759 var previousRangeCount;
bsw/jbe@1309 3760 if (selectionSupportsMultipleRanges) {
bsw/jbe@1309 3761 previousRangeCount = this.rangeCount;
bsw/jbe@1309 3762 } else {
bsw/jbe@1309 3763 this.removeAllRanges();
bsw/jbe@1309 3764 previousRangeCount = 0;
bsw/jbe@1309 3765 }
bsw/jbe@1309 3766 // Clone the native range so that changing the selected range does not affect the selection.
bsw/jbe@1309 3767 // This is contrary to the spec but is the only way to achieve consistency between browsers. See
bsw/jbe@1309 3768 // issue 80.
bsw/jbe@1309 3769 var clonedNativeRange = getNativeRange(range).cloneRange();
bsw/jbe@1309 3770 try {
bsw/jbe@1309 3771 this.nativeSelection.addRange(clonedNativeRange);
bsw/jbe@1309 3772 } catch (ex) {
bsw/jbe@1309 3773 }
bsw/jbe@1309 3774
bsw/jbe@1309 3775 // Check whether adding the range was successful
bsw/jbe@1309 3776 this.rangeCount = this.nativeSelection.rangeCount;
bsw/jbe@1309 3777
bsw/jbe@1309 3778 if (this.rangeCount == previousRangeCount + 1) {
bsw/jbe@1309 3779 // The range was added successfully
bsw/jbe@1309 3780
bsw/jbe@1309 3781 // Check whether the range that we added to the selection is reflected in the last range extracted from
bsw/jbe@1309 3782 // the selection
bsw/jbe@1309 3783 if (api.config.checkSelectionRanges) {
bsw/jbe@1309 3784 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
bsw/jbe@1309 3785 if (nativeRange && !rangesEqual(nativeRange, range)) {
bsw/jbe@1309 3786 // Happens in WebKit with, for example, a selection placed at the start of a text node
bsw/jbe@1309 3787 range = new WrappedRange(nativeRange);
bsw/jbe@1309 3788 }
bsw/jbe@1309 3789 }
bsw/jbe@1309 3790 this._ranges[this.rangeCount - 1] = range;
bsw/jbe@1309 3791 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
bsw/jbe@1309 3792 this.isCollapsed = selectionIsCollapsed(this);
bsw/jbe@1309 3793 } else {
bsw/jbe@1309 3794 // The range was not added successfully. The simplest thing is to refresh
bsw/jbe@1309 3795 this.refresh();
bsw/jbe@1309 3796 }
bsw/jbe@1309 3797 }
bsw/jbe@1309 3798 }
bsw/jbe@1309 3799 };
bsw/jbe@1309 3800 } else {
bsw/jbe@1309 3801 selProto.addRange = function(range, direction) {
bsw/jbe@1309 3802 if (isDirectionBackward(direction) && selectionHasExtend) {
bsw/jbe@1309 3803 addRangeBackward(this, range);
bsw/jbe@1309 3804 } else {
bsw/jbe@1309 3805 this.nativeSelection.addRange(getNativeRange(range));
bsw/jbe@1309 3806 this.refresh();
bsw/jbe@1309 3807 }
bsw/jbe@1309 3808 };
bsw/jbe@1309 3809 }
bsw/jbe@1309 3810
bsw/jbe@1309 3811 selProto.setRanges = function(ranges) {
bsw/jbe@1309 3812 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
bsw/jbe@1309 3813 createControlSelection(this, ranges);
bsw/jbe@1309 3814 } else {
bsw/jbe@1309 3815 this.removeAllRanges();
bsw/jbe@1309 3816 for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309 3817 this.addRange(ranges[i]);
bsw/jbe@1309 3818 }
bsw/jbe@1309 3819 }
bsw/jbe@1309 3820 };
bsw/jbe@1309 3821 } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
bsw/jbe@1309 3822 implementsControlRange && useDocumentSelection) {
bsw/jbe@1309 3823
bsw/jbe@1309 3824 selProto.removeAllRanges = function() {
bsw/jbe@1309 3825 // Added try/catch as fix for issue #21
bsw/jbe@1309 3826 try {
bsw/jbe@1309 3827 this.docSelection.empty();
bsw/jbe@1309 3828
bsw/jbe@1309 3829 // Check for empty() not working (issue #24)
bsw/jbe@1309 3830 if (this.docSelection.type != "None") {
bsw/jbe@1309 3831 // Work around failure to empty a control selection by instead selecting a TextRange and then
bsw/jbe@1309 3832 // calling empty()
bsw/jbe@1309 3833 var doc;
bsw/jbe@1309 3834 if (this.anchorNode) {
bsw/jbe@1309 3835 doc = getDocument(this.anchorNode);
bsw/jbe@1309 3836 } else if (this.docSelection.type == CONTROL) {
bsw/jbe@1309 3837 var controlRange = this.docSelection.createRange();
bsw/jbe@1309 3838 if (controlRange.length) {
bsw/jbe@1309 3839 doc = getDocument( controlRange.item(0) );
bsw/jbe@1309 3840 }
bsw/jbe@1309 3841 }
bsw/jbe@1309 3842 if (doc) {
bsw/jbe@1309 3843 var textRange = getBody(doc).createTextRange();
bsw/jbe@1309 3844 textRange.select();
bsw/jbe@1309 3845 this.docSelection.empty();
bsw/jbe@1309 3846 }
bsw/jbe@1309 3847 }
bsw/jbe@1309 3848 } catch(ex) {}
bsw/jbe@1309 3849 updateEmptySelection(this);
bsw/jbe@1309 3850 };
bsw/jbe@1309 3851
bsw/jbe@1309 3852 selProto.addRange = function(range) {
bsw/jbe@1309 3853 if (this.docSelection.type == CONTROL) {
bsw/jbe@1309 3854 addRangeToControlSelection(this, range);
bsw/jbe@1309 3855 } else {
bsw/jbe@1309 3856 api.WrappedTextRange.rangeToTextRange(range).select();
bsw/jbe@1309 3857 this._ranges[0] = range;
bsw/jbe@1309 3858 this.rangeCount = 1;
bsw/jbe@1309 3859 this.isCollapsed = this._ranges[0].collapsed;
bsw/jbe@1309 3860 updateAnchorAndFocusFromRange(this, range, false);
bsw/jbe@1309 3861 }
bsw/jbe@1309 3862 };
bsw/jbe@1309 3863
bsw/jbe@1309 3864 selProto.setRanges = function(ranges) {
bsw/jbe@1309 3865 this.removeAllRanges();
bsw/jbe@1309 3866 var rangeCount = ranges.length;
bsw/jbe@1309 3867 if (rangeCount > 1) {
bsw/jbe@1309 3868 createControlSelection(this, ranges);
bsw/jbe@1309 3869 } else if (rangeCount) {
bsw/jbe@1309 3870 this.addRange(ranges[0]);
bsw/jbe@1309 3871 }
bsw/jbe@1309 3872 };
bsw/jbe@1309 3873 } else {
bsw/jbe@1309 3874 module.fail("No means of selecting a Range or TextRange was found");
bsw/jbe@1309 3875 return false;
bsw/jbe@1309 3876 }
bsw/jbe@1309 3877
bsw/jbe@1309 3878 selProto.getRangeAt = function(index) {
bsw/jbe@1309 3879 if (index < 0 || index >= this.rangeCount) {
bsw/jbe@1309 3880 throw new DOMException("INDEX_SIZE_ERR");
bsw/jbe@1309 3881 } else {
bsw/jbe@1309 3882 // Clone the range to preserve selection-range independence. See issue 80.
bsw/jbe@1309 3883 return this._ranges[index].cloneRange();
bsw/jbe@1309 3884 }
bsw/jbe@1309 3885 };
bsw/jbe@1309 3886
bsw/jbe@1309 3887 var refreshSelection;
bsw/jbe@1309 3888
bsw/jbe@1309 3889 if (useDocumentSelection) {
bsw/jbe@1309 3890 refreshSelection = function(sel) {
bsw/jbe@1309 3891 var range;
bsw/jbe@1309 3892 if (api.isSelectionValid(sel.win)) {
bsw/jbe@1309 3893 range = sel.docSelection.createRange();
bsw/jbe@1309 3894 } else {
bsw/jbe@1309 3895 range = getBody(sel.win.document).createTextRange();
bsw/jbe@1309 3896 range.collapse(true);
bsw/jbe@1309 3897 }
bsw/jbe@1309 3898
bsw/jbe@1309 3899 if (sel.docSelection.type == CONTROL) {
bsw/jbe@1309 3900 updateControlSelection(sel);
bsw/jbe@1309 3901 } else if (isTextRange(range)) {
bsw/jbe@1309 3902 updateFromTextRange(sel, range);
bsw/jbe@1309 3903 } else {
bsw/jbe@1309 3904 updateEmptySelection(sel);
bsw/jbe@1309 3905 }
bsw/jbe@1309 3906 };
bsw/jbe@1309 3907 } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
bsw/jbe@1309 3908 refreshSelection = function(sel) {
bsw/jbe@1309 3909 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
bsw/jbe@1309 3910 updateControlSelection(sel);
bsw/jbe@1309 3911 } else {
bsw/jbe@1309 3912 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
bsw/jbe@1309 3913 if (sel.rangeCount) {
bsw/jbe@1309 3914 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
bsw/jbe@1309 3915 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
bsw/jbe@1309 3916 }
bsw/jbe@1309 3917 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
bsw/jbe@1309 3918 sel.isCollapsed = selectionIsCollapsed(sel);
bsw/jbe@1309 3919 } else {
bsw/jbe@1309 3920 updateEmptySelection(sel);
bsw/jbe@1309 3921 }
bsw/jbe@1309 3922 }
bsw/jbe@1309 3923 };
bsw/jbe@1309 3924 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
bsw/jbe@1309 3925 refreshSelection = function(sel) {
bsw/jbe@1309 3926 var range, nativeSel = sel.nativeSelection;
bsw/jbe@1309 3927 if (nativeSel.anchorNode) {
bsw/jbe@1309 3928 range = getSelectionRangeAt(nativeSel, 0);
bsw/jbe@1309 3929 sel._ranges = [range];
bsw/jbe@1309 3930 sel.rangeCount = 1;
bsw/jbe@1309 3931 updateAnchorAndFocusFromNativeSelection(sel);
bsw/jbe@1309 3932 sel.isCollapsed = selectionIsCollapsed(sel);
bsw/jbe@1309 3933 } else {
bsw/jbe@1309 3934 updateEmptySelection(sel);
bsw/jbe@1309 3935 }
bsw/jbe@1309 3936 };
bsw/jbe@1309 3937 } else {
bsw/jbe@1309 3938 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
bsw/jbe@1309 3939 return false;
bsw/jbe@1309 3940 }
bsw/jbe@1309 3941
bsw/jbe@1309 3942 selProto.refresh = function(checkForChanges) {
bsw/jbe@1309 3943 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
bsw/jbe@1309 3944 var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
bsw/jbe@1309 3945
bsw/jbe@1309 3946 refreshSelection(this);
bsw/jbe@1309 3947 if (checkForChanges) {
bsw/jbe@1309 3948 // Check the range count first
bsw/jbe@1309 3949 var i = oldRanges.length;
bsw/jbe@1309 3950 if (i != this._ranges.length) {
bsw/jbe@1309 3951 return true;
bsw/jbe@1309 3952 }
bsw/jbe@1309 3953
bsw/jbe@1309 3954 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
bsw/jbe@1309 3955 // ranges after this
bsw/jbe@1309 3956 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
bsw/jbe@1309 3957 return true;
bsw/jbe@1309 3958 }
bsw/jbe@1309 3959
bsw/jbe@1309 3960 // Finally, compare each range in turn
bsw/jbe@1309 3961 while (i--) {
bsw/jbe@1309 3962 if (!rangesEqual(oldRanges[i], this._ranges[i])) {
bsw/jbe@1309 3963 return true;
bsw/jbe@1309 3964 }
bsw/jbe@1309 3965 }
bsw/jbe@1309 3966 return false;
bsw/jbe@1309 3967 }
bsw/jbe@1309 3968 };
bsw/jbe@1309 3969
bsw/jbe@1309 3970 // Removal of a single range
bsw/jbe@1309 3971 var removeRangeManually = function(sel, range) {
bsw/jbe@1309 3972 var ranges = sel.getAllRanges();
bsw/jbe@1309 3973 sel.removeAllRanges();
bsw/jbe@1309 3974 for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309 3975 if (!rangesEqual(range, ranges[i])) {
bsw/jbe@1309 3976 sel.addRange(ranges[i]);
bsw/jbe@1309 3977 }
bsw/jbe@1309 3978 }
bsw/jbe@1309 3979 if (!sel.rangeCount) {
bsw/jbe@1309 3980 updateEmptySelection(sel);
bsw/jbe@1309 3981 }
bsw/jbe@1309 3982 };
bsw/jbe@1309 3983
bsw/jbe@1309 3984 if (implementsControlRange && implementsDocSelection) {
bsw/jbe@1309 3985 selProto.removeRange = function(range) {
bsw/jbe@1309 3986 if (this.docSelection.type == CONTROL) {
bsw/jbe@1309 3987 var controlRange = this.docSelection.createRange();
bsw/jbe@1309 3988 var rangeElement = getSingleElementFromRange(range);
bsw/jbe@1309 3989
bsw/jbe@1309 3990 // Create a new ControlRange containing all the elements in the selected ControlRange minus the
bsw/jbe@1309 3991 // element contained by the supplied range
bsw/jbe@1309 3992 var doc = getDocument(controlRange.item(0));
bsw/jbe@1309 3993 var newControlRange = getBody(doc).createControlRange();
bsw/jbe@1309 3994 var el, removed = false;
bsw/jbe@1309 3995 for (var i = 0, len = controlRange.length; i < len; ++i) {
bsw/jbe@1309 3996 el = controlRange.item(i);
bsw/jbe@1309 3997 if (el !== rangeElement || removed) {
bsw/jbe@1309 3998 newControlRange.add(controlRange.item(i));
bsw/jbe@1309 3999 } else {
bsw/jbe@1309 4000 removed = true;
bsw/jbe@1309 4001 }
bsw/jbe@1309 4002 }
bsw/jbe@1309 4003 newControlRange.select();
bsw/jbe@1309 4004
bsw/jbe@1309 4005 // Update the wrapped selection based on what's now in the native selection
bsw/jbe@1309 4006 updateControlSelection(this);
bsw/jbe@1309 4007 } else {
bsw/jbe@1309 4008 removeRangeManually(this, range);
bsw/jbe@1309 4009 }
bsw/jbe@1309 4010 };
bsw/jbe@1309 4011 } else {
bsw/jbe@1309 4012 selProto.removeRange = function(range) {
bsw/jbe@1309 4013 removeRangeManually(this, range);
bsw/jbe@1309 4014 };
bsw/jbe@1309 4015 }
bsw/jbe@1309 4016
bsw/jbe@1309 4017 // Detecting if a selection is backward
bsw/jbe@1309 4018 var selectionIsBackward;
bsw/jbe@1309 4019 if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
bsw/jbe@1309 4020 selectionIsBackward = winSelectionIsBackward;
bsw/jbe@1309 4021
bsw/jbe@1309 4022 selProto.isBackward = function() {
bsw/jbe@1309 4023 return selectionIsBackward(this);
bsw/jbe@1309 4024 };
bsw/jbe@1309 4025 } else {
bsw/jbe@1309 4026 selectionIsBackward = selProto.isBackward = function() {
bsw/jbe@1309 4027 return false;
bsw/jbe@1309 4028 };
bsw/jbe@1309 4029 }
bsw/jbe@1309 4030
bsw/jbe@1309 4031 // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
bsw/jbe@1309 4032 selProto.isBackwards = selProto.isBackward;
bsw/jbe@1309 4033
bsw/jbe@1309 4034 // Selection stringifier
bsw/jbe@1309 4035 // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
bsw/jbe@1309 4036 // The current spec does not yet define this method.
bsw/jbe@1309 4037 selProto.toString = function() {
bsw/jbe@1309 4038 var rangeTexts = [];
bsw/jbe@1309 4039 for (var i = 0, len = this.rangeCount; i < len; ++i) {
bsw/jbe@1309 4040 rangeTexts[i] = "" + this._ranges[i];
bsw/jbe@1309 4041 }
bsw/jbe@1309 4042 return rangeTexts.join("");
bsw/jbe@1309 4043 };
bsw/jbe@1309 4044
bsw/jbe@1309 4045 function assertNodeInSameDocument(sel, node) {
bsw/jbe@1309 4046 if (sel.win.document != getDocument(node)) {
bsw/jbe@1309 4047 throw new DOMException("WRONG_DOCUMENT_ERR");
bsw/jbe@1309 4048 }
bsw/jbe@1309 4049 }
bsw/jbe@1309 4050
bsw/jbe@1309 4051 // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
bsw/jbe@1309 4052 selProto.collapse = function(node, offset) {
bsw/jbe@1309 4053 assertNodeInSameDocument(this, node);
bsw/jbe@1309 4054 var range = api.createRange(node);
bsw/jbe@1309 4055 range.collapseToPoint(node, offset);
bsw/jbe@1309 4056 this.setSingleRange(range);
bsw/jbe@1309 4057 this.isCollapsed = true;
bsw/jbe@1309 4058 };
bsw/jbe@1309 4059
bsw/jbe@1309 4060 selProto.collapseToStart = function() {
bsw/jbe@1309 4061 if (this.rangeCount) {
bsw/jbe@1309 4062 var range = this._ranges[0];
bsw/jbe@1309 4063 this.collapse(range.startContainer, range.startOffset);
bsw/jbe@1309 4064 } else {
bsw/jbe@1309 4065 throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309 4066 }
bsw/jbe@1309 4067 };
bsw/jbe@1309 4068
bsw/jbe@1309 4069 selProto.collapseToEnd = function() {
bsw/jbe@1309 4070 if (this.rangeCount) {
bsw/jbe@1309 4071 var range = this._ranges[this.rangeCount - 1];
bsw/jbe@1309 4072 this.collapse(range.endContainer, range.endOffset);
bsw/jbe@1309 4073 } else {
bsw/jbe@1309 4074 throw new DOMException("INVALID_STATE_ERR");
bsw/jbe@1309 4075 }
bsw/jbe@1309 4076 };
bsw/jbe@1309 4077
bsw/jbe@1309 4078 // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
bsw/jbe@1309 4079 // specified so the native implementation is never used by Rangy.
bsw/jbe@1309 4080 selProto.selectAllChildren = function(node) {
bsw/jbe@1309 4081 assertNodeInSameDocument(this, node);
bsw/jbe@1309 4082 var range = api.createRange(node);
bsw/jbe@1309 4083 range.selectNodeContents(node);
bsw/jbe@1309 4084 this.setSingleRange(range);
bsw/jbe@1309 4085 };
bsw/jbe@1309 4086
bsw/jbe@1309 4087 selProto.deleteFromDocument = function() {
bsw/jbe@1309 4088 // Sepcial behaviour required for IE's control selections
bsw/jbe@1309 4089 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
bsw/jbe@1309 4090 var controlRange = this.docSelection.createRange();
bsw/jbe@1309 4091 var element;
bsw/jbe@1309 4092 while (controlRange.length) {
bsw/jbe@1309 4093 element = controlRange.item(0);
bsw/jbe@1309 4094 controlRange.remove(element);
bsw/jbe@1309 4095 dom.removeNode(element);
bsw/jbe@1309 4096 }
bsw/jbe@1309 4097 this.refresh();
bsw/jbe@1309 4098 } else if (this.rangeCount) {
bsw/jbe@1309 4099 var ranges = this.getAllRanges();
bsw/jbe@1309 4100 if (ranges.length) {
bsw/jbe@1309 4101 this.removeAllRanges();
bsw/jbe@1309 4102 for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309 4103 ranges[i].deleteContents();
bsw/jbe@1309 4104 }
bsw/jbe@1309 4105 // The spec says nothing about what the selection should contain after calling deleteContents on each
bsw/jbe@1309 4106 // range. Firefox moves the selection to where the final selected range was, so we emulate that
bsw/jbe@1309 4107 this.addRange(ranges[len - 1]);
bsw/jbe@1309 4108 }
bsw/jbe@1309 4109 }
bsw/jbe@1309 4110 };
bsw/jbe@1309 4111
bsw/jbe@1309 4112 // The following are non-standard extensions
bsw/jbe@1309 4113 selProto.eachRange = function(func, returnValue) {
bsw/jbe@1309 4114 for (var i = 0, len = this._ranges.length; i < len; ++i) {
bsw/jbe@1309 4115 if ( func( this.getRangeAt(i) ) ) {
bsw/jbe@1309 4116 return returnValue;
bsw/jbe@1309 4117 }
bsw/jbe@1309 4118 }
bsw/jbe@1309 4119 };
bsw/jbe@1309 4120
bsw/jbe@1309 4121 selProto.getAllRanges = function() {
bsw/jbe@1309 4122 var ranges = [];
bsw/jbe@1309 4123 this.eachRange(function(range) {
bsw/jbe@1309 4124 ranges.push(range);
bsw/jbe@1309 4125 });
bsw/jbe@1309 4126 return ranges;
bsw/jbe@1309 4127 };
bsw/jbe@1309 4128
bsw/jbe@1309 4129 selProto.setSingleRange = function(range, direction) {
bsw/jbe@1309 4130 this.removeAllRanges();
bsw/jbe@1309 4131 this.addRange(range, direction);
bsw/jbe@1309 4132 };
bsw/jbe@1309 4133
bsw/jbe@1309 4134 selProto.callMethodOnEachRange = function(methodName, params) {
bsw/jbe@1309 4135 var results = [];
bsw/jbe@1309 4136 this.eachRange( function(range) {
bsw/jbe@1309 4137 results.push( range[methodName].apply(range, params || []) );
bsw/jbe@1309 4138 } );
bsw/jbe@1309 4139 return results;
bsw/jbe@1309 4140 };
bsw/jbe@1309 4141
bsw/jbe@1309 4142 function createStartOrEndSetter(isStart) {
bsw/jbe@1309 4143 return function(node, offset) {
bsw/jbe@1309 4144 var range;
bsw/jbe@1309 4145 if (this.rangeCount) {
bsw/jbe@1309 4146 range = this.getRangeAt(0);
bsw/jbe@1309 4147 range["set" + (isStart ? "Start" : "End")](node, offset);
bsw/jbe@1309 4148 } else {
bsw/jbe@1309 4149 range = api.createRange(this.win.document);
bsw/jbe@1309 4150 range.setStartAndEnd(node, offset);
bsw/jbe@1309 4151 }
bsw/jbe@1309 4152 this.setSingleRange(range, this.isBackward());
bsw/jbe@1309 4153 };
bsw/jbe@1309 4154 }
bsw/jbe@1309 4155
bsw/jbe@1309 4156 selProto.setStart = createStartOrEndSetter(true);
bsw/jbe@1309 4157 selProto.setEnd = createStartOrEndSetter(false);
bsw/jbe@1309 4158
bsw/jbe@1309 4159 // Add select() method to Range prototype. Any existing selection will be removed.
bsw/jbe@1309 4160 api.rangePrototype.select = function(direction) {
bsw/jbe@1309 4161 getSelection( this.getDocument() ).setSingleRange(this, direction);
bsw/jbe@1309 4162 };
bsw/jbe@1309 4163
bsw/jbe@1309 4164 selProto.changeEachRange = function(func) {
bsw/jbe@1309 4165 var ranges = [];
bsw/jbe@1309 4166 var backward = this.isBackward();
bsw/jbe@1309 4167
bsw/jbe@1309 4168 this.eachRange(function(range) {
bsw/jbe@1309 4169 func(range);
bsw/jbe@1309 4170 ranges.push(range);
bsw/jbe@1309 4171 });
bsw/jbe@1309 4172
bsw/jbe@1309 4173 this.removeAllRanges();
bsw/jbe@1309 4174 if (backward && ranges.length == 1) {
bsw/jbe@1309 4175 this.addRange(ranges[0], "backward");
bsw/jbe@1309 4176 } else {
bsw/jbe@1309 4177 this.setRanges(ranges);
bsw/jbe@1309 4178 }
bsw/jbe@1309 4179 };
bsw/jbe@1309 4180
bsw/jbe@1309 4181 selProto.containsNode = function(node, allowPartial) {
bsw/jbe@1309 4182 return this.eachRange( function(range) {
bsw/jbe@1309 4183 return range.containsNode(node, allowPartial);
bsw/jbe@1309 4184 }, true ) || false;
bsw/jbe@1309 4185 };
bsw/jbe@1309 4186
bsw/jbe@1309 4187 selProto.getBookmark = function(containerNode) {
bsw/jbe@1309 4188 return {
bsw/jbe@1309 4189 backward: this.isBackward(),
bsw/jbe@1309 4190 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
bsw/jbe@1309 4191 };
bsw/jbe@1309 4192 };
bsw/jbe@1309 4193
bsw/jbe@1309 4194 selProto.moveToBookmark = function(bookmark) {
bsw/jbe@1309 4195 var selRanges = [];
bsw/jbe@1309 4196 for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
bsw/jbe@1309 4197 range = api.createRange(this.win);
bsw/jbe@1309 4198 range.moveToBookmark(rangeBookmark);
bsw/jbe@1309 4199 selRanges.push(range);
bsw/jbe@1309 4200 }
bsw/jbe@1309 4201 if (bookmark.backward) {
bsw/jbe@1309 4202 this.setSingleRange(selRanges[0], "backward");
bsw/jbe@1309 4203 } else {
bsw/jbe@1309 4204 this.setRanges(selRanges);
bsw/jbe@1309 4205 }
bsw/jbe@1309 4206 };
bsw/jbe@1309 4207
bsw/jbe@1309 4208 selProto.saveRanges = function() {
bsw/jbe@1309 4209 return {
bsw/jbe@1309 4210 backward: this.isBackward(),
bsw/jbe@1309 4211 ranges: this.callMethodOnEachRange("cloneRange")
bsw/jbe@1309 4212 };
bsw/jbe@1309 4213 };
bsw/jbe@1309 4214
bsw/jbe@1309 4215 selProto.restoreRanges = function(selRanges) {
bsw/jbe@1309 4216 this.removeAllRanges();
bsw/jbe@1309 4217 for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
bsw/jbe@1309 4218 this.addRange(range, (selRanges.backward && i == 0));
bsw/jbe@1309 4219 }
bsw/jbe@1309 4220 };
bsw/jbe@1309 4221
bsw/jbe@1309 4222 selProto.toHtml = function() {
bsw/jbe@1309 4223 var rangeHtmls = [];
bsw/jbe@1309 4224 this.eachRange(function(range) {
bsw/jbe@1309 4225 rangeHtmls.push( DomRange.toHtml(range) );
bsw/jbe@1309 4226 });
bsw/jbe@1309 4227 return rangeHtmls.join("");
bsw/jbe@1309 4228 };
bsw/jbe@1309 4229
bsw/jbe@1309 4230 if (features.implementsTextRange) {
bsw/jbe@1309 4231 selProto.getNativeTextRange = function() {
bsw/jbe@1309 4232 var sel, textRange;
bsw/jbe@1309 4233 if ( (sel = this.docSelection) ) {
bsw/jbe@1309 4234 var range = sel.createRange();
bsw/jbe@1309 4235 if (isTextRange(range)) {
bsw/jbe@1309 4236 return range;
bsw/jbe@1309 4237 } else {
bsw/jbe@1309 4238 throw module.createError("getNativeTextRange: selection is a control selection");
bsw/jbe@1309 4239 }
bsw/jbe@1309 4240 } else if (this.rangeCount > 0) {
bsw/jbe@1309 4241 return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
bsw/jbe@1309 4242 } else {
bsw/jbe@1309 4243 throw module.createError("getNativeTextRange: selection contains no range");
bsw/jbe@1309 4244 }
bsw/jbe@1309 4245 };
bsw/jbe@1309 4246 }
bsw/jbe@1309 4247
bsw/jbe@1309 4248 function inspect(sel) {
bsw/jbe@1309 4249 var rangeInspects = [];
bsw/jbe@1309 4250 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
bsw/jbe@1309 4251 var focus = new DomPosition(sel.focusNode, sel.focusOffset);
bsw/jbe@1309 4252 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
bsw/jbe@1309 4253
bsw/jbe@1309 4254 if (typeof sel.rangeCount != "undefined") {
bsw/jbe@1309 4255 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
bsw/jbe@1309 4256 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
bsw/jbe@1309 4257 }
bsw/jbe@1309 4258 }
bsw/jbe@1309 4259 return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
bsw/jbe@1309 4260 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
bsw/jbe@1309 4261 }
bsw/jbe@1309 4262
bsw/jbe@1309 4263 selProto.getName = function() {
bsw/jbe@1309 4264 return "WrappedSelection";
bsw/jbe@1309 4265 };
bsw/jbe@1309 4266
bsw/jbe@1309 4267 selProto.inspect = function() {
bsw/jbe@1309 4268 return inspect(this);
bsw/jbe@1309 4269 };
bsw/jbe@1309 4270
bsw/jbe@1309 4271 selProto.detach = function() {
bsw/jbe@1309 4272 actOnCachedSelection(this.win, "delete");
bsw/jbe@1309 4273 deleteProperties(this);
bsw/jbe@1309 4274 };
bsw/jbe@1309 4275
bsw/jbe@1309 4276 WrappedSelection.detachAll = function() {
bsw/jbe@1309 4277 actOnCachedSelection(null, "deleteAll");
bsw/jbe@1309 4278 };
bsw/jbe@1309 4279
bsw/jbe@1309 4280 WrappedSelection.inspect = inspect;
bsw/jbe@1309 4281 WrappedSelection.isDirectionBackward = isDirectionBackward;
bsw/jbe@1309 4282
bsw/jbe@1309 4283 api.Selection = WrappedSelection;
bsw/jbe@1309 4284
bsw/jbe@1309 4285 api.selectionPrototype = selProto;
bsw/jbe@1309 4286
bsw/jbe@1309 4287 api.addShimListener(function(win) {
bsw/jbe@1309 4288 if (typeof win.getSelection == "undefined") {
bsw/jbe@1309 4289 win.getSelection = function() {
bsw/jbe@1309 4290 return getSelection(win);
bsw/jbe@1309 4291 };
bsw/jbe@1309 4292 }
bsw/jbe@1309 4293 win = null;
bsw/jbe@1309 4294 });
bsw/jbe@1309 4295 });
bsw/jbe@1309 4296
bsw/jbe@1309 4297
bsw/jbe@1309 4298 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 4299
bsw/jbe@1309 4300 // Wait for document to load before initializing
bsw/jbe@1309 4301 var docReady = false;
bsw/jbe@1309 4302
bsw/jbe@1309 4303 var loadHandler = function(e) {
bsw/jbe@1309 4304 if (!docReady) {
bsw/jbe@1309 4305 docReady = true;
bsw/jbe@1309 4306 if (!api.initialized && api.config.autoInitialize) {
bsw/jbe@1309 4307 init();
bsw/jbe@1309 4308 }
bsw/jbe@1309 4309 }
bsw/jbe@1309 4310 };
bsw/jbe@1309 4311
bsw/jbe@1309 4312 if (isBrowser) {
bsw/jbe@1309 4313 // Test whether the document has already been loaded and initialize immediately if so
bsw/jbe@1309 4314 if (document.readyState == "complete") {
bsw/jbe@1309 4315 loadHandler();
bsw/jbe@1309 4316 } else {
bsw/jbe@1309 4317 if (isHostMethod(document, "addEventListener")) {
bsw/jbe@1309 4318 document.addEventListener("DOMContentLoaded", loadHandler, false);
bsw/jbe@1309 4319 }
bsw/jbe@1309 4320
bsw/jbe@1309 4321 // Add a fallback in case the DOMContentLoaded event isn't supported
bsw/jbe@1309 4322 addListener(window, "load", loadHandler);
bsw/jbe@1309 4323 }
bsw/jbe@1309 4324 }
bsw/jbe@1309 4325
bsw/jbe@1309 4326 rangy = api;
bsw/jbe@1309 4327 })();
bsw/jbe@1309 4328
bsw/jbe@1309 4329 /**
bsw/jbe@1309 4330 * Selection save and restore module for Rangy.
bsw/jbe@1309 4331 * Saves and restores user selections using marker invisible elements in the DOM.
bsw/jbe@1309 4332 *
bsw/jbe@1309 4333 * Part of Rangy, a cross-browser JavaScript range and selection library
bsw/jbe@1309 4334 * https://github.com/timdown/rangy
bsw/jbe@1309 4335 *
bsw/jbe@1309 4336 * Depends on Rangy core.
bsw/jbe@1309 4337 *
bsw/jbe@1309 4338 * Copyright 2015, Tim Down
bsw/jbe@1309 4339 * Licensed under the MIT license.
bsw/jbe@1309 4340 * Version: 1.3.1-dev
bsw/jbe@1309 4341 * Build date: 20 May 2015
bsw/jbe@1309 4342 *
bsw/jbe@1309 4343 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
bsw/jbe@1309 4344 */
bsw/jbe@1309 4345 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
bsw/jbe@1309 4346 var dom = api.dom;
bsw/jbe@1309 4347 var removeNode = dom.removeNode;
bsw/jbe@1309 4348 var isDirectionBackward = api.Selection.isDirectionBackward;
bsw/jbe@1309 4349 var markerTextChar = "\ufeff";
bsw/jbe@1309 4350
bsw/jbe@1309 4351 function gEBI(id, doc) {
bsw/jbe@1309 4352 return (doc || document).getElementById(id);
bsw/jbe@1309 4353 }
bsw/jbe@1309 4354
bsw/jbe@1309 4355 function insertRangeBoundaryMarker(range, atStart) {
bsw/jbe@1309 4356 var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
bsw/jbe@1309 4357 var markerEl;
bsw/jbe@1309 4358 var doc = dom.getDocument(range.startContainer);
bsw/jbe@1309 4359
bsw/jbe@1309 4360 // Clone the Range and collapse to the appropriate boundary point
bsw/jbe@1309 4361 var boundaryRange = range.cloneRange();
bsw/jbe@1309 4362 boundaryRange.collapse(atStart);
bsw/jbe@1309 4363
bsw/jbe@1309 4364 // Create the marker element containing a single invisible character using DOM methods and insert it
bsw/jbe@1309 4365 markerEl = doc.createElement("span");
bsw/jbe@1309 4366 markerEl.id = markerId;
bsw/jbe@1309 4367 markerEl.style.lineHeight = "0";
bsw/jbe@1309 4368 markerEl.style.display = "none";
bsw/jbe@1309 4369 markerEl.className = "rangySelectionBoundary";
bsw/jbe@1309 4370 markerEl.appendChild(doc.createTextNode(markerTextChar));
bsw/jbe@1309 4371
bsw/jbe@1309 4372 boundaryRange.insertNode(markerEl);
bsw/jbe@1309 4373 return markerEl;
bsw/jbe@1309 4374 }
bsw/jbe@1309 4375
bsw/jbe@1309 4376 function setRangeBoundary(doc, range, markerId, atStart) {
bsw/jbe@1309 4377 var markerEl = gEBI(markerId, doc);
bsw/jbe@1309 4378 if (markerEl) {
bsw/jbe@1309 4379 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
bsw/jbe@1309 4380 removeNode(markerEl);
bsw/jbe@1309 4381 } else {
bsw/jbe@1309 4382 module.warn("Marker element has been removed. Cannot restore selection.");
bsw/jbe@1309 4383 }
bsw/jbe@1309 4384 }
bsw/jbe@1309 4385
bsw/jbe@1309 4386 function compareRanges(r1, r2) {
bsw/jbe@1309 4387 return r2.compareBoundaryPoints(r1.START_TO_START, r1);
bsw/jbe@1309 4388 }
bsw/jbe@1309 4389
bsw/jbe@1309 4390 function saveRange(range, direction) {
bsw/jbe@1309 4391 var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
bsw/jbe@1309 4392 var backward = isDirectionBackward(direction);
bsw/jbe@1309 4393
bsw/jbe@1309 4394 if (range.collapsed) {
bsw/jbe@1309 4395 endEl = insertRangeBoundaryMarker(range, false);
bsw/jbe@1309 4396 return {
bsw/jbe@1309 4397 document: doc,
bsw/jbe@1309 4398 markerId: endEl.id,
bsw/jbe@1309 4399 collapsed: true
bsw/jbe@1309 4400 };
bsw/jbe@1309 4401 } else {
bsw/jbe@1309 4402 endEl = insertRangeBoundaryMarker(range, false);
bsw/jbe@1309 4403 startEl = insertRangeBoundaryMarker(range, true);
bsw/jbe@1309 4404
bsw/jbe@1309 4405 return {
bsw/jbe@1309 4406 document: doc,
bsw/jbe@1309 4407 startMarkerId: startEl.id,
bsw/jbe@1309 4408 endMarkerId: endEl.id,
bsw/jbe@1309 4409 collapsed: false,
bsw/jbe@1309 4410 backward: backward,
bsw/jbe@1309 4411 toString: function() {
bsw/jbe@1309 4412 return "original text: '" + text + "', new text: '" + range.toString() + "'";
bsw/jbe@1309 4413 }
bsw/jbe@1309 4414 };
bsw/jbe@1309 4415 }
bsw/jbe@1309 4416 }
bsw/jbe@1309 4417
bsw/jbe@1309 4418 function restoreRange(rangeInfo, normalize) {
bsw/jbe@1309 4419 var doc = rangeInfo.document;
bsw/jbe@1309 4420 if (typeof normalize == "undefined") {
bsw/jbe@1309 4421 normalize = true;
bsw/jbe@1309 4422 }
bsw/jbe@1309 4423 var range = api.createRange(doc);
bsw/jbe@1309 4424 if (rangeInfo.collapsed) {
bsw/jbe@1309 4425 var markerEl = gEBI(rangeInfo.markerId, doc);
bsw/jbe@1309 4426 if (markerEl) {
bsw/jbe@1309 4427 markerEl.style.display = "inline";
bsw/jbe@1309 4428 var previousNode = markerEl.previousSibling;
bsw/jbe@1309 4429
bsw/jbe@1309 4430 // Workaround for issue 17
bsw/jbe@1309 4431 if (previousNode && previousNode.nodeType == 3) {
bsw/jbe@1309 4432 removeNode(markerEl);
bsw/jbe@1309 4433 range.collapseToPoint(previousNode, previousNode.length);
bsw/jbe@1309 4434 } else {
bsw/jbe@1309 4435 range.collapseBefore(markerEl);
bsw/jbe@1309 4436 removeNode(markerEl);
bsw/jbe@1309 4437 }
bsw/jbe@1309 4438 } else {
bsw/jbe@1309 4439 module.warn("Marker element has been removed. Cannot restore selection.");
bsw/jbe@1309 4440 }
bsw/jbe@1309 4441 } else {
bsw/jbe@1309 4442 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
bsw/jbe@1309 4443 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
bsw/jbe@1309 4444 }
bsw/jbe@1309 4445
bsw/jbe@1309 4446 if (normalize) {
bsw/jbe@1309 4447 range.normalizeBoundaries();
bsw/jbe@1309 4448 }
bsw/jbe@1309 4449
bsw/jbe@1309 4450 return range;
bsw/jbe@1309 4451 }
bsw/jbe@1309 4452
bsw/jbe@1309 4453 function saveRanges(ranges, direction) {
bsw/jbe@1309 4454 var rangeInfos = [], range, doc;
bsw/jbe@1309 4455 var backward = isDirectionBackward(direction);
bsw/jbe@1309 4456
bsw/jbe@1309 4457 // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
bsw/jbe@1309 4458 ranges = ranges.slice(0);
bsw/jbe@1309 4459 ranges.sort(compareRanges);
bsw/jbe@1309 4460
bsw/jbe@1309 4461 for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309 4462 rangeInfos[i] = saveRange(ranges[i], backward);
bsw/jbe@1309 4463 }
bsw/jbe@1309 4464
bsw/jbe@1309 4465 // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
bsw/jbe@1309 4466 // between its markers
bsw/jbe@1309 4467 for (i = len - 1; i >= 0; --i) {
bsw/jbe@1309 4468 range = ranges[i];
bsw/jbe@1309 4469 doc = api.DomRange.getRangeDocument(range);
bsw/jbe@1309 4470 if (range.collapsed) {
bsw/jbe@1309 4471 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
bsw/jbe@1309 4472 } else {
bsw/jbe@1309 4473 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
bsw/jbe@1309 4474 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
bsw/jbe@1309 4475 }
bsw/jbe@1309 4476 }
bsw/jbe@1309 4477
bsw/jbe@1309 4478 return rangeInfos;
bsw/jbe@1309 4479 }
bsw/jbe@1309 4480
bsw/jbe@1309 4481 function saveSelection(win) {
bsw/jbe@1309 4482 if (!api.isSelectionValid(win)) {
bsw/jbe@1309 4483 module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
bsw/jbe@1309 4484 return null;
bsw/jbe@1309 4485 }
bsw/jbe@1309 4486 var sel = api.getSelection(win);
bsw/jbe@1309 4487 var ranges = sel.getAllRanges();
bsw/jbe@1309 4488 var backward = (ranges.length == 1 && sel.isBackward());
bsw/jbe@1309 4489
bsw/jbe@1309 4490 var rangeInfos = saveRanges(ranges, backward);
bsw/jbe@1309 4491
bsw/jbe@1309 4492 // Ensure current selection is unaffected
bsw/jbe@1309 4493 if (backward) {
bsw/jbe@1309 4494 sel.setSingleRange(ranges[0], backward);
bsw/jbe@1309 4495 } else {
bsw/jbe@1309 4496 sel.setRanges(ranges);
bsw/jbe@1309 4497 }
bsw/jbe@1309 4498
bsw/jbe@1309 4499 return {
bsw/jbe@1309 4500 win: win,
bsw/jbe@1309 4501 rangeInfos: rangeInfos,
bsw/jbe@1309 4502 restored: false
bsw/jbe@1309 4503 };
bsw/jbe@1309 4504 }
bsw/jbe@1309 4505
bsw/jbe@1309 4506 function restoreRanges(rangeInfos) {
bsw/jbe@1309 4507 var ranges = [];
bsw/jbe@1309 4508
bsw/jbe@1309 4509 // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
bsw/jbe@1309 4510 // normalization affecting previously restored ranges.
bsw/jbe@1309 4511 var rangeCount = rangeInfos.length;
bsw/jbe@1309 4512
bsw/jbe@1309 4513 for (var i = rangeCount - 1; i >= 0; i--) {
bsw/jbe@1309 4514 ranges[i] = restoreRange(rangeInfos[i], true);
bsw/jbe@1309 4515 }
bsw/jbe@1309 4516
bsw/jbe@1309 4517 return ranges;
bsw/jbe@1309 4518 }
bsw/jbe@1309 4519
bsw/jbe@1309 4520 function restoreSelection(savedSelection, preserveDirection) {
bsw/jbe@1309 4521 if (!savedSelection.restored) {
bsw/jbe@1309 4522 var rangeInfos = savedSelection.rangeInfos;
bsw/jbe@1309 4523 var sel = api.getSelection(savedSelection.win);
bsw/jbe@1309 4524 var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
bsw/jbe@1309 4525
bsw/jbe@1309 4526 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
bsw/jbe@1309 4527 sel.removeAllRanges();
bsw/jbe@1309 4528 sel.addRange(ranges[0], true);
bsw/jbe@1309 4529 } else {
bsw/jbe@1309 4530 sel.setRanges(ranges);
bsw/jbe@1309 4531 }
bsw/jbe@1309 4532
bsw/jbe@1309 4533 savedSelection.restored = true;
bsw/jbe@1309 4534 }
bsw/jbe@1309 4535 }
bsw/jbe@1309 4536
bsw/jbe@1309 4537 function removeMarkerElement(doc, markerId) {
bsw/jbe@1309 4538 var markerEl = gEBI(markerId, doc);
bsw/jbe@1309 4539 if (markerEl) {
bsw/jbe@1309 4540 removeNode(markerEl);
bsw/jbe@1309 4541 }
bsw/jbe@1309 4542 }
bsw/jbe@1309 4543
bsw/jbe@1309 4544 function removeMarkers(savedSelection) {
bsw/jbe@1309 4545 var rangeInfos = savedSelection.rangeInfos;
bsw/jbe@1309 4546 for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
bsw/jbe@1309 4547 rangeInfo = rangeInfos[i];
bsw/jbe@1309 4548 if (rangeInfo.collapsed) {
bsw/jbe@1309 4549 removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
bsw/jbe@1309 4550 } else {
bsw/jbe@1309 4551 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
bsw/jbe@1309 4552 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
bsw/jbe@1309 4553 }
bsw/jbe@1309 4554 }
bsw/jbe@1309 4555 }
bsw/jbe@1309 4556
bsw/jbe@1309 4557 api.util.extend(api, {
bsw/jbe@1309 4558 saveRange: saveRange,
bsw/jbe@1309 4559 restoreRange: restoreRange,
bsw/jbe@1309 4560 saveRanges: saveRanges,
bsw/jbe@1309 4561 restoreRanges: restoreRanges,
bsw/jbe@1309 4562 saveSelection: saveSelection,
bsw/jbe@1309 4563 restoreSelection: restoreSelection,
bsw/jbe@1309 4564 removeMarkerElement: removeMarkerElement,
bsw/jbe@1309 4565 removeMarkers: removeMarkers
bsw/jbe@1309 4566 });
bsw/jbe@1309 4567 });
bsw/jbe@1309 4568
bsw/jbe@1309 4569 /**
bsw/jbe@1309 4570 * Text range module for Rangy.
bsw/jbe@1309 4571 * Text-based manipulation and searching of ranges and selections.
bsw/jbe@1309 4572 *
bsw/jbe@1309 4573 * Features
bsw/jbe@1309 4574 *
bsw/jbe@1309 4575 * - Ability to move range boundaries by character or word offsets
bsw/jbe@1309 4576 * - Customizable word tokenizer
bsw/jbe@1309 4577 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
bsw/jbe@1309 4578 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
bsw/jbe@1309 4579 * sensitivity
bsw/jbe@1309 4580 * - Selection and range save/restore as text offsets within a node
bsw/jbe@1309 4581 * - Methods to return visible text within a range or selection
bsw/jbe@1309 4582 * - innerText method for elements
bsw/jbe@1309 4583 *
bsw/jbe@1309 4584 * References
bsw/jbe@1309 4585 *
bsw/jbe@1309 4586 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
bsw/jbe@1309 4587 * http://aryeh.name/spec/innertext/innertext.html
bsw/jbe@1309 4588 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
bsw/jbe@1309 4589 *
bsw/jbe@1309 4590 * Part of Rangy, a cross-browser JavaScript range and selection library
bsw/jbe@1309 4591 * https://github.com/timdown/rangy
bsw/jbe@1309 4592 *
bsw/jbe@1309 4593 * Depends on Rangy core.
bsw/jbe@1309 4594 *
bsw/jbe@1309 4595 * Copyright 2015, Tim Down
bsw/jbe@1309 4596 * Licensed under the MIT license.
bsw/jbe@1309 4597 * Version: 1.3.1-dev
bsw/jbe@1309 4598 * Build date: 20 May 2015
bsw/jbe@1309 4599 */
bsw/jbe@1309 4600
bsw/jbe@1309 4601 /**
bsw/jbe@1309 4602 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
bsw/jbe@1309 4603 *
bsw/jbe@1309 4604 * First, a <br>: this is relatively simple. For the following HTML:
bsw/jbe@1309 4605 *
bsw/jbe@1309 4606 * 1 <br>2
bsw/jbe@1309 4607 *
bsw/jbe@1309 4608 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
bsw/jbe@1309 4609 * textarea, the space is present) and allow the caret to be placed after it.
bsw/jbe@1309 4610 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
bsw/jbe@1309 4611 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right
bsw/jbe@1309 4612 * arrow keys show this) and includes the space in the selection.
bsw/jbe@1309 4613 *
bsw/jbe@1309 4614 * The other case is the line break or breaks implied by block elements. For the following HTML:
bsw/jbe@1309 4615 *
bsw/jbe@1309 4616 * <p>1 </p><p>2<p>
bsw/jbe@1309 4617 *
bsw/jbe@1309 4618 * - WebKit does not acknowledge the space in any way
bsw/jbe@1309 4619 * - Firefox, IE and Opera as per <br>
bsw/jbe@1309 4620 *
bsw/jbe@1309 4621 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
bsw/jbe@1309 4622 *
bsw/jbe@1309 4623 * <p style="white-space: pre-line">1
bsw/jbe@1309 4624 * 2</p>
bsw/jbe@1309 4625 *
bsw/jbe@1309 4626 * - Firefox and WebKit include the space in caret positions
bsw/jbe@1309 4627 * - IE does not support pre-line up to and including version 9
bsw/jbe@1309 4628 * - Opera ignores the space
bsw/jbe@1309 4629 * - Trailing space only renders if there is a non-collapsed character in the line
bsw/jbe@1309 4630 *
bsw/jbe@1309 4631 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
bsw/jbe@1309 4632 * feature-tested
bsw/jbe@1309 4633 *
bsw/jbe@1309 4634 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
bsw/jbe@1309 4635 */
bsw/jbe@1309 4636 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
bsw/jbe@1309 4637 var UNDEF = "undefined";
bsw/jbe@1309 4638 var CHARACTER = "character", WORD = "word";
bsw/jbe@1309 4639 var dom = api.dom, util = api.util;
bsw/jbe@1309 4640 var extend = util.extend;
bsw/jbe@1309 4641 var createOptions = util.createOptions;
bsw/jbe@1309 4642 var getBody = dom.getBody;
bsw/jbe@1309 4643
bsw/jbe@1309 4644
bsw/jbe@1309 4645 var spacesRegex = /^[ \t\f\r\n]+$/;
bsw/jbe@1309 4646 var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
bsw/jbe@1309 4647 var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
bsw/jbe@1309 4648 var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
bsw/jbe@1309 4649 var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
bsw/jbe@1309 4650
bsw/jbe@1309 4651 var defaultLanguage = "en";
bsw/jbe@1309 4652
bsw/jbe@1309 4653 var isDirectionBackward = api.Selection.isDirectionBackward;
bsw/jbe@1309 4654
bsw/jbe@1309 4655 // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
bsw/jbe@1309 4656 // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
bsw/jbe@1309 4657 var trailingSpaceInBlockCollapses = false;
bsw/jbe@1309 4658 var trailingSpaceBeforeBrCollapses = false;
bsw/jbe@1309 4659 var trailingSpaceBeforeBlockCollapses = false;
bsw/jbe@1309 4660 var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
bsw/jbe@1309 4661
bsw/jbe@1309 4662 (function() {
bsw/jbe@1309 4663 var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
bsw/jbe@1309 4664 var p = el.firstChild;
bsw/jbe@1309 4665 var sel = api.getSelection();
bsw/jbe@1309 4666 sel.collapse(p.lastChild, 2);
bsw/jbe@1309 4667 sel.setStart(p.firstChild, 0);
bsw/jbe@1309 4668 trailingSpaceInBlockCollapses = ("" + sel).length == 1;
bsw/jbe@1309 4669
bsw/jbe@1309 4670 el.innerHTML = "1 <br />";
bsw/jbe@1309 4671 sel.collapse(el, 2);
bsw/jbe@1309 4672 sel.setStart(el.firstChild, 0);
bsw/jbe@1309 4673 trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
bsw/jbe@1309 4674
bsw/jbe@1309 4675 el.innerHTML = "1 <p>1</p>";
bsw/jbe@1309 4676 sel.collapse(el, 2);
bsw/jbe@1309 4677 sel.setStart(el.firstChild, 0);
bsw/jbe@1309 4678 trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
bsw/jbe@1309 4679
bsw/jbe@1309 4680 dom.removeNode(el);
bsw/jbe@1309 4681 sel.removeAllRanges();
bsw/jbe@1309 4682 })();
bsw/jbe@1309 4683
bsw/jbe@1309 4684 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 4685
bsw/jbe@1309 4686 // This function must create word and non-word tokens for the whole of the text supplied to it
bsw/jbe@1309 4687 function defaultTokenizer(chars, wordOptions) {
bsw/jbe@1309 4688 var word = chars.join(""), result, tokenRanges = [];
bsw/jbe@1309 4689
bsw/jbe@1309 4690 function createTokenRange(start, end, isWord) {
bsw/jbe@1309 4691 tokenRanges.push( { start: start, end: end, isWord: isWord } );
bsw/jbe@1309 4692 }
bsw/jbe@1309 4693
bsw/jbe@1309 4694 // Match words and mark characters
bsw/jbe@1309 4695 var lastWordEnd = 0, wordStart, wordEnd;
bsw/jbe@1309 4696 while ( (result = wordOptions.wordRegex.exec(word)) ) {
bsw/jbe@1309 4697 wordStart = result.index;
bsw/jbe@1309 4698 wordEnd = wordStart + result[0].length;
bsw/jbe@1309 4699
bsw/jbe@1309 4700 // Create token for non-word characters preceding this word
bsw/jbe@1309 4701 if (wordStart > lastWordEnd) {
bsw/jbe@1309 4702 createTokenRange(lastWordEnd, wordStart, false);
bsw/jbe@1309 4703 }
bsw/jbe@1309 4704
bsw/jbe@1309 4705 // Get trailing space characters for word
bsw/jbe@1309 4706 if (wordOptions.includeTrailingSpace) {
bsw/jbe@1309 4707 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
bsw/jbe@1309 4708 ++wordEnd;
bsw/jbe@1309 4709 }
bsw/jbe@1309 4710 }
bsw/jbe@1309 4711 createTokenRange(wordStart, wordEnd, true);
bsw/jbe@1309 4712 lastWordEnd = wordEnd;
bsw/jbe@1309 4713 }
bsw/jbe@1309 4714
bsw/jbe@1309 4715 // Create token for trailing non-word characters, if any exist
bsw/jbe@1309 4716 if (lastWordEnd < chars.length) {
bsw/jbe@1309 4717 createTokenRange(lastWordEnd, chars.length, false);
bsw/jbe@1309 4718 }
bsw/jbe@1309 4719
bsw/jbe@1309 4720 return tokenRanges;
bsw/jbe@1309 4721 }
bsw/jbe@1309 4722
bsw/jbe@1309 4723 function convertCharRangeToToken(chars, tokenRange) {
bsw/jbe@1309 4724 var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
bsw/jbe@1309 4725 var token = {
bsw/jbe@1309 4726 isWord: tokenRange.isWord,
bsw/jbe@1309 4727 chars: tokenChars,
bsw/jbe@1309 4728 toString: function() {
bsw/jbe@1309 4729 return tokenChars.join("");
bsw/jbe@1309 4730 }
bsw/jbe@1309 4731 };
bsw/jbe@1309 4732 for (var i = 0, len = tokenChars.length; i < len; ++i) {
bsw/jbe@1309 4733 tokenChars[i].token = token;
bsw/jbe@1309 4734 }
bsw/jbe@1309 4735 return token;
bsw/jbe@1309 4736 }
bsw/jbe@1309 4737
bsw/jbe@1309 4738 function tokenize(chars, wordOptions, tokenizer) {
bsw/jbe@1309 4739 var tokenRanges = tokenizer(chars, wordOptions);
bsw/jbe@1309 4740 var tokens = [];
bsw/jbe@1309 4741 for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
bsw/jbe@1309 4742 tokens.push( convertCharRangeToToken(chars, tokenRange) );
bsw/jbe@1309 4743 }
bsw/jbe@1309 4744 return tokens;
bsw/jbe@1309 4745 }
bsw/jbe@1309 4746
bsw/jbe@1309 4747 var defaultCharacterOptions = {
bsw/jbe@1309 4748 includeBlockContentTrailingSpace: true,
bsw/jbe@1309 4749 includeSpaceBeforeBr: true,
bsw/jbe@1309 4750 includeSpaceBeforeBlock: true,
bsw/jbe@1309 4751 includePreLineTrailingSpace: true,
bsw/jbe@1309 4752 ignoreCharacters: ""
bsw/jbe@1309 4753 };
bsw/jbe@1309 4754
bsw/jbe@1309 4755 function normalizeIgnoredCharacters(ignoredCharacters) {
bsw/jbe@1309 4756 // Check if character is ignored
bsw/jbe@1309 4757 var ignoredChars = ignoredCharacters || "";
bsw/jbe@1309 4758
bsw/jbe@1309 4759 // Normalize ignored characters into a string consisting of characters in ascending order of character code
bsw/jbe@1309 4760 var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
bsw/jbe@1309 4761 ignoredCharsArray.sort(function(char1, char2) {
bsw/jbe@1309 4762 return char1.charCodeAt(0) - char2.charCodeAt(0);
bsw/jbe@1309 4763 });
bsw/jbe@1309 4764
bsw/jbe@1309 4765 /// Convert back to a string and remove duplicates
bsw/jbe@1309 4766 return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
bsw/jbe@1309 4767 }
bsw/jbe@1309 4768
bsw/jbe@1309 4769 var defaultCaretCharacterOptions = {
bsw/jbe@1309 4770 includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
bsw/jbe@1309 4771 includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
bsw/jbe@1309 4772 includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
bsw/jbe@1309 4773 includePreLineTrailingSpace: true
bsw/jbe@1309 4774 };
bsw/jbe@1309 4775
bsw/jbe@1309 4776 var defaultWordOptions = {
bsw/jbe@1309 4777 "en": {
bsw/jbe@1309 4778 wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
bsw/jbe@1309 4779 includeTrailingSpace: false,
bsw/jbe@1309 4780 tokenizer: defaultTokenizer
bsw/jbe@1309 4781 }
bsw/jbe@1309 4782 };
bsw/jbe@1309 4783
bsw/jbe@1309 4784 var defaultFindOptions = {
bsw/jbe@1309 4785 caseSensitive: false,
bsw/jbe@1309 4786 withinRange: null,
bsw/jbe@1309 4787 wholeWordsOnly: false,
bsw/jbe@1309 4788 wrap: false,
bsw/jbe@1309 4789 direction: "forward",
bsw/jbe@1309 4790 wordOptions: null,
bsw/jbe@1309 4791 characterOptions: null
bsw/jbe@1309 4792 };
bsw/jbe@1309 4793
bsw/jbe@1309 4794 var defaultMoveOptions = {
bsw/jbe@1309 4795 wordOptions: null,
bsw/jbe@1309 4796 characterOptions: null
bsw/jbe@1309 4797 };
bsw/jbe@1309 4798
bsw/jbe@1309 4799 var defaultExpandOptions = {
bsw/jbe@1309 4800 wordOptions: null,
bsw/jbe@1309 4801 characterOptions: null,
bsw/jbe@1309 4802 trim: false,
bsw/jbe@1309 4803 trimStart: true,
bsw/jbe@1309 4804 trimEnd: true
bsw/jbe@1309 4805 };
bsw/jbe@1309 4806
bsw/jbe@1309 4807 var defaultWordIteratorOptions = {
bsw/jbe@1309 4808 wordOptions: null,
bsw/jbe@1309 4809 characterOptions: null,
bsw/jbe@1309 4810 direction: "forward"
bsw/jbe@1309 4811 };
bsw/jbe@1309 4812
bsw/jbe@1309 4813 function createWordOptions(options) {
bsw/jbe@1309 4814 var lang, defaults;
bsw/jbe@1309 4815 if (!options) {
bsw/jbe@1309 4816 return defaultWordOptions[defaultLanguage];
bsw/jbe@1309 4817 } else {
bsw/jbe@1309 4818 lang = options.language || defaultLanguage;
bsw/jbe@1309 4819 defaults = {};
bsw/jbe@1309 4820 extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
bsw/jbe@1309 4821 extend(defaults, options);
bsw/jbe@1309 4822 return defaults;
bsw/jbe@1309 4823 }
bsw/jbe@1309 4824 }
bsw/jbe@1309 4825
bsw/jbe@1309 4826 function createNestedOptions(optionsParam, defaults) {
bsw/jbe@1309 4827 var options = createOptions(optionsParam, defaults);
bsw/jbe@1309 4828 if (defaults.hasOwnProperty("wordOptions")) {
bsw/jbe@1309 4829 options.wordOptions = createWordOptions(options.wordOptions);
bsw/jbe@1309 4830 }
bsw/jbe@1309 4831 if (defaults.hasOwnProperty("characterOptions")) {
bsw/jbe@1309 4832 options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
bsw/jbe@1309 4833 }
bsw/jbe@1309 4834 return options;
bsw/jbe@1309 4835 }
bsw/jbe@1309 4836
bsw/jbe@1309 4837 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 4838
bsw/jbe@1309 4839 /* DOM utility functions */
bsw/jbe@1309 4840 var getComputedStyleProperty = dom.getComputedStyleProperty;
bsw/jbe@1309 4841
bsw/jbe@1309 4842 // Create cachable versions of DOM functions
bsw/jbe@1309 4843
bsw/jbe@1309 4844 // Test for old IE's incorrect display properties
bsw/jbe@1309 4845 var tableCssDisplayBlock;
bsw/jbe@1309 4846 (function() {
bsw/jbe@1309 4847 var table = document.createElement("table");
bsw/jbe@1309 4848 var body = getBody(document);
bsw/jbe@1309 4849 body.appendChild(table);
bsw/jbe@1309 4850 tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
bsw/jbe@1309 4851 body.removeChild(table);
bsw/jbe@1309 4852 })();
bsw/jbe@1309 4853
bsw/jbe@1309 4854 var defaultDisplayValueForTag = {
bsw/jbe@1309 4855 table: "table",
bsw/jbe@1309 4856 caption: "table-caption",
bsw/jbe@1309 4857 colgroup: "table-column-group",
bsw/jbe@1309 4858 col: "table-column",
bsw/jbe@1309 4859 thead: "table-header-group",
bsw/jbe@1309 4860 tbody: "table-row-group",
bsw/jbe@1309 4861 tfoot: "table-footer-group",
bsw/jbe@1309 4862 tr: "table-row",
bsw/jbe@1309 4863 td: "table-cell",
bsw/jbe@1309 4864 th: "table-cell"
bsw/jbe@1309 4865 };
bsw/jbe@1309 4866
bsw/jbe@1309 4867 // Corrects IE's "block" value for table-related elements
bsw/jbe@1309 4868 function getComputedDisplay(el, win) {
bsw/jbe@1309 4869 var display = getComputedStyleProperty(el, "display", win);
bsw/jbe@1309 4870 var tagName = el.tagName.toLowerCase();
bsw/jbe@1309 4871 return (display == "block" &&
bsw/jbe@1309 4872 tableCssDisplayBlock &&
bsw/jbe@1309 4873 defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
bsw/jbe@1309 4874 defaultDisplayValueForTag[tagName] : display;
bsw/jbe@1309 4875 }
bsw/jbe@1309 4876
bsw/jbe@1309 4877 function isHidden(node) {
bsw/jbe@1309 4878 var ancestors = getAncestorsAndSelf(node);
bsw/jbe@1309 4879 for (var i = 0, len = ancestors.length; i < len; ++i) {
bsw/jbe@1309 4880 if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
bsw/jbe@1309 4881 return true;
bsw/jbe@1309 4882 }
bsw/jbe@1309 4883 }
bsw/jbe@1309 4884
bsw/jbe@1309 4885 return false;
bsw/jbe@1309 4886 }
bsw/jbe@1309 4887
bsw/jbe@1309 4888 function isVisibilityHiddenTextNode(textNode) {
bsw/jbe@1309 4889 var el;
bsw/jbe@1309 4890 return textNode.nodeType == 3 &&
bsw/jbe@1309 4891 (el = textNode.parentNode) &&
bsw/jbe@1309 4892 getComputedStyleProperty(el, "visibility") == "hidden";
bsw/jbe@1309 4893 }
bsw/jbe@1309 4894
bsw/jbe@1309 4895 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 4896
bsw/jbe@1309 4897
bsw/jbe@1309 4898 // "A block node is either an Element whose "display" property does not have
bsw/jbe@1309 4899 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
bsw/jbe@1309 4900 // Document, or a DocumentFragment."
bsw/jbe@1309 4901 function isBlockNode(node) {
bsw/jbe@1309 4902 return node &&
bsw/jbe@1309 4903 ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
bsw/jbe@1309 4904 node.nodeType == 9 || node.nodeType == 11);
bsw/jbe@1309 4905 }
bsw/jbe@1309 4906
bsw/jbe@1309 4907 function getLastDescendantOrSelf(node) {
bsw/jbe@1309 4908 var lastChild = node.lastChild;
bsw/jbe@1309 4909 return lastChild ? getLastDescendantOrSelf(lastChild) : node;
bsw/jbe@1309 4910 }
bsw/jbe@1309 4911
bsw/jbe@1309 4912 function containsPositions(node) {
bsw/jbe@1309 4913 return dom.isCharacterDataNode(node) ||
bsw/jbe@1309 4914 !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
bsw/jbe@1309 4915 }
bsw/jbe@1309 4916
bsw/jbe@1309 4917 function getAncestors(node) {
bsw/jbe@1309 4918 var ancestors = [];
bsw/jbe@1309 4919 while (node.parentNode) {
bsw/jbe@1309 4920 ancestors.unshift(node.parentNode);
bsw/jbe@1309 4921 node = node.parentNode;
bsw/jbe@1309 4922 }
bsw/jbe@1309 4923 return ancestors;
bsw/jbe@1309 4924 }
bsw/jbe@1309 4925
bsw/jbe@1309 4926 function getAncestorsAndSelf(node) {
bsw/jbe@1309 4927 return getAncestors(node).concat([node]);
bsw/jbe@1309 4928 }
bsw/jbe@1309 4929
bsw/jbe@1309 4930 function nextNodeDescendants(node) {
bsw/jbe@1309 4931 while (node && !node.nextSibling) {
bsw/jbe@1309 4932 node = node.parentNode;
bsw/jbe@1309 4933 }
bsw/jbe@1309 4934 if (!node) {
bsw/jbe@1309 4935 return null;
bsw/jbe@1309 4936 }
bsw/jbe@1309 4937 return node.nextSibling;
bsw/jbe@1309 4938 }
bsw/jbe@1309 4939
bsw/jbe@1309 4940 function nextNode(node, excludeChildren) {
bsw/jbe@1309 4941 if (!excludeChildren && node.hasChildNodes()) {
bsw/jbe@1309 4942 return node.firstChild;
bsw/jbe@1309 4943 }
bsw/jbe@1309 4944 return nextNodeDescendants(node);
bsw/jbe@1309 4945 }
bsw/jbe@1309 4946
bsw/jbe@1309 4947 function previousNode(node) {
bsw/jbe@1309 4948 var previous = node.previousSibling;
bsw/jbe@1309 4949 if (previous) {
bsw/jbe@1309 4950 node = previous;
bsw/jbe@1309 4951 while (node.hasChildNodes()) {
bsw/jbe@1309 4952 node = node.lastChild;
bsw/jbe@1309 4953 }
bsw/jbe@1309 4954 return node;
bsw/jbe@1309 4955 }
bsw/jbe@1309 4956 var parent = node.parentNode;
bsw/jbe@1309 4957 if (parent && parent.nodeType == 1) {
bsw/jbe@1309 4958 return parent;
bsw/jbe@1309 4959 }
bsw/jbe@1309 4960 return null;
bsw/jbe@1309 4961 }
bsw/jbe@1309 4962
bsw/jbe@1309 4963 // Adpated from Aryeh's code.
bsw/jbe@1309 4964 // "A whitespace node is either a Text node whose data is the empty string; or
bsw/jbe@1309 4965 // a Text node whose data consists only of one or more tabs (0x0009), line
bsw/jbe@1309 4966 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
bsw/jbe@1309 4967 // parent is an Element whose resolved value for "white-space" is "normal" or
bsw/jbe@1309 4968 // "nowrap"; or a Text node whose data consists only of one or more tabs
bsw/jbe@1309 4969 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
bsw/jbe@1309 4970 // parent is an Element whose resolved value for "white-space" is "pre-line"."
bsw/jbe@1309 4971 function isWhitespaceNode(node) {
bsw/jbe@1309 4972 if (!node || node.nodeType != 3) {
bsw/jbe@1309 4973 return false;
bsw/jbe@1309 4974 }
bsw/jbe@1309 4975 var text = node.data;
bsw/jbe@1309 4976 if (text === "") {
bsw/jbe@1309 4977 return true;
bsw/jbe@1309 4978 }
bsw/jbe@1309 4979 var parent = node.parentNode;
bsw/jbe@1309 4980 if (!parent || parent.nodeType != 1) {
bsw/jbe@1309 4981 return false;
bsw/jbe@1309 4982 }
bsw/jbe@1309 4983 var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
bsw/jbe@1309 4984
bsw/jbe@1309 4985 return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
bsw/jbe@1309 4986 (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
bsw/jbe@1309 4987 }
bsw/jbe@1309 4988
bsw/jbe@1309 4989 // Adpated from Aryeh's code.
bsw/jbe@1309 4990 // "node is a collapsed whitespace node if the following algorithm returns
bsw/jbe@1309 4991 // true:"
bsw/jbe@1309 4992 function isCollapsedWhitespaceNode(node) {
bsw/jbe@1309 4993 // "If node's data is the empty string, return true."
bsw/jbe@1309 4994 if (node.data === "") {
bsw/jbe@1309 4995 return true;
bsw/jbe@1309 4996 }
bsw/jbe@1309 4997
bsw/jbe@1309 4998 // "If node is not a whitespace node, return false."
bsw/jbe@1309 4999 if (!isWhitespaceNode(node)) {
bsw/jbe@1309 5000 return false;
bsw/jbe@1309 5001 }
bsw/jbe@1309 5002
bsw/jbe@1309 5003 // "Let ancestor be node's parent."
bsw/jbe@1309 5004 var ancestor = node.parentNode;
bsw/jbe@1309 5005
bsw/jbe@1309 5006 // "If ancestor is null, return true."
bsw/jbe@1309 5007 if (!ancestor) {
bsw/jbe@1309 5008 return true;
bsw/jbe@1309 5009 }
bsw/jbe@1309 5010
bsw/jbe@1309 5011 // "If the "display" property of some ancestor of node has resolved value "none", return true."
bsw/jbe@1309 5012 if (isHidden(node)) {
bsw/jbe@1309 5013 return true;
bsw/jbe@1309 5014 }
bsw/jbe@1309 5015
bsw/jbe@1309 5016 return false;
bsw/jbe@1309 5017 }
bsw/jbe@1309 5018
bsw/jbe@1309 5019 function isCollapsedNode(node) {
bsw/jbe@1309 5020 var type = node.nodeType;
bsw/jbe@1309 5021 return type == 7 /* PROCESSING_INSTRUCTION */ ||
bsw/jbe@1309 5022 type == 8 /* COMMENT */ ||
bsw/jbe@1309 5023 isHidden(node) ||
bsw/jbe@1309 5024 /^(script|style)$/i.test(node.nodeName) ||
bsw/jbe@1309 5025 isVisibilityHiddenTextNode(node) ||
bsw/jbe@1309 5026 isCollapsedWhitespaceNode(node);
bsw/jbe@1309 5027 }
bsw/jbe@1309 5028
bsw/jbe@1309 5029 function isIgnoredNode(node, win) {
bsw/jbe@1309 5030 var type = node.nodeType;
bsw/jbe@1309 5031 return type == 7 /* PROCESSING_INSTRUCTION */ ||
bsw/jbe@1309 5032 type == 8 /* COMMENT */ ||
bsw/jbe@1309 5033 (type == 1 && getComputedDisplay(node, win) == "none");
bsw/jbe@1309 5034 }
bsw/jbe@1309 5035
bsw/jbe@1309 5036 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5037
bsw/jbe@1309 5038 // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
bsw/jbe@1309 5039
bsw/jbe@1309 5040 function Cache() {
bsw/jbe@1309 5041 this.store = {};
bsw/jbe@1309 5042 }
bsw/jbe@1309 5043
bsw/jbe@1309 5044 Cache.prototype = {
bsw/jbe@1309 5045 get: function(key) {
bsw/jbe@1309 5046 return this.store.hasOwnProperty(key) ? this.store[key] : null;
bsw/jbe@1309 5047 },
bsw/jbe@1309 5048
bsw/jbe@1309 5049 set: function(key, value) {
bsw/jbe@1309 5050 return this.store[key] = value;
bsw/jbe@1309 5051 }
bsw/jbe@1309 5052 };
bsw/jbe@1309 5053
bsw/jbe@1309 5054 var cachedCount = 0, uncachedCount = 0;
bsw/jbe@1309 5055
bsw/jbe@1309 5056 function createCachingGetter(methodName, func, objProperty) {
bsw/jbe@1309 5057 return function(args) {
bsw/jbe@1309 5058 var cache = this.cache;
bsw/jbe@1309 5059 if (cache.hasOwnProperty(methodName)) {
bsw/jbe@1309 5060 cachedCount++;
bsw/jbe@1309 5061 return cache[methodName];
bsw/jbe@1309 5062 } else {
bsw/jbe@1309 5063 uncachedCount++;
bsw/jbe@1309 5064 var value = func.call(this, objProperty ? this[objProperty] : this, args);
bsw/jbe@1309 5065 cache[methodName] = value;
bsw/jbe@1309 5066 return value;
bsw/jbe@1309 5067 }
bsw/jbe@1309 5068 };
bsw/jbe@1309 5069 }
bsw/jbe@1309 5070
bsw/jbe@1309 5071 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5072
bsw/jbe@1309 5073 function NodeWrapper(node, session) {
bsw/jbe@1309 5074 this.node = node;
bsw/jbe@1309 5075 this.session = session;
bsw/jbe@1309 5076 this.cache = new Cache();
bsw/jbe@1309 5077 this.positions = new Cache();
bsw/jbe@1309 5078 }
bsw/jbe@1309 5079
bsw/jbe@1309 5080 var nodeProto = {
bsw/jbe@1309 5081 getPosition: function(offset) {
bsw/jbe@1309 5082 var positions = this.positions;
bsw/jbe@1309 5083 return positions.get(offset) || positions.set(offset, new Position(this, offset));
bsw/jbe@1309 5084 },
bsw/jbe@1309 5085
bsw/jbe@1309 5086 toString: function() {
bsw/jbe@1309 5087 return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
bsw/jbe@1309 5088 }
bsw/jbe@1309 5089 };
bsw/jbe@1309 5090
bsw/jbe@1309 5091 NodeWrapper.prototype = nodeProto;
bsw/jbe@1309 5092
bsw/jbe@1309 5093 var EMPTY = "EMPTY",
bsw/jbe@1309 5094 NON_SPACE = "NON_SPACE",
bsw/jbe@1309 5095 UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
bsw/jbe@1309 5096 COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
bsw/jbe@1309 5097 TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
bsw/jbe@1309 5098 TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
bsw/jbe@1309 5099 TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
bsw/jbe@1309 5100 PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
bsw/jbe@1309 5101 TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
bsw/jbe@1309 5102 INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
bsw/jbe@1309 5103
bsw/jbe@1309 5104 extend(nodeProto, {
bsw/jbe@1309 5105 isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
bsw/jbe@1309 5106 getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
bsw/jbe@1309 5107 getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
bsw/jbe@1309 5108 containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
bsw/jbe@1309 5109 isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
bsw/jbe@1309 5110 isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
bsw/jbe@1309 5111 getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
bsw/jbe@1309 5112 isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
bsw/jbe@1309 5113 isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
bsw/jbe@1309 5114 next: createCachingGetter("nextPos", nextNode, "node"),
bsw/jbe@1309 5115 previous: createCachingGetter("previous", previousNode, "node"),
bsw/jbe@1309 5116
bsw/jbe@1309 5117 getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
bsw/jbe@1309 5118 var spaceRegex = null, collapseSpaces = false;
bsw/jbe@1309 5119 var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
bsw/jbe@1309 5120 var preLine = (cssWhitespace == "pre-line");
bsw/jbe@1309 5121 if (preLine) {
bsw/jbe@1309 5122 spaceRegex = spacesMinusLineBreaksRegex;
bsw/jbe@1309 5123 collapseSpaces = true;
bsw/jbe@1309 5124 } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
bsw/jbe@1309 5125 spaceRegex = spacesRegex;
bsw/jbe@1309 5126 collapseSpaces = true;
bsw/jbe@1309 5127 }
bsw/jbe@1309 5128
bsw/jbe@1309 5129 return {
bsw/jbe@1309 5130 node: textNode,
bsw/jbe@1309 5131 text: textNode.data,
bsw/jbe@1309 5132 spaceRegex: spaceRegex,
bsw/jbe@1309 5133 collapseSpaces: collapseSpaces,
bsw/jbe@1309 5134 preLine: preLine
bsw/jbe@1309 5135 };
bsw/jbe@1309 5136 }, "node"),
bsw/jbe@1309 5137
bsw/jbe@1309 5138 hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
bsw/jbe@1309 5139 var session = this.session;
bsw/jbe@1309 5140 var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
bsw/jbe@1309 5141 var firstPosInEl = session.getPosition(el, 0);
bsw/jbe@1309 5142
bsw/jbe@1309 5143 var pos = backward ? posAfterEl : firstPosInEl;
bsw/jbe@1309 5144 var endPos = backward ? firstPosInEl : posAfterEl;
bsw/jbe@1309 5145
bsw/jbe@1309 5146 /*
bsw/jbe@1309 5147 <body><p>X </p><p>Y</p></body>
bsw/jbe@1309 5148
bsw/jbe@1309 5149 Positions:
bsw/jbe@1309 5150
bsw/jbe@1309 5151 body:0:""
bsw/jbe@1309 5152 p:0:""
bsw/jbe@1309 5153 text:0:""
bsw/jbe@1309 5154 text:1:"X"
bsw/jbe@1309 5155 text:2:TRAILING_SPACE_IN_BLOCK
bsw/jbe@1309 5156 text:3:COLLAPSED_SPACE
bsw/jbe@1309 5157 p:1:""
bsw/jbe@1309 5158 body:1:"\n"
bsw/jbe@1309 5159 p:0:""
bsw/jbe@1309 5160 text:0:""
bsw/jbe@1309 5161 text:1:"Y"
bsw/jbe@1309 5162
bsw/jbe@1309 5163 A character is a TRAILING_SPACE_IN_BLOCK iff:
bsw/jbe@1309 5164
bsw/jbe@1309 5165 - There is no uncollapsed character after it within the visible containing block element
bsw/jbe@1309 5166
bsw/jbe@1309 5167 A character is a TRAILING_SPACE_BEFORE_BR iff:
bsw/jbe@1309 5168
bsw/jbe@1309 5169 - There is no uncollapsed character after it preceding a <br> element
bsw/jbe@1309 5170
bsw/jbe@1309 5171 An element has inner text iff
bsw/jbe@1309 5172
bsw/jbe@1309 5173 - It is not hidden
bsw/jbe@1309 5174 - It contains an uncollapsed character
bsw/jbe@1309 5175
bsw/jbe@1309 5176 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
bsw/jbe@1309 5177 */
bsw/jbe@1309 5178
bsw/jbe@1309 5179 while (pos !== endPos) {
bsw/jbe@1309 5180 pos.prepopulateChar();
bsw/jbe@1309 5181 if (pos.isDefinitelyNonEmpty()) {
bsw/jbe@1309 5182 return true;
bsw/jbe@1309 5183 }
bsw/jbe@1309 5184 pos = backward ? pos.previousVisible() : pos.nextVisible();
bsw/jbe@1309 5185 }
bsw/jbe@1309 5186
bsw/jbe@1309 5187 return false;
bsw/jbe@1309 5188 }, "node"),
bsw/jbe@1309 5189
bsw/jbe@1309 5190 isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
bsw/jbe@1309 5191 // Ensure that a block element containing a <br> is considered to have inner text
bsw/jbe@1309 5192 var brs = el.getElementsByTagName("br");
bsw/jbe@1309 5193 for (var i = 0, len = brs.length; i < len; ++i) {
bsw/jbe@1309 5194 if (!isCollapsedNode(brs[i])) {
bsw/jbe@1309 5195 return true;
bsw/jbe@1309 5196 }
bsw/jbe@1309 5197 }
bsw/jbe@1309 5198 return this.hasInnerText();
bsw/jbe@1309 5199 }, "node"),
bsw/jbe@1309 5200
bsw/jbe@1309 5201 getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
bsw/jbe@1309 5202 if (el.tagName.toLowerCase() == "br") {
bsw/jbe@1309 5203 return "";
bsw/jbe@1309 5204 } else {
bsw/jbe@1309 5205 switch (this.getComputedDisplay()) {
bsw/jbe@1309 5206 case "inline":
bsw/jbe@1309 5207 var child = el.lastChild;
bsw/jbe@1309 5208 while (child) {
bsw/jbe@1309 5209 if (!isIgnoredNode(child)) {
bsw/jbe@1309 5210 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
bsw/jbe@1309 5211 }
bsw/jbe@1309 5212 child = child.previousSibling;
bsw/jbe@1309 5213 }
bsw/jbe@1309 5214 break;
bsw/jbe@1309 5215 case "inline-block":
bsw/jbe@1309 5216 case "inline-table":
bsw/jbe@1309 5217 case "none":
bsw/jbe@1309 5218 case "table-column":
bsw/jbe@1309 5219 case "table-column-group":
bsw/jbe@1309 5220 break;
bsw/jbe@1309 5221 case "table-cell":
bsw/jbe@1309 5222 return "\t";
bsw/jbe@1309 5223 default:
bsw/jbe@1309 5224 return this.isRenderedBlock(true) ? "\n" : "";
bsw/jbe@1309 5225 }
bsw/jbe@1309 5226 }
bsw/jbe@1309 5227 return "";
bsw/jbe@1309 5228 }, "node"),
bsw/jbe@1309 5229
bsw/jbe@1309 5230 getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
bsw/jbe@1309 5231 switch (this.getComputedDisplay()) {
bsw/jbe@1309 5232 case "inline":
bsw/jbe@1309 5233 case "inline-block":
bsw/jbe@1309 5234 case "inline-table":
bsw/jbe@1309 5235 case "none":
bsw/jbe@1309 5236 case "table-column":
bsw/jbe@1309 5237 case "table-column-group":
bsw/jbe@1309 5238 case "table-cell":
bsw/jbe@1309 5239 break;
bsw/jbe@1309 5240 default:
bsw/jbe@1309 5241 return this.isRenderedBlock(false) ? "\n" : "";
bsw/jbe@1309 5242 }
bsw/jbe@1309 5243 return "";
bsw/jbe@1309 5244 }, "node")
bsw/jbe@1309 5245 });
bsw/jbe@1309 5246
bsw/jbe@1309 5247 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5248
bsw/jbe@1309 5249 function Position(nodeWrapper, offset) {
bsw/jbe@1309 5250 this.offset = offset;
bsw/jbe@1309 5251 this.nodeWrapper = nodeWrapper;
bsw/jbe@1309 5252 this.node = nodeWrapper.node;
bsw/jbe@1309 5253 this.session = nodeWrapper.session;
bsw/jbe@1309 5254 this.cache = new Cache();
bsw/jbe@1309 5255 }
bsw/jbe@1309 5256
bsw/jbe@1309 5257 function inspectPosition() {
bsw/jbe@1309 5258 return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
bsw/jbe@1309 5259 }
bsw/jbe@1309 5260
bsw/jbe@1309 5261 var positionProto = {
bsw/jbe@1309 5262 character: "",
bsw/jbe@1309 5263 characterType: EMPTY,
bsw/jbe@1309 5264 isBr: false,
bsw/jbe@1309 5265
bsw/jbe@1309 5266 /*
bsw/jbe@1309 5267 This method:
bsw/jbe@1309 5268 - Fully populates positions that have characters that can be determined independently of any other characters.
bsw/jbe@1309 5269 - Populates most types of space positions with a provisional character. The character is finalized later.
bsw/jbe@1309 5270 */
bsw/jbe@1309 5271 prepopulateChar: function() {
bsw/jbe@1309 5272 var pos = this;
bsw/jbe@1309 5273 if (!pos.prepopulatedChar) {
bsw/jbe@1309 5274 var node = pos.node, offset = pos.offset;
bsw/jbe@1309 5275 var visibleChar = "", charType = EMPTY;
bsw/jbe@1309 5276 var finalizedChar = false;
bsw/jbe@1309 5277 if (offset > 0) {
bsw/jbe@1309 5278 if (node.nodeType == 3) {
bsw/jbe@1309 5279 var text = node.data;
bsw/jbe@1309 5280 var textChar = text.charAt(offset - 1);
bsw/jbe@1309 5281
bsw/jbe@1309 5282 var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
bsw/jbe@1309 5283 var spaceRegex = nodeInfo.spaceRegex;
bsw/jbe@1309 5284 if (nodeInfo.collapseSpaces) {
bsw/jbe@1309 5285 if (spaceRegex.test(textChar)) {
bsw/jbe@1309 5286 // "If the character at position is from set, append a single space (U+0020) to newdata and advance
bsw/jbe@1309 5287 // position until the character at position is not from set."
bsw/jbe@1309 5288
bsw/jbe@1309 5289 // We also need to check for the case where we're in a pre-line and we have a space preceding a
bsw/jbe@1309 5290 // line break, because such spaces are collapsed in some browsers
bsw/jbe@1309 5291 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
bsw/jbe@1309 5292 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
bsw/jbe@1309 5293 visibleChar = " ";
bsw/jbe@1309 5294 charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
bsw/jbe@1309 5295 } else {
bsw/jbe@1309 5296 visibleChar = " ";
bsw/jbe@1309 5297 //pos.checkForFollowingLineBreak = true;
bsw/jbe@1309 5298 charType = COLLAPSIBLE_SPACE;
bsw/jbe@1309 5299 }
bsw/jbe@1309 5300 } else {
bsw/jbe@1309 5301 visibleChar = textChar;
bsw/jbe@1309 5302 charType = NON_SPACE;
bsw/jbe@1309 5303 finalizedChar = true;
bsw/jbe@1309 5304 }
bsw/jbe@1309 5305 } else {
bsw/jbe@1309 5306 visibleChar = textChar;
bsw/jbe@1309 5307 charType = UNCOLLAPSIBLE_SPACE;
bsw/jbe@1309 5308 finalizedChar = true;
bsw/jbe@1309 5309 }
bsw/jbe@1309 5310 } else {
bsw/jbe@1309 5311 var nodePassed = node.childNodes[offset - 1];
bsw/jbe@1309 5312 if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
bsw/jbe@1309 5313 if (nodePassed.tagName.toLowerCase() == "br") {
bsw/jbe@1309 5314 visibleChar = "\n";
bsw/jbe@1309 5315 pos.isBr = true;
bsw/jbe@1309 5316 charType = COLLAPSIBLE_SPACE;
bsw/jbe@1309 5317 finalizedChar = false;
bsw/jbe@1309 5318 } else {
bsw/jbe@1309 5319 pos.checkForTrailingSpace = true;
bsw/jbe@1309 5320 }
bsw/jbe@1309 5321 }
bsw/jbe@1309 5322
bsw/jbe@1309 5323 // Check the leading space of the next node for the case when a block element follows an inline
bsw/jbe@1309 5324 // element or text node. In that case, there is an implied line break between the two nodes.
bsw/jbe@1309 5325 if (!visibleChar) {
bsw/jbe@1309 5326 var nextNode = node.childNodes[offset];
bsw/jbe@1309 5327 if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
bsw/jbe@1309 5328 pos.checkForLeadingSpace = true;
bsw/jbe@1309 5329 }
bsw/jbe@1309 5330 }
bsw/jbe@1309 5331 }
bsw/jbe@1309 5332 }
bsw/jbe@1309 5333
bsw/jbe@1309 5334 pos.prepopulatedChar = true;
bsw/jbe@1309 5335 pos.character = visibleChar;
bsw/jbe@1309 5336 pos.characterType = charType;
bsw/jbe@1309 5337 pos.isCharInvariant = finalizedChar;
bsw/jbe@1309 5338 }
bsw/jbe@1309 5339 },
bsw/jbe@1309 5340
bsw/jbe@1309 5341 isDefinitelyNonEmpty: function() {
bsw/jbe@1309 5342 var charType = this.characterType;
bsw/jbe@1309 5343 return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
bsw/jbe@1309 5344 },
bsw/jbe@1309 5345
bsw/jbe@1309 5346 // Resolve leading and trailing spaces, which may involve prepopulating other positions
bsw/jbe@1309 5347 resolveLeadingAndTrailingSpaces: function() {
bsw/jbe@1309 5348 if (!this.prepopulatedChar) {
bsw/jbe@1309 5349 this.prepopulateChar();
bsw/jbe@1309 5350 }
bsw/jbe@1309 5351 if (this.checkForTrailingSpace) {
bsw/jbe@1309 5352 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
bsw/jbe@1309 5353 if (trailingSpace) {
bsw/jbe@1309 5354 this.isTrailingSpace = true;
bsw/jbe@1309 5355 this.character = trailingSpace;
bsw/jbe@1309 5356 this.characterType = COLLAPSIBLE_SPACE;
bsw/jbe@1309 5357 }
bsw/jbe@1309 5358 this.checkForTrailingSpace = false;
bsw/jbe@1309 5359 }
bsw/jbe@1309 5360 if (this.checkForLeadingSpace) {
bsw/jbe@1309 5361 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
bsw/jbe@1309 5362 if (leadingSpace) {
bsw/jbe@1309 5363 this.isLeadingSpace = true;
bsw/jbe@1309 5364 this.character = leadingSpace;
bsw/jbe@1309 5365 this.characterType = COLLAPSIBLE_SPACE;
bsw/jbe@1309 5366 }
bsw/jbe@1309 5367 this.checkForLeadingSpace = false;
bsw/jbe@1309 5368 }
bsw/jbe@1309 5369 },
bsw/jbe@1309 5370
bsw/jbe@1309 5371 getPrecedingUncollapsedPosition: function(characterOptions) {
bsw/jbe@1309 5372 var pos = this, character;
bsw/jbe@1309 5373 while ( (pos = pos.previousVisible()) ) {
bsw/jbe@1309 5374 character = pos.getCharacter(characterOptions);
bsw/jbe@1309 5375 if (character !== "") {
bsw/jbe@1309 5376 return pos;
bsw/jbe@1309 5377 }
bsw/jbe@1309 5378 }
bsw/jbe@1309 5379
bsw/jbe@1309 5380 return null;
bsw/jbe@1309 5381 },
bsw/jbe@1309 5382
bsw/jbe@1309 5383 getCharacter: function(characterOptions) {
bsw/jbe@1309 5384 this.resolveLeadingAndTrailingSpaces();
bsw/jbe@1309 5385
bsw/jbe@1309 5386 var thisChar = this.character, returnChar;
bsw/jbe@1309 5387
bsw/jbe@1309 5388 // Check if character is ignored
bsw/jbe@1309 5389 var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
bsw/jbe@1309 5390 var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
bsw/jbe@1309 5391
bsw/jbe@1309 5392 // Check if this position's character is invariant (i.e. not dependent on character options) and return it
bsw/jbe@1309 5393 // if so
bsw/jbe@1309 5394 if (this.isCharInvariant) {
bsw/jbe@1309 5395 returnChar = isIgnoredCharacter ? "" : thisChar;
bsw/jbe@1309 5396 return returnChar;
bsw/jbe@1309 5397 }
bsw/jbe@1309 5398
bsw/jbe@1309 5399 var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
bsw/jbe@1309 5400 var cachedChar = this.cache.get(cacheKey);
bsw/jbe@1309 5401 if (cachedChar !== null) {
bsw/jbe@1309 5402 return cachedChar;
bsw/jbe@1309 5403 }
bsw/jbe@1309 5404
bsw/jbe@1309 5405 // We need to actually get the character now
bsw/jbe@1309 5406 var character = "";
bsw/jbe@1309 5407 var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
bsw/jbe@1309 5408
bsw/jbe@1309 5409 var nextPos, previousPos;
bsw/jbe@1309 5410 var gotPreviousPos = false;
bsw/jbe@1309 5411 var pos = this;
bsw/jbe@1309 5412
bsw/jbe@1309 5413 function getPreviousPos() {
bsw/jbe@1309 5414 if (!gotPreviousPos) {
bsw/jbe@1309 5415 previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
bsw/jbe@1309 5416 gotPreviousPos = true;
bsw/jbe@1309 5417 }
bsw/jbe@1309 5418 return previousPos;
bsw/jbe@1309 5419 }
bsw/jbe@1309 5420
bsw/jbe@1309 5421 // Disallow a collapsible space that is followed by a line break or is the last character
bsw/jbe@1309 5422 if (collapsible) {
bsw/jbe@1309 5423 // Allow a trailing space that we've previously determined should be included
bsw/jbe@1309 5424 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
bsw/jbe@1309 5425 character = "\n";
bsw/jbe@1309 5426 }
bsw/jbe@1309 5427 // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
bsw/jbe@1309 5428 // or follows a collapsible included space
bsw/jbe@1309 5429 else if (thisChar == " " &&
bsw/jbe@1309 5430 (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
bsw/jbe@1309 5431 }
bsw/jbe@1309 5432 // Allow a leading line break unless it follows a line break
bsw/jbe@1309 5433 else if (thisChar == "\n" && this.isLeadingSpace) {
bsw/jbe@1309 5434 if (getPreviousPos() && previousPos.character != "\n") {
bsw/jbe@1309 5435 character = "\n";
bsw/jbe@1309 5436 } else {
bsw/jbe@1309 5437 }
bsw/jbe@1309 5438 } else {
bsw/jbe@1309 5439 nextPos = this.nextUncollapsed();
bsw/jbe@1309 5440 if (nextPos) {
bsw/jbe@1309 5441 if (nextPos.isBr) {
bsw/jbe@1309 5442 this.type = TRAILING_SPACE_BEFORE_BR;
bsw/jbe@1309 5443 } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
bsw/jbe@1309 5444 this.type = TRAILING_SPACE_IN_BLOCK;
bsw/jbe@1309 5445 } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
bsw/jbe@1309 5446 this.type = TRAILING_SPACE_BEFORE_BLOCK;
bsw/jbe@1309 5447 }
bsw/jbe@1309 5448
bsw/jbe@1309 5449 if (nextPos.character == "\n") {
bsw/jbe@1309 5450 if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
bsw/jbe@1309 5451 } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
bsw/jbe@1309 5452 } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
bsw/jbe@1309 5453 } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
bsw/jbe@1309 5454 } else if (thisChar == "\n") {
bsw/jbe@1309 5455 if (nextPos.isTrailingSpace) {
bsw/jbe@1309 5456 if (this.isTrailingSpace) {
bsw/jbe@1309 5457 } else if (this.isBr) {
bsw/jbe@1309 5458 nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
bsw/jbe@1309 5459
bsw/jbe@1309 5460 if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
bsw/jbe@1309 5461 nextPos.character = "";
bsw/jbe@1309 5462 } else {
bsw/jbe@1309 5463 nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
bsw/jbe@1309 5464 }
bsw/jbe@1309 5465 }
bsw/jbe@1309 5466 } else {
bsw/jbe@1309 5467 character = "\n";
bsw/jbe@1309 5468 }
bsw/jbe@1309 5469 } else if (thisChar == " ") {
bsw/jbe@1309 5470 character = " ";
bsw/jbe@1309 5471 } else {
bsw/jbe@1309 5472 }
bsw/jbe@1309 5473 } else {
bsw/jbe@1309 5474 character = thisChar;
bsw/jbe@1309 5475 }
bsw/jbe@1309 5476 } else {
bsw/jbe@1309 5477 }
bsw/jbe@1309 5478 }
bsw/jbe@1309 5479 }
bsw/jbe@1309 5480
bsw/jbe@1309 5481 if (ignoredChars.indexOf(character) > -1) {
bsw/jbe@1309 5482 character = "";
bsw/jbe@1309 5483 }
bsw/jbe@1309 5484
bsw/jbe@1309 5485
bsw/jbe@1309 5486 this.cache.set(cacheKey, character);
bsw/jbe@1309 5487
bsw/jbe@1309 5488 return character;
bsw/jbe@1309 5489 },
bsw/jbe@1309 5490
bsw/jbe@1309 5491 equals: function(pos) {
bsw/jbe@1309 5492 return !!pos && this.node === pos.node && this.offset === pos.offset;
bsw/jbe@1309 5493 },
bsw/jbe@1309 5494
bsw/jbe@1309 5495 inspect: inspectPosition,
bsw/jbe@1309 5496
bsw/jbe@1309 5497 toString: function() {
bsw/jbe@1309 5498 return this.character;
bsw/jbe@1309 5499 }
bsw/jbe@1309 5500 };
bsw/jbe@1309 5501
bsw/jbe@1309 5502 Position.prototype = positionProto;
bsw/jbe@1309 5503
bsw/jbe@1309 5504 extend(positionProto, {
bsw/jbe@1309 5505 next: createCachingGetter("nextPos", function(pos) {
bsw/jbe@1309 5506 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
bsw/jbe@1309 5507 if (!node) {
bsw/jbe@1309 5508 return null;
bsw/jbe@1309 5509 }
bsw/jbe@1309 5510 var nextNode, nextOffset, child;
bsw/jbe@1309 5511 if (offset == nodeWrapper.getLength()) {
bsw/jbe@1309 5512 // Move onto the next node
bsw/jbe@1309 5513 nextNode = node.parentNode;
bsw/jbe@1309 5514 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
bsw/jbe@1309 5515 } else {
bsw/jbe@1309 5516 if (nodeWrapper.isCharacterDataNode()) {
bsw/jbe@1309 5517 nextNode = node;
bsw/jbe@1309 5518 nextOffset = offset + 1;
bsw/jbe@1309 5519 } else {
bsw/jbe@1309 5520 child = node.childNodes[offset];
bsw/jbe@1309 5521 // Go into the children next, if children there are
bsw/jbe@1309 5522 if (session.getNodeWrapper(child).containsPositions()) {
bsw/jbe@1309 5523 nextNode = child;
bsw/jbe@1309 5524 nextOffset = 0;
bsw/jbe@1309 5525 } else {
bsw/jbe@1309 5526 nextNode = node;
bsw/jbe@1309 5527 nextOffset = offset + 1;
bsw/jbe@1309 5528 }
bsw/jbe@1309 5529 }
bsw/jbe@1309 5530 }
bsw/jbe@1309 5531
bsw/jbe@1309 5532 return nextNode ? session.getPosition(nextNode, nextOffset) : null;
bsw/jbe@1309 5533 }),
bsw/jbe@1309 5534
bsw/jbe@1309 5535 previous: createCachingGetter("previous", function(pos) {
bsw/jbe@1309 5536 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
bsw/jbe@1309 5537 var previousNode, previousOffset, child;
bsw/jbe@1309 5538 if (offset == 0) {
bsw/jbe@1309 5539 previousNode = node.parentNode;
bsw/jbe@1309 5540 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
bsw/jbe@1309 5541 } else {
bsw/jbe@1309 5542 if (nodeWrapper.isCharacterDataNode()) {
bsw/jbe@1309 5543 previousNode = node;
bsw/jbe@1309 5544 previousOffset = offset - 1;
bsw/jbe@1309 5545 } else {
bsw/jbe@1309 5546 child = node.childNodes[offset - 1];
bsw/jbe@1309 5547 // Go into the children next, if children there are
bsw/jbe@1309 5548 if (session.getNodeWrapper(child).containsPositions()) {
bsw/jbe@1309 5549 previousNode = child;
bsw/jbe@1309 5550 previousOffset = dom.getNodeLength(child);
bsw/jbe@1309 5551 } else {
bsw/jbe@1309 5552 previousNode = node;
bsw/jbe@1309 5553 previousOffset = offset - 1;
bsw/jbe@1309 5554 }
bsw/jbe@1309 5555 }
bsw/jbe@1309 5556 }
bsw/jbe@1309 5557 return previousNode ? session.getPosition(previousNode, previousOffset) : null;
bsw/jbe@1309 5558 }),
bsw/jbe@1309 5559
bsw/jbe@1309 5560 /*
bsw/jbe@1309 5561 Next and previous position moving functions that filter out
bsw/jbe@1309 5562
bsw/jbe@1309 5563 - Hidden (CSS visibility/display) elements
bsw/jbe@1309 5564 - Script and style elements
bsw/jbe@1309 5565 */
bsw/jbe@1309 5566 nextVisible: createCachingGetter("nextVisible", function(pos) {
bsw/jbe@1309 5567 var next = pos.next();
bsw/jbe@1309 5568 if (!next) {
bsw/jbe@1309 5569 return null;
bsw/jbe@1309 5570 }
bsw/jbe@1309 5571 var nodeWrapper = next.nodeWrapper, node = next.node;
bsw/jbe@1309 5572 var newPos = next;
bsw/jbe@1309 5573 if (nodeWrapper.isCollapsed()) {
bsw/jbe@1309 5574 // We're skipping this node and all its descendants
bsw/jbe@1309 5575 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
bsw/jbe@1309 5576 }
bsw/jbe@1309 5577 return newPos;
bsw/jbe@1309 5578 }),
bsw/jbe@1309 5579
bsw/jbe@1309 5580 nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
bsw/jbe@1309 5581 var nextPos = pos;
bsw/jbe@1309 5582 while ( (nextPos = nextPos.nextVisible()) ) {
bsw/jbe@1309 5583 nextPos.resolveLeadingAndTrailingSpaces();
bsw/jbe@1309 5584 if (nextPos.character !== "") {
bsw/jbe@1309 5585 return nextPos;
bsw/jbe@1309 5586 }
bsw/jbe@1309 5587 }
bsw/jbe@1309 5588 return null;
bsw/jbe@1309 5589 }),
bsw/jbe@1309 5590
bsw/jbe@1309 5591 previousVisible: createCachingGetter("previousVisible", function(pos) {
bsw/jbe@1309 5592 var previous = pos.previous();
bsw/jbe@1309 5593 if (!previous) {
bsw/jbe@1309 5594 return null;
bsw/jbe@1309 5595 }
bsw/jbe@1309 5596 var nodeWrapper = previous.nodeWrapper, node = previous.node;
bsw/jbe@1309 5597 var newPos = previous;
bsw/jbe@1309 5598 if (nodeWrapper.isCollapsed()) {
bsw/jbe@1309 5599 // We're skipping this node and all its descendants
bsw/jbe@1309 5600 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
bsw/jbe@1309 5601 }
bsw/jbe@1309 5602 return newPos;
bsw/jbe@1309 5603 })
bsw/jbe@1309 5604 });
bsw/jbe@1309 5605
bsw/jbe@1309 5606 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5607
bsw/jbe@1309 5608 var currentSession = null;
bsw/jbe@1309 5609
bsw/jbe@1309 5610 var Session = (function() {
bsw/jbe@1309 5611 function createWrapperCache(nodeProperty) {
bsw/jbe@1309 5612 var cache = new Cache();
bsw/jbe@1309 5613
bsw/jbe@1309 5614 return {
bsw/jbe@1309 5615 get: function(node) {
bsw/jbe@1309 5616 var wrappersByProperty = cache.get(node[nodeProperty]);
bsw/jbe@1309 5617 if (wrappersByProperty) {
bsw/jbe@1309 5618 for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
bsw/jbe@1309 5619 if (wrapper.node === node) {
bsw/jbe@1309 5620 return wrapper;
bsw/jbe@1309 5621 }
bsw/jbe@1309 5622 }
bsw/jbe@1309 5623 }
bsw/jbe@1309 5624 return null;
bsw/jbe@1309 5625 },
bsw/jbe@1309 5626
bsw/jbe@1309 5627 set: function(nodeWrapper) {
bsw/jbe@1309 5628 var property = nodeWrapper.node[nodeProperty];
bsw/jbe@1309 5629 var wrappersByProperty = cache.get(property) || cache.set(property, []);
bsw/jbe@1309 5630 wrappersByProperty.push(nodeWrapper);
bsw/jbe@1309 5631 }
bsw/jbe@1309 5632 };
bsw/jbe@1309 5633 }
bsw/jbe@1309 5634
bsw/jbe@1309 5635 var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
bsw/jbe@1309 5636
bsw/jbe@1309 5637 function Session() {
bsw/jbe@1309 5638 this.initCaches();
bsw/jbe@1309 5639 }
bsw/jbe@1309 5640
bsw/jbe@1309 5641 Session.prototype = {
bsw/jbe@1309 5642 initCaches: function() {
bsw/jbe@1309 5643 this.elementCache = uniqueIDSupported ? (function() {
bsw/jbe@1309 5644 var elementsCache = new Cache();
bsw/jbe@1309 5645
bsw/jbe@1309 5646 return {
bsw/jbe@1309 5647 get: function(el) {
bsw/jbe@1309 5648 return elementsCache.get(el.uniqueID);
bsw/jbe@1309 5649 },
bsw/jbe@1309 5650
bsw/jbe@1309 5651 set: function(elWrapper) {
bsw/jbe@1309 5652 elementsCache.set(elWrapper.node.uniqueID, elWrapper);
bsw/jbe@1309 5653 }
bsw/jbe@1309 5654 };
bsw/jbe@1309 5655 })() : createWrapperCache("tagName");
bsw/jbe@1309 5656
bsw/jbe@1309 5657 // Store text nodes keyed by data, although we may need to truncate this
bsw/jbe@1309 5658 this.textNodeCache = createWrapperCache("data");
bsw/jbe@1309 5659 this.otherNodeCache = createWrapperCache("nodeName");
bsw/jbe@1309 5660 },
bsw/jbe@1309 5661
bsw/jbe@1309 5662 getNodeWrapper: function(node) {
bsw/jbe@1309 5663 var wrapperCache;
bsw/jbe@1309 5664 switch (node.nodeType) {
bsw/jbe@1309 5665 case 1:
bsw/jbe@1309 5666 wrapperCache = this.elementCache;
bsw/jbe@1309 5667 break;
bsw/jbe@1309 5668 case 3:
bsw/jbe@1309 5669 wrapperCache = this.textNodeCache;
bsw/jbe@1309 5670 break;
bsw/jbe@1309 5671 default:
bsw/jbe@1309 5672 wrapperCache = this.otherNodeCache;
bsw/jbe@1309 5673 break;
bsw/jbe@1309 5674 }
bsw/jbe@1309 5675
bsw/jbe@1309 5676 var wrapper = wrapperCache.get(node);
bsw/jbe@1309 5677 if (!wrapper) {
bsw/jbe@1309 5678 wrapper = new NodeWrapper(node, this);
bsw/jbe@1309 5679 wrapperCache.set(wrapper);
bsw/jbe@1309 5680 }
bsw/jbe@1309 5681 return wrapper;
bsw/jbe@1309 5682 },
bsw/jbe@1309 5683
bsw/jbe@1309 5684 getPosition: function(node, offset) {
bsw/jbe@1309 5685 return this.getNodeWrapper(node).getPosition(offset);
bsw/jbe@1309 5686 },
bsw/jbe@1309 5687
bsw/jbe@1309 5688 getRangeBoundaryPosition: function(range, isStart) {
bsw/jbe@1309 5689 var prefix = isStart ? "start" : "end";
bsw/jbe@1309 5690 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
bsw/jbe@1309 5691 },
bsw/jbe@1309 5692
bsw/jbe@1309 5693 detach: function() {
bsw/jbe@1309 5694 this.elementCache = this.textNodeCache = this.otherNodeCache = null;
bsw/jbe@1309 5695 }
bsw/jbe@1309 5696 };
bsw/jbe@1309 5697
bsw/jbe@1309 5698 return Session;
bsw/jbe@1309 5699 })();
bsw/jbe@1309 5700
bsw/jbe@1309 5701 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5702
bsw/jbe@1309 5703 function startSession() {
bsw/jbe@1309 5704 endSession();
bsw/jbe@1309 5705 return (currentSession = new Session());
bsw/jbe@1309 5706 }
bsw/jbe@1309 5707
bsw/jbe@1309 5708 function getSession() {
bsw/jbe@1309 5709 return currentSession || startSession();
bsw/jbe@1309 5710 }
bsw/jbe@1309 5711
bsw/jbe@1309 5712 function endSession() {
bsw/jbe@1309 5713 if (currentSession) {
bsw/jbe@1309 5714 currentSession.detach();
bsw/jbe@1309 5715 }
bsw/jbe@1309 5716 currentSession = null;
bsw/jbe@1309 5717 }
bsw/jbe@1309 5718
bsw/jbe@1309 5719 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5720
bsw/jbe@1309 5721 // Extensions to the rangy.dom utility object
bsw/jbe@1309 5722
bsw/jbe@1309 5723 extend(dom, {
bsw/jbe@1309 5724 nextNode: nextNode,
bsw/jbe@1309 5725 previousNode: previousNode
bsw/jbe@1309 5726 });
bsw/jbe@1309 5727
bsw/jbe@1309 5728 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 5729
bsw/jbe@1309 5730 function createCharacterIterator(startPos, backward, endPos, characterOptions) {
bsw/jbe@1309 5731
bsw/jbe@1309 5732 // Adjust the end position to ensure that it is actually reached
bsw/jbe@1309 5733 if (endPos) {
bsw/jbe@1309 5734 if (backward) {
bsw/jbe@1309 5735 if (isCollapsedNode(endPos.node)) {
bsw/jbe@1309 5736 endPos = startPos.previousVisible();
bsw/jbe@1309 5737 }
bsw/jbe@1309 5738 } else {
bsw/jbe@1309 5739 if (isCollapsedNode(endPos.node)) {
bsw/jbe@1309 5740 endPos = endPos.nextVisible();
bsw/jbe@1309 5741 }
bsw/jbe@1309 5742 }
bsw/jbe@1309 5743 }
bsw/jbe@1309 5744
bsw/jbe@1309 5745 var pos = startPos, finished = false;
bsw/jbe@1309 5746
bsw/jbe@1309 5747 function next() {
bsw/jbe@1309 5748 var charPos = null;
bsw/jbe@1309 5749 if (backward) {
bsw/jbe@1309 5750 charPos = pos;
bsw/jbe@1309 5751 if (!finished) {
bsw/jbe@1309 5752 pos = pos.previousVisible();
bsw/jbe@1309 5753 finished = !pos || (endPos && pos.equals(endPos));
bsw/jbe@1309 5754 }
bsw/jbe@1309 5755 } else {
bsw/jbe@1309 5756 if (!finished) {
bsw/jbe@1309 5757 charPos = pos = pos.nextVisible();
bsw/jbe@1309 5758 finished = !pos || (endPos && pos.equals(endPos));
bsw/jbe@1309 5759 }
bsw/jbe@1309 5760 }
bsw/jbe@1309 5761 if (finished) {
bsw/jbe@1309 5762 pos = null;
bsw/jbe@1309 5763 }
bsw/jbe@1309 5764 return charPos;
bsw/jbe@1309 5765 }
bsw/jbe@1309 5766
bsw/jbe@1309 5767 var previousTextPos, returnPreviousTextPos = false;
bsw/jbe@1309 5768
bsw/jbe@1309 5769 return {
bsw/jbe@1309 5770 next: function() {
bsw/jbe@1309 5771 if (returnPreviousTextPos) {
bsw/jbe@1309 5772 returnPreviousTextPos = false;
bsw/jbe@1309 5773 return previousTextPos;
bsw/jbe@1309 5774 } else {
bsw/jbe@1309 5775 var pos, character;
bsw/jbe@1309 5776 while ( (pos = next()) ) {
bsw/jbe@1309 5777 character = pos.getCharacter(characterOptions);
bsw/jbe@1309 5778 if (character) {
bsw/jbe@1309 5779 previousTextPos = pos;
bsw/jbe@1309 5780 return pos;
bsw/jbe@1309 5781 }
bsw/jbe@1309 5782 }
bsw/jbe@1309 5783 return null;
bsw/jbe@1309 5784 }
bsw/jbe@1309 5785 },
bsw/jbe@1309 5786
bsw/jbe@1309 5787 rewind: function() {
bsw/jbe@1309 5788 if (previousTextPos) {
bsw/jbe@1309 5789 returnPreviousTextPos = true;
bsw/jbe@1309 5790 } else {
bsw/jbe@1309 5791 throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
bsw/jbe@1309 5792 }
bsw/jbe@1309 5793 },
bsw/jbe@1309 5794
bsw/jbe@1309 5795 dispose: function() {
bsw/jbe@1309 5796 startPos = endPos = null;
bsw/jbe@1309 5797 }
bsw/jbe@1309 5798 };
bsw/jbe@1309 5799 }
bsw/jbe@1309 5800
bsw/jbe@1309 5801 var arrayIndexOf = Array.prototype.indexOf ?
bsw/jbe@1309 5802 function(arr, val) {
bsw/jbe@1309 5803 return arr.indexOf(val);
bsw/jbe@1309 5804 } :
bsw/jbe@1309 5805 function(arr, val) {
bsw/jbe@1309 5806 for (var i = 0, len = arr.length; i < len; ++i) {
bsw/jbe@1309 5807 if (arr[i] === val) {
bsw/jbe@1309 5808 return i;
bsw/jbe@1309 5809 }
bsw/jbe@1309 5810 }
bsw/jbe@1309 5811 return -1;
bsw/jbe@1309 5812 };
bsw/jbe@1309 5813
bsw/jbe@1309 5814 // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
bsw/jbe@1309 5815 // is called and there is no more tokenized text
bsw/jbe@1309 5816 function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
bsw/jbe@1309 5817 var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
bsw/jbe@1309 5818 var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
bsw/jbe@1309 5819 var tokenizer = wordOptions.tokenizer;
bsw/jbe@1309 5820
bsw/jbe@1309 5821 // Consumes a word and the whitespace beyond it
bsw/jbe@1309 5822 function consumeWord(forward) {
bsw/jbe@1309 5823 var pos, textChar;
bsw/jbe@1309 5824 var newChars = [], it = forward ? forwardIterator : backwardIterator;
bsw/jbe@1309 5825
bsw/jbe@1309 5826 var passedWordBoundary = false, insideWord = false;
bsw/jbe@1309 5827
bsw/jbe@1309 5828 while ( (pos = it.next()) ) {
bsw/jbe@1309 5829 textChar = pos.character;
bsw/jbe@1309 5830
bsw/jbe@1309 5831
bsw/jbe@1309 5832 if (allWhiteSpaceRegex.test(textChar)) {
bsw/jbe@1309 5833 if (insideWord) {
bsw/jbe@1309 5834 insideWord = false;
bsw/jbe@1309 5835 passedWordBoundary = true;
bsw/jbe@1309 5836 }
bsw/jbe@1309 5837 } else {
bsw/jbe@1309 5838 if (passedWordBoundary) {
bsw/jbe@1309 5839 it.rewind();
bsw/jbe@1309 5840 break;
bsw/jbe@1309 5841 } else {
bsw/jbe@1309 5842 insideWord = true;
bsw/jbe@1309 5843 }
bsw/jbe@1309 5844 }
bsw/jbe@1309 5845 newChars.push(pos);
bsw/jbe@1309 5846 }
bsw/jbe@1309 5847
bsw/jbe@1309 5848
bsw/jbe@1309 5849 return newChars;
bsw/jbe@1309 5850 }
bsw/jbe@1309 5851
bsw/jbe@1309 5852 // Get initial word surrounding initial position and tokenize it
bsw/jbe@1309 5853 var forwardChars = consumeWord(true);
bsw/jbe@1309 5854 var backwardChars = consumeWord(false).reverse();
bsw/jbe@1309 5855 var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
bsw/jbe@1309 5856
bsw/jbe@1309 5857 // Create initial token buffers
bsw/jbe@1309 5858 var forwardTokensBuffer = forwardChars.length ?
bsw/jbe@1309 5859 tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
bsw/jbe@1309 5860
bsw/jbe@1309 5861 var backwardTokensBuffer = backwardChars.length ?
bsw/jbe@1309 5862 tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
bsw/jbe@1309 5863
bsw/jbe@1309 5864 function inspectBuffer(buffer) {
bsw/jbe@1309 5865 var textPositions = ["[" + buffer.length + "]"];
bsw/jbe@1309 5866 for (var i = 0; i < buffer.length; ++i) {
bsw/jbe@1309 5867 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
bsw/jbe@1309 5868 }
bsw/jbe@1309 5869 return textPositions;
bsw/jbe@1309 5870 }
bsw/jbe@1309 5871
bsw/jbe@1309 5872
bsw/jbe@1309 5873 return {
bsw/jbe@1309 5874 nextEndToken: function() {
bsw/jbe@1309 5875 var lastToken, forwardChars;
bsw/jbe@1309 5876
bsw/jbe@1309 5877 // If we're down to the last token, consume character chunks until we have a word or run out of
bsw/jbe@1309 5878 // characters to consume
bsw/jbe@1309 5879 while ( forwardTokensBuffer.length == 1 &&
bsw/jbe@1309 5880 !(lastToken = forwardTokensBuffer[0]).isWord &&
bsw/jbe@1309 5881 (forwardChars = consumeWord(true)).length > 0) {
bsw/jbe@1309 5882
bsw/jbe@1309 5883 // Merge trailing non-word into next word and tokenize
bsw/jbe@1309 5884 forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
bsw/jbe@1309 5885 }
bsw/jbe@1309 5886
bsw/jbe@1309 5887 return forwardTokensBuffer.shift();
bsw/jbe@1309 5888 },
bsw/jbe@1309 5889
bsw/jbe@1309 5890 previousStartToken: function() {
bsw/jbe@1309 5891 var lastToken, backwardChars;
bsw/jbe@1309 5892
bsw/jbe@1309 5893 // If we're down to the last token, consume character chunks until we have a word or run out of
bsw/jbe@1309 5894 // characters to consume
bsw/jbe@1309 5895 while ( backwardTokensBuffer.length == 1 &&
bsw/jbe@1309 5896 !(lastToken = backwardTokensBuffer[0]).isWord &&
bsw/jbe@1309 5897 (backwardChars = consumeWord(false)).length > 0) {
bsw/jbe@1309 5898
bsw/jbe@1309 5899 // Merge leading non-word into next word and tokenize
bsw/jbe@1309 5900 backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
bsw/jbe@1309 5901 }
bsw/jbe@1309 5902
bsw/jbe@1309 5903 return backwardTokensBuffer.pop();
bsw/jbe@1309 5904 },
bsw/jbe@1309 5905
bsw/jbe@1309 5906 dispose: function() {
bsw/jbe@1309 5907 forwardIterator.dispose();
bsw/jbe@1309 5908 backwardIterator.dispose();
bsw/jbe@1309 5909 forwardTokensBuffer = backwardTokensBuffer = null;
bsw/jbe@1309 5910 }
bsw/jbe@1309 5911 };
bsw/jbe@1309 5912 }
bsw/jbe@1309 5913
bsw/jbe@1309 5914 function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
bsw/jbe@1309 5915 var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
bsw/jbe@1309 5916 if (count !== 0) {
bsw/jbe@1309 5917 var backward = (count < 0);
bsw/jbe@1309 5918
bsw/jbe@1309 5919 switch (unit) {
bsw/jbe@1309 5920 case CHARACTER:
bsw/jbe@1309 5921 charIterator = createCharacterIterator(pos, backward, null, characterOptions);
bsw/jbe@1309 5922 while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
bsw/jbe@1309 5923 ++unitsMoved;
bsw/jbe@1309 5924 newPos = currentPos;
bsw/jbe@1309 5925 }
bsw/jbe@1309 5926 nextPos = currentPos;
bsw/jbe@1309 5927 charIterator.dispose();
bsw/jbe@1309 5928 break;
bsw/jbe@1309 5929 case WORD:
bsw/jbe@1309 5930 var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
bsw/jbe@1309 5931 var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
bsw/jbe@1309 5932
bsw/jbe@1309 5933 while ( (token = next()) && unitsMoved < absCount ) {
bsw/jbe@1309 5934 if (token.isWord) {
bsw/jbe@1309 5935 ++unitsMoved;
bsw/jbe@1309 5936 newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
bsw/jbe@1309 5937 }
bsw/jbe@1309 5938 }
bsw/jbe@1309 5939 break;
bsw/jbe@1309 5940 default:
bsw/jbe@1309 5941 throw new Error("movePositionBy: unit '" + unit + "' not implemented");
bsw/jbe@1309 5942 }
bsw/jbe@1309 5943
bsw/jbe@1309 5944 // Perform any necessary position tweaks
bsw/jbe@1309 5945 if (backward) {
bsw/jbe@1309 5946 newPos = newPos.previousVisible();
bsw/jbe@1309 5947 unitsMoved = -unitsMoved;
bsw/jbe@1309 5948 } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
bsw/jbe@1309 5949 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
bsw/jbe@1309 5950 // before a block element (for example, the line break between "1" and "2" in the following HTML:
bsw/jbe@1309 5951 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
bsw/jbe@1309 5952 // corresponds with a different selection position in most browsers from the one we want (i.e. at the
bsw/jbe@1309 5953 // start of the contents of the block element). We get round this by advancing the position returned to
bsw/jbe@1309 5954 // the last possible equivalent visible position.
bsw/jbe@1309 5955 if (unit == WORD) {
bsw/jbe@1309 5956 charIterator = createCharacterIterator(pos, false, null, characterOptions);
bsw/jbe@1309 5957 nextPos = charIterator.next();
bsw/jbe@1309 5958 charIterator.dispose();
bsw/jbe@1309 5959 }
bsw/jbe@1309 5960 if (nextPos) {
bsw/jbe@1309 5961 newPos = nextPos.previousVisible();
bsw/jbe@1309 5962 }
bsw/jbe@1309 5963 }
bsw/jbe@1309 5964 }
bsw/jbe@1309 5965
bsw/jbe@1309 5966
bsw/jbe@1309 5967 return {
bsw/jbe@1309 5968 position: newPos,
bsw/jbe@1309 5969 unitsMoved: unitsMoved
bsw/jbe@1309 5970 };
bsw/jbe@1309 5971 }
bsw/jbe@1309 5972
bsw/jbe@1309 5973 function createRangeCharacterIterator(session, range, characterOptions, backward) {
bsw/jbe@1309 5974 var rangeStart = session.getRangeBoundaryPosition(range, true);
bsw/jbe@1309 5975 var rangeEnd = session.getRangeBoundaryPosition(range, false);
bsw/jbe@1309 5976 var itStart = backward ? rangeEnd : rangeStart;
bsw/jbe@1309 5977 var itEnd = backward ? rangeStart : rangeEnd;
bsw/jbe@1309 5978
bsw/jbe@1309 5979 return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
bsw/jbe@1309 5980 }
bsw/jbe@1309 5981
bsw/jbe@1309 5982 function getRangeCharacters(session, range, characterOptions) {
bsw/jbe@1309 5983
bsw/jbe@1309 5984 var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
bsw/jbe@1309 5985 while ( (pos = it.next()) ) {
bsw/jbe@1309 5986 chars.push(pos);
bsw/jbe@1309 5987 }
bsw/jbe@1309 5988
bsw/jbe@1309 5989 it.dispose();
bsw/jbe@1309 5990 return chars;
bsw/jbe@1309 5991 }
bsw/jbe@1309 5992
bsw/jbe@1309 5993 function isWholeWord(startPos, endPos, wordOptions) {
bsw/jbe@1309 5994 var range = api.createRange(startPos.node);
bsw/jbe@1309 5995 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
bsw/jbe@1309 5996 return !range.expand("word", { wordOptions: wordOptions });
bsw/jbe@1309 5997 }
bsw/jbe@1309 5998
bsw/jbe@1309 5999 function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
bsw/jbe@1309 6000 var backward = isDirectionBackward(findOptions.direction);
bsw/jbe@1309 6001 var it = createCharacterIterator(
bsw/jbe@1309 6002 initialPos,
bsw/jbe@1309 6003 backward,
bsw/jbe@1309 6004 initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
bsw/jbe@1309 6005 findOptions.characterOptions
bsw/jbe@1309 6006 );
bsw/jbe@1309 6007 var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
bsw/jbe@1309 6008 var result, insideRegexMatch;
bsw/jbe@1309 6009 var returnValue = null;
bsw/jbe@1309 6010
bsw/jbe@1309 6011 function handleMatch(startIndex, endIndex) {
bsw/jbe@1309 6012 var startPos = chars[startIndex].previousVisible();
bsw/jbe@1309 6013 var endPos = chars[endIndex - 1];
bsw/jbe@1309 6014 var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
bsw/jbe@1309 6015
bsw/jbe@1309 6016 return {
bsw/jbe@1309 6017 startPos: startPos,
bsw/jbe@1309 6018 endPos: endPos,
bsw/jbe@1309 6019 valid: valid
bsw/jbe@1309 6020 };
bsw/jbe@1309 6021 }
bsw/jbe@1309 6022
bsw/jbe@1309 6023 while ( (pos = it.next()) ) {
bsw/jbe@1309 6024 currentChar = pos.character;
bsw/jbe@1309 6025 if (!isRegex && !findOptions.caseSensitive) {
bsw/jbe@1309 6026 currentChar = currentChar.toLowerCase();
bsw/jbe@1309 6027 }
bsw/jbe@1309 6028
bsw/jbe@1309 6029 if (backward) {
bsw/jbe@1309 6030 chars.unshift(pos);
bsw/jbe@1309 6031 text = currentChar + text;
bsw/jbe@1309 6032 } else {
bsw/jbe@1309 6033 chars.push(pos);
bsw/jbe@1309 6034 text += currentChar;
bsw/jbe@1309 6035 }
bsw/jbe@1309 6036
bsw/jbe@1309 6037 if (isRegex) {
bsw/jbe@1309 6038 result = searchTerm.exec(text);
bsw/jbe@1309 6039 if (result) {
bsw/jbe@1309 6040 matchStartIndex = result.index;
bsw/jbe@1309 6041 matchEndIndex = matchStartIndex + result[0].length;
bsw/jbe@1309 6042 if (insideRegexMatch) {
bsw/jbe@1309 6043 // Check whether the match is now over
bsw/jbe@1309 6044 if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
bsw/jbe@1309 6045 returnValue = handleMatch(matchStartIndex, matchEndIndex);
bsw/jbe@1309 6046 break;
bsw/jbe@1309 6047 }
bsw/jbe@1309 6048 } else {
bsw/jbe@1309 6049 insideRegexMatch = true;
bsw/jbe@1309 6050 }
bsw/jbe@1309 6051 }
bsw/jbe@1309 6052 } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
bsw/jbe@1309 6053 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
bsw/jbe@1309 6054 break;
bsw/jbe@1309 6055 }
bsw/jbe@1309 6056 }
bsw/jbe@1309 6057
bsw/jbe@1309 6058 // Check whether regex match extends to the end of the range
bsw/jbe@1309 6059 if (insideRegexMatch) {
bsw/jbe@1309 6060 returnValue = handleMatch(matchStartIndex, matchEndIndex);
bsw/jbe@1309 6061 }
bsw/jbe@1309 6062 it.dispose();
bsw/jbe@1309 6063
bsw/jbe@1309 6064 return returnValue;
bsw/jbe@1309 6065 }
bsw/jbe@1309 6066
bsw/jbe@1309 6067 function createEntryPointFunction(func) {
bsw/jbe@1309 6068 return function() {
bsw/jbe@1309 6069 var sessionRunning = !!currentSession;
bsw/jbe@1309 6070 var session = getSession();
bsw/jbe@1309 6071 var args = [session].concat( util.toArray(arguments) );
bsw/jbe@1309 6072 var returnValue = func.apply(this, args);
bsw/jbe@1309 6073 if (!sessionRunning) {
bsw/jbe@1309 6074 endSession();
bsw/jbe@1309 6075 }
bsw/jbe@1309 6076 return returnValue;
bsw/jbe@1309 6077 };
bsw/jbe@1309 6078 }
bsw/jbe@1309 6079
bsw/jbe@1309 6080 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 6081
bsw/jbe@1309 6082 // Extensions to the Rangy Range object
bsw/jbe@1309 6083
bsw/jbe@1309 6084 function createRangeBoundaryMover(isStart, collapse) {
bsw/jbe@1309 6085 /*
bsw/jbe@1309 6086 Unit can be "character" or "word"
bsw/jbe@1309 6087 Options:
bsw/jbe@1309 6088
bsw/jbe@1309 6089 - includeTrailingSpace
bsw/jbe@1309 6090 - wordRegex
bsw/jbe@1309 6091 - tokenizer
bsw/jbe@1309 6092 - collapseSpaceBeforeLineBreak
bsw/jbe@1309 6093 */
bsw/jbe@1309 6094 return createEntryPointFunction(
bsw/jbe@1309 6095 function(session, unit, count, moveOptions) {
bsw/jbe@1309 6096 if (typeof count == UNDEF) {
bsw/jbe@1309 6097 count = unit;
bsw/jbe@1309 6098 unit = CHARACTER;
bsw/jbe@1309 6099 }
bsw/jbe@1309 6100 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
bsw/jbe@1309 6101
bsw/jbe@1309 6102 var boundaryIsStart = isStart;
bsw/jbe@1309 6103 if (collapse) {
bsw/jbe@1309 6104 boundaryIsStart = (count >= 0);
bsw/jbe@1309 6105 this.collapse(!boundaryIsStart);
bsw/jbe@1309 6106 }
bsw/jbe@1309 6107 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
bsw/jbe@1309 6108 var newPos = moveResult.position;
bsw/jbe@1309 6109 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
bsw/jbe@1309 6110 return moveResult.unitsMoved;
bsw/jbe@1309 6111 }
bsw/jbe@1309 6112 );
bsw/jbe@1309 6113 }
bsw/jbe@1309 6114
bsw/jbe@1309 6115 function createRangeTrimmer(isStart) {
bsw/jbe@1309 6116 return createEntryPointFunction(
bsw/jbe@1309 6117 function(session, characterOptions) {
bsw/jbe@1309 6118 characterOptions = createOptions(characterOptions, defaultCharacterOptions);
bsw/jbe@1309 6119 var pos;
bsw/jbe@1309 6120 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
bsw/jbe@1309 6121 var trimCharCount = 0;
bsw/jbe@1309 6122 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
bsw/jbe@1309 6123 ++trimCharCount;
bsw/jbe@1309 6124 }
bsw/jbe@1309 6125 it.dispose();
bsw/jbe@1309 6126 var trimmed = (trimCharCount > 0);
bsw/jbe@1309 6127 if (trimmed) {
bsw/jbe@1309 6128 this[isStart ? "moveStart" : "moveEnd"](
bsw/jbe@1309 6129 "character",
bsw/jbe@1309 6130 isStart ? trimCharCount : -trimCharCount,
bsw/jbe@1309 6131 { characterOptions: characterOptions }
bsw/jbe@1309 6132 );
bsw/jbe@1309 6133 }
bsw/jbe@1309 6134 return trimmed;
bsw/jbe@1309 6135 }
bsw/jbe@1309 6136 );
bsw/jbe@1309 6137 }
bsw/jbe@1309 6138
bsw/jbe@1309 6139 extend(api.rangePrototype, {
bsw/jbe@1309 6140 moveStart: createRangeBoundaryMover(true, false),
bsw/jbe@1309 6141
bsw/jbe@1309 6142 moveEnd: createRangeBoundaryMover(false, false),
bsw/jbe@1309 6143
bsw/jbe@1309 6144 move: createRangeBoundaryMover(true, true),
bsw/jbe@1309 6145
bsw/jbe@1309 6146 trimStart: createRangeTrimmer(true),
bsw/jbe@1309 6147
bsw/jbe@1309 6148 trimEnd: createRangeTrimmer(false),
bsw/jbe@1309 6149
bsw/jbe@1309 6150 trim: createEntryPointFunction(
bsw/jbe@1309 6151 function(session, characterOptions) {
bsw/jbe@1309 6152 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
bsw/jbe@1309 6153 return startTrimmed || endTrimmed;
bsw/jbe@1309 6154 }
bsw/jbe@1309 6155 ),
bsw/jbe@1309 6156
bsw/jbe@1309 6157 expand: createEntryPointFunction(
bsw/jbe@1309 6158 function(session, unit, expandOptions) {
bsw/jbe@1309 6159 var moved = false;
bsw/jbe@1309 6160 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
bsw/jbe@1309 6161 var characterOptions = expandOptions.characterOptions;
bsw/jbe@1309 6162 if (!unit) {
bsw/jbe@1309 6163 unit = CHARACTER;
bsw/jbe@1309 6164 }
bsw/jbe@1309 6165 if (unit == WORD) {
bsw/jbe@1309 6166 var wordOptions = expandOptions.wordOptions;
bsw/jbe@1309 6167 var startPos = session.getRangeBoundaryPosition(this, true);
bsw/jbe@1309 6168 var endPos = session.getRangeBoundaryPosition(this, false);
bsw/jbe@1309 6169
bsw/jbe@1309 6170 var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
bsw/jbe@1309 6171 var startToken = startTokenizedTextProvider.nextEndToken();
bsw/jbe@1309 6172 var newStartPos = startToken.chars[0].previousVisible();
bsw/jbe@1309 6173 var endToken, newEndPos;
bsw/jbe@1309 6174
bsw/jbe@1309 6175 if (this.collapsed) {
bsw/jbe@1309 6176 endToken = startToken;
bsw/jbe@1309 6177 } else {
bsw/jbe@1309 6178 var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
bsw/jbe@1309 6179 endToken = endTokenizedTextProvider.previousStartToken();
bsw/jbe@1309 6180 }
bsw/jbe@1309 6181 newEndPos = endToken.chars[endToken.chars.length - 1];
bsw/jbe@1309 6182
bsw/jbe@1309 6183 if (!newStartPos.equals(startPos)) {
bsw/jbe@1309 6184 this.setStart(newStartPos.node, newStartPos.offset);
bsw/jbe@1309 6185 moved = true;
bsw/jbe@1309 6186 }
bsw/jbe@1309 6187 if (newEndPos && !newEndPos.equals(endPos)) {
bsw/jbe@1309 6188 this.setEnd(newEndPos.node, newEndPos.offset);
bsw/jbe@1309 6189 moved = true;
bsw/jbe@1309 6190 }
bsw/jbe@1309 6191
bsw/jbe@1309 6192 if (expandOptions.trim) {
bsw/jbe@1309 6193 if (expandOptions.trimStart) {
bsw/jbe@1309 6194 moved = this.trimStart(characterOptions) || moved;
bsw/jbe@1309 6195 }
bsw/jbe@1309 6196 if (expandOptions.trimEnd) {
bsw/jbe@1309 6197 moved = this.trimEnd(characterOptions) || moved;
bsw/jbe@1309 6198 }
bsw/jbe@1309 6199 }
bsw/jbe@1309 6200
bsw/jbe@1309 6201 return moved;
bsw/jbe@1309 6202 } else {
bsw/jbe@1309 6203 return this.moveEnd(CHARACTER, 1, expandOptions);
bsw/jbe@1309 6204 }
bsw/jbe@1309 6205 }
bsw/jbe@1309 6206 ),
bsw/jbe@1309 6207
bsw/jbe@1309 6208 text: createEntryPointFunction(
bsw/jbe@1309 6209 function(session, characterOptions) {
bsw/jbe@1309 6210 return this.collapsed ?
bsw/jbe@1309 6211 "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
bsw/jbe@1309 6212 }
bsw/jbe@1309 6213 ),
bsw/jbe@1309 6214
bsw/jbe@1309 6215 selectCharacters: createEntryPointFunction(
bsw/jbe@1309 6216 function(session, containerNode, startIndex, endIndex, characterOptions) {
bsw/jbe@1309 6217 var moveOptions = { characterOptions: characterOptions };
bsw/jbe@1309 6218 if (!containerNode) {
bsw/jbe@1309 6219 containerNode = getBody( this.getDocument() );
bsw/jbe@1309 6220 }
bsw/jbe@1309 6221 this.selectNodeContents(containerNode);
bsw/jbe@1309 6222 this.collapse(true);
bsw/jbe@1309 6223 this.moveStart("character", startIndex, moveOptions);
bsw/jbe@1309 6224 this.collapse(true);
bsw/jbe@1309 6225 this.moveEnd("character", endIndex - startIndex, moveOptions);
bsw/jbe@1309 6226 }
bsw/jbe@1309 6227 ),
bsw/jbe@1309 6228
bsw/jbe@1309 6229 // Character indexes are relative to the start of node
bsw/jbe@1309 6230 toCharacterRange: createEntryPointFunction(
bsw/jbe@1309 6231 function(session, containerNode, characterOptions) {
bsw/jbe@1309 6232 if (!containerNode) {
bsw/jbe@1309 6233 containerNode = getBody( this.getDocument() );
bsw/jbe@1309 6234 }
bsw/jbe@1309 6235 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
bsw/jbe@1309 6236 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
bsw/jbe@1309 6237 var rangeBetween = this.cloneRange();
bsw/jbe@1309 6238 var startIndex, endIndex;
bsw/jbe@1309 6239 if (rangeStartsBeforeNode) {
bsw/jbe@1309 6240 rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
bsw/jbe@1309 6241 startIndex = -rangeBetween.text(characterOptions).length;
bsw/jbe@1309 6242 } else {
bsw/jbe@1309 6243 rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
bsw/jbe@1309 6244 startIndex = rangeBetween.text(characterOptions).length;
bsw/jbe@1309 6245 }
bsw/jbe@1309 6246 endIndex = startIndex + this.text(characterOptions).length;
bsw/jbe@1309 6247
bsw/jbe@1309 6248 return {
bsw/jbe@1309 6249 start: startIndex,
bsw/jbe@1309 6250 end: endIndex
bsw/jbe@1309 6251 };
bsw/jbe@1309 6252 }
bsw/jbe@1309 6253 ),
bsw/jbe@1309 6254
bsw/jbe@1309 6255 findText: createEntryPointFunction(
bsw/jbe@1309 6256 function(session, searchTermParam, findOptions) {
bsw/jbe@1309 6257 // Set up options
bsw/jbe@1309 6258 findOptions = createNestedOptions(findOptions, defaultFindOptions);
bsw/jbe@1309 6259
bsw/jbe@1309 6260 // Create word options if we're matching whole words only
bsw/jbe@1309 6261 if (findOptions.wholeWordsOnly) {
bsw/jbe@1309 6262 // We don't ever want trailing spaces for search results
bsw/jbe@1309 6263 findOptions.wordOptions.includeTrailingSpace = false;
bsw/jbe@1309 6264 }
bsw/jbe@1309 6265
bsw/jbe@1309 6266 var backward = isDirectionBackward(findOptions.direction);
bsw/jbe@1309 6267
bsw/jbe@1309 6268 // Create a range representing the search scope if none was provided
bsw/jbe@1309 6269 var searchScopeRange = findOptions.withinRange;
bsw/jbe@1309 6270 if (!searchScopeRange) {
bsw/jbe@1309 6271 searchScopeRange = api.createRange();
bsw/jbe@1309 6272 searchScopeRange.selectNodeContents(this.getDocument());
bsw/jbe@1309 6273 }
bsw/jbe@1309 6274
bsw/jbe@1309 6275 // Examine and prepare the search term
bsw/jbe@1309 6276 var searchTerm = searchTermParam, isRegex = false;
bsw/jbe@1309 6277 if (typeof searchTerm == "string") {
bsw/jbe@1309 6278 if (!findOptions.caseSensitive) {
bsw/jbe@1309 6279 searchTerm = searchTerm.toLowerCase();
bsw/jbe@1309 6280 }
bsw/jbe@1309 6281 } else {
bsw/jbe@1309 6282 isRegex = true;
bsw/jbe@1309 6283 }
bsw/jbe@1309 6284
bsw/jbe@1309 6285 var initialPos = session.getRangeBoundaryPosition(this, !backward);
bsw/jbe@1309 6286
bsw/jbe@1309 6287 // Adjust initial position if it lies outside the search scope
bsw/jbe@1309 6288 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
bsw/jbe@1309 6289
bsw/jbe@1309 6290 if (comparison === -1) {
bsw/jbe@1309 6291 initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
bsw/jbe@1309 6292 } else if (comparison === 1) {
bsw/jbe@1309 6293 initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
bsw/jbe@1309 6294 }
bsw/jbe@1309 6295
bsw/jbe@1309 6296 var pos = initialPos;
bsw/jbe@1309 6297 var wrappedAround = false;
bsw/jbe@1309 6298
bsw/jbe@1309 6299 // Try to find a match and ignore invalid ones
bsw/jbe@1309 6300 var findResult;
bsw/jbe@1309 6301 while (true) {
bsw/jbe@1309 6302 findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
bsw/jbe@1309 6303
bsw/jbe@1309 6304 if (findResult) {
bsw/jbe@1309 6305 if (findResult.valid) {
bsw/jbe@1309 6306 this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
bsw/jbe@1309 6307 return true;
bsw/jbe@1309 6308 } else {
bsw/jbe@1309 6309 // We've found a match that is not a whole word, so we carry on searching from the point immediately
bsw/jbe@1309 6310 // after the match
bsw/jbe@1309 6311 pos = backward ? findResult.startPos : findResult.endPos;
bsw/jbe@1309 6312 }
bsw/jbe@1309 6313 } else if (findOptions.wrap && !wrappedAround) {
bsw/jbe@1309 6314 // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
bsw/jbe@1309 6315 searchScopeRange = searchScopeRange.cloneRange();
bsw/jbe@1309 6316 pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
bsw/jbe@1309 6317 searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
bsw/jbe@1309 6318 wrappedAround = true;
bsw/jbe@1309 6319 } else {
bsw/jbe@1309 6320 // Nothing found and we can't wrap around, so we're done
bsw/jbe@1309 6321 return false;
bsw/jbe@1309 6322 }
bsw/jbe@1309 6323 }
bsw/jbe@1309 6324 }
bsw/jbe@1309 6325 ),
bsw/jbe@1309 6326
bsw/jbe@1309 6327 pasteHtml: function(html) {
bsw/jbe@1309 6328 this.deleteContents();
bsw/jbe@1309 6329 if (html) {
bsw/jbe@1309 6330 var frag = this.createContextualFragment(html);
bsw/jbe@1309 6331 var lastChild = frag.lastChild;
bsw/jbe@1309 6332 this.insertNode(frag);
bsw/jbe@1309 6333 this.collapseAfter(lastChild);
bsw/jbe@1309 6334 }
bsw/jbe@1309 6335 }
bsw/jbe@1309 6336 });
bsw/jbe@1309 6337
bsw/jbe@1309 6338 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 6339
bsw/jbe@1309 6340 // Extensions to the Rangy Selection object
bsw/jbe@1309 6341
bsw/jbe@1309 6342 function createSelectionTrimmer(methodName) {
bsw/jbe@1309 6343 return createEntryPointFunction(
bsw/jbe@1309 6344 function(session, characterOptions) {
bsw/jbe@1309 6345 var trimmed = false;
bsw/jbe@1309 6346 this.changeEachRange(function(range) {
bsw/jbe@1309 6347 trimmed = range[methodName](characterOptions) || trimmed;
bsw/jbe@1309 6348 });
bsw/jbe@1309 6349 return trimmed;
bsw/jbe@1309 6350 }
bsw/jbe@1309 6351 );
bsw/jbe@1309 6352 }
bsw/jbe@1309 6353
bsw/jbe@1309 6354 extend(api.selectionPrototype, {
bsw/jbe@1309 6355 expand: createEntryPointFunction(
bsw/jbe@1309 6356 function(session, unit, expandOptions) {
bsw/jbe@1309 6357 this.changeEachRange(function(range) {
bsw/jbe@1309 6358 range.expand(unit, expandOptions);
bsw/jbe@1309 6359 });
bsw/jbe@1309 6360 }
bsw/jbe@1309 6361 ),
bsw/jbe@1309 6362
bsw/jbe@1309 6363 move: createEntryPointFunction(
bsw/jbe@1309 6364 function(session, unit, count, options) {
bsw/jbe@1309 6365 var unitsMoved = 0;
bsw/jbe@1309 6366 if (this.focusNode) {
bsw/jbe@1309 6367 this.collapse(this.focusNode, this.focusOffset);
bsw/jbe@1309 6368 var range = this.getRangeAt(0);
bsw/jbe@1309 6369 if (!options) {
bsw/jbe@1309 6370 options = {};
bsw/jbe@1309 6371 }
bsw/jbe@1309 6372 options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
bsw/jbe@1309 6373 unitsMoved = range.move(unit, count, options);
bsw/jbe@1309 6374 this.setSingleRange(range);
bsw/jbe@1309 6375 }
bsw/jbe@1309 6376 return unitsMoved;
bsw/jbe@1309 6377 }
bsw/jbe@1309 6378 ),
bsw/jbe@1309 6379
bsw/jbe@1309 6380 trimStart: createSelectionTrimmer("trimStart"),
bsw/jbe@1309 6381 trimEnd: createSelectionTrimmer("trimEnd"),
bsw/jbe@1309 6382 trim: createSelectionTrimmer("trim"),
bsw/jbe@1309 6383
bsw/jbe@1309 6384 selectCharacters: createEntryPointFunction(
bsw/jbe@1309 6385 function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
bsw/jbe@1309 6386 var range = api.createRange(containerNode);
bsw/jbe@1309 6387 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
bsw/jbe@1309 6388 this.setSingleRange(range, direction);
bsw/jbe@1309 6389 }
bsw/jbe@1309 6390 ),
bsw/jbe@1309 6391
bsw/jbe@1309 6392 saveCharacterRanges: createEntryPointFunction(
bsw/jbe@1309 6393 function(session, containerNode, characterOptions) {
bsw/jbe@1309 6394 var ranges = this.getAllRanges(), rangeCount = ranges.length;
bsw/jbe@1309 6395 var rangeInfos = [];
bsw/jbe@1309 6396
bsw/jbe@1309 6397 var backward = rangeCount == 1 && this.isBackward();
bsw/jbe@1309 6398
bsw/jbe@1309 6399 for (var i = 0, len = ranges.length; i < len; ++i) {
bsw/jbe@1309 6400 rangeInfos[i] = {
bsw/jbe@1309 6401 characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
bsw/jbe@1309 6402 backward: backward,
bsw/jbe@1309 6403 characterOptions: characterOptions
bsw/jbe@1309 6404 };
bsw/jbe@1309 6405 }
bsw/jbe@1309 6406
bsw/jbe@1309 6407 return rangeInfos;
bsw/jbe@1309 6408 }
bsw/jbe@1309 6409 ),
bsw/jbe@1309 6410
bsw/jbe@1309 6411 restoreCharacterRanges: createEntryPointFunction(
bsw/jbe@1309 6412 function(session, containerNode, saved) {
bsw/jbe@1309 6413 this.removeAllRanges();
bsw/jbe@1309 6414 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
bsw/jbe@1309 6415 rangeInfo = saved[i];
bsw/jbe@1309 6416 characterRange = rangeInfo.characterRange;
bsw/jbe@1309 6417 range = api.createRange(containerNode);
bsw/jbe@1309 6418 range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
bsw/jbe@1309 6419 this.addRange(range, rangeInfo.backward);
bsw/jbe@1309 6420 }
bsw/jbe@1309 6421 }
bsw/jbe@1309 6422 ),
bsw/jbe@1309 6423
bsw/jbe@1309 6424 text: createEntryPointFunction(
bsw/jbe@1309 6425 function(session, characterOptions) {
bsw/jbe@1309 6426 var rangeTexts = [];
bsw/jbe@1309 6427 for (var i = 0, len = this.rangeCount; i < len; ++i) {
bsw/jbe@1309 6428 rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
bsw/jbe@1309 6429 }
bsw/jbe@1309 6430 return rangeTexts.join("");
bsw/jbe@1309 6431 }
bsw/jbe@1309 6432 )
bsw/jbe@1309 6433 });
bsw/jbe@1309 6434
bsw/jbe@1309 6435 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 6436
bsw/jbe@1309 6437 // Extensions to the core rangy object
bsw/jbe@1309 6438
bsw/jbe@1309 6439 api.innerText = function(el, characterOptions) {
bsw/jbe@1309 6440 var range = api.createRange(el);
bsw/jbe@1309 6441 range.selectNodeContents(el);
bsw/jbe@1309 6442 var text = range.text(characterOptions);
bsw/jbe@1309 6443 return text;
bsw/jbe@1309 6444 };
bsw/jbe@1309 6445
bsw/jbe@1309 6446 api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
bsw/jbe@1309 6447 var session = getSession();
bsw/jbe@1309 6448 iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
bsw/jbe@1309 6449 var startPos = session.getPosition(startNode, startOffset);
bsw/jbe@1309 6450 var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
bsw/jbe@1309 6451 var backward = isDirectionBackward(iteratorOptions.direction);
bsw/jbe@1309 6452
bsw/jbe@1309 6453 return {
bsw/jbe@1309 6454 next: function() {
bsw/jbe@1309 6455 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
bsw/jbe@1309 6456 },
bsw/jbe@1309 6457
bsw/jbe@1309 6458 dispose: function() {
bsw/jbe@1309 6459 tokenizedTextProvider.dispose();
bsw/jbe@1309 6460 this.next = function() {};
bsw/jbe@1309 6461 }
bsw/jbe@1309 6462 };
bsw/jbe@1309 6463 };
bsw/jbe@1309 6464
bsw/jbe@1309 6465 /*----------------------------------------------------------------------------------------------------------------*/
bsw/jbe@1309 6466
bsw/jbe@1309 6467 api.noMutation = function(func) {
bsw/jbe@1309 6468 var session = getSession();
bsw/jbe@1309 6469 func(session);
bsw/jbe@1309 6470 endSession();
bsw/jbe@1309 6471 };
bsw/jbe@1309 6472
bsw/jbe@1309 6473 api.noMutation.createEntryPointFunction = createEntryPointFunction;
bsw/jbe@1309 6474
bsw/jbe@1309 6475 api.textRange = {
bsw/jbe@1309 6476 isBlockNode: isBlockNode,
bsw/jbe@1309 6477 isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
bsw/jbe@1309 6478
bsw/jbe@1309 6479 createPosition: createEntryPointFunction(
bsw/jbe@1309 6480 function(session, node, offset) {
bsw/jbe@1309 6481 return session.getPosition(node, offset);
bsw/jbe@1309 6482 }
bsw/jbe@1309 6483 )
bsw/jbe@1309 6484 };
bsw/jbe@1309 6485 });
bsw/jbe@1309 6486
bsw/jbe@1309 6487 /**
bsw/jbe@1309 6488 * Detect browser support for specific features
bsw/jbe@1309 6489 */
bsw/jbe@1309 6490 wysihtml.browser = (function() {
bsw/jbe@1309 6491 var userAgent = navigator.userAgent,
bsw/jbe@1309 6492 testElement = document.createElement("div"),
bsw/jbe@1309 6493 // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
bsw/jbe@1309 6494 // We need to be extra careful about Microsoft as it shows increasing tendency of tainting its userAgent strings with false feathers
bsw/jbe@1309 6495 isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1 && !isIE(),
bsw/jbe@1309 6496 isWebKit = userAgent.indexOf("AppleWebKit/") !== -1 && !isIE(),
bsw/jbe@1309 6497 isChrome = userAgent.indexOf("Chrome/") !== -1 && !isIE(),
bsw/jbe@1309 6498 isOpera = userAgent.indexOf("Opera/") !== -1 && !isIE();
bsw/jbe@1309 6499
bsw/jbe@1309 6500 function iosVersion(userAgent) {
bsw/jbe@1309 6501 return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
bsw/jbe@1309 6502 }
bsw/jbe@1309 6503
bsw/jbe@1309 6504 function androidVersion(userAgent) {
bsw/jbe@1309 6505 return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
bsw/jbe@1309 6506 }
bsw/jbe@1309 6507
bsw/jbe@1309 6508 function isIE(version, equation) {
bsw/jbe@1309 6509 var rv = -1,
bsw/jbe@1309 6510 re;
bsw/jbe@1309 6511
bsw/jbe@1309 6512 if (navigator.appName == 'Microsoft Internet Explorer') {
bsw/jbe@1309 6513 re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
bsw/jbe@1309 6514 } else if (navigator.appName == 'Netscape') {
bsw/jbe@1309 6515 if (navigator.userAgent.indexOf("Trident") > -1) {
bsw/jbe@1309 6516 re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
bsw/jbe@1309 6517 } else if ((/Edge\/(\d+)./i).test(navigator.userAgent)) {
bsw/jbe@1309 6518 re = /Edge\/(\d+)./i;
bsw/jbe@1309 6519 }
bsw/jbe@1309 6520 }
bsw/jbe@1309 6521
bsw/jbe@1309 6522 if (re && re.exec(navigator.userAgent) != null) {
bsw/jbe@1309 6523 rv = parseFloat(RegExp.$1);
bsw/jbe@1309 6524 }
bsw/jbe@1309 6525
bsw/jbe@1309 6526 if (rv === -1) { return false; }
bsw/jbe@1309 6527 if (!version) { return true; }
bsw/jbe@1309 6528 if (!equation) { return version === rv; }
bsw/jbe@1309 6529 if (equation === "<") { return version < rv; }
bsw/jbe@1309 6530 if (equation === ">") { return version > rv; }
bsw/jbe@1309 6531 if (equation === "<=") { return version <= rv; }
bsw/jbe@1309 6532 if (equation === ">=") { return version >= rv; }
bsw/jbe@1309 6533 }
bsw/jbe@1309 6534
bsw/jbe@1309 6535 return {
bsw/jbe@1309 6536 // Static variable needed, publicly accessible, to be able override it in unit tests
bsw/jbe@1309 6537 USER_AGENT: userAgent,
bsw/jbe@1309 6538
bsw/jbe@1309 6539 /**
bsw/jbe@1309 6540 * Exclude browsers that are not capable of displaying and handling
bsw/jbe@1309 6541 * contentEditable as desired:
bsw/jbe@1309 6542 * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
bsw/jbe@1309 6543 * - IE < 8 create invalid markup and crash randomly from time to time
bsw/jbe@1309 6544 *
bsw/jbe@1309 6545 * @return {Boolean}
bsw/jbe@1309 6546 */
bsw/jbe@1309 6547 supported: function() {
bsw/jbe@1309 6548 var userAgent = this.USER_AGENT.toLowerCase(),
bsw/jbe@1309 6549 // Essential for making html elements editable
bsw/jbe@1309 6550 hasContentEditableSupport = "contentEditable" in testElement,
bsw/jbe@1309 6551 // Following methods are needed in order to interact with the contentEditable area
bsw/jbe@1309 6552 hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,
bsw/jbe@1309 6553 // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
bsw/jbe@1309 6554 hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,
bsw/jbe@1309 6555 // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
bsw/jbe@1309 6556 isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
bsw/jbe@1309 6557 return hasContentEditableSupport
bsw/jbe@1309 6558 && hasEditingApiSupport
bsw/jbe@1309 6559 && hasQuerySelectorSupport
bsw/jbe@1309 6560 && !isIncompatibleMobileBrowser;
bsw/jbe@1309 6561 },
bsw/jbe@1309 6562
bsw/jbe@1309 6563 isTouchDevice: function() {
bsw/jbe@1309 6564 return this.supportsEvent("touchmove");
bsw/jbe@1309 6565 },
bsw/jbe@1309 6566
bsw/jbe@1309 6567 isIos: function() {
bsw/jbe@1309 6568 return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
bsw/jbe@1309 6569 },
bsw/jbe@1309 6570
bsw/jbe@1309 6571 isAndroid: function() {
bsw/jbe@1309 6572 return this.USER_AGENT.indexOf("Android") !== -1;
bsw/jbe@1309 6573 },
bsw/jbe@1309 6574
bsw/jbe@1309 6575 /**
bsw/jbe@1309 6576 * Whether the browser supports sandboxed iframes
bsw/jbe@1309 6577 * Currently only IE 6+ offers such feature <iframe security="restricted">
bsw/jbe@1309 6578 *
bsw/jbe@1309 6579 * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
bsw/jbe@1309 6580 * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
bsw/jbe@1309 6581 *
bsw/jbe@1309 6582 * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
bsw/jbe@1309 6583 */
bsw/jbe@1309 6584 supportsSandboxedIframes: function() {
bsw/jbe@1309 6585 return isIE();
bsw/jbe@1309 6586 },
bsw/jbe@1309 6587
bsw/jbe@1309 6588 /**
bsw/jbe@1309 6589 * IE6+7 throw a mixed content warning when the src of an iframe
bsw/jbe@1309 6590 * is empty/unset or about:blank
bsw/jbe@1309 6591 * window.querySelector is implemented as of IE8
bsw/jbe@1309 6592 */
bsw/jbe@1309 6593 throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
bsw/jbe@1309 6594 return !("querySelector" in document);
bsw/jbe@1309 6595 },
bsw/jbe@1309 6596
bsw/jbe@1309 6597 /**
bsw/jbe@1309 6598 * Whether the caret is correctly displayed in contentEditable elements
bsw/jbe@1309 6599 * Firefox sometimes shows a huge caret in the beginning after focusing
bsw/jbe@1309 6600 */
bsw/jbe@1309 6601 displaysCaretInEmptyContentEditableCorrectly: function() {
bsw/jbe@1309 6602 return isIE(12, ">");
bsw/jbe@1309 6603 },
bsw/jbe@1309 6604
bsw/jbe@1309 6605 /**
bsw/jbe@1309 6606 * Opera and IE are the only browsers who offer the css value
bsw/jbe@1309 6607 * in the original unit, thx to the currentStyle object
bsw/jbe@1309 6608 * All other browsers provide the computed style in px via window.getComputedStyle
bsw/jbe@1309 6609 */
bsw/jbe@1309 6610 hasCurrentStyleProperty: function() {
bsw/jbe@1309 6611 return "currentStyle" in testElement;
bsw/jbe@1309 6612 },
bsw/jbe@1309 6613
bsw/jbe@1309 6614 /**
bsw/jbe@1309 6615 * Whether the browser inserts a <br> when pressing enter in a contentEditable element
bsw/jbe@1309 6616 */
bsw/jbe@1309 6617 insertsLineBreaksOnReturn: function() {
bsw/jbe@1309 6618 return isGecko;
bsw/jbe@1309 6619 },
bsw/jbe@1309 6620
bsw/jbe@1309 6621 supportsPlaceholderAttributeOn: function(element) {
bsw/jbe@1309 6622 return "placeholder" in element;
bsw/jbe@1309 6623 },
bsw/jbe@1309 6624
bsw/jbe@1309 6625 supportsEvent: function(eventName) {
bsw/jbe@1309 6626 return "on" + eventName in testElement || (function() {
bsw/jbe@1309 6627 testElement.setAttribute("on" + eventName, "return;");
bsw/jbe@1309 6628 return typeof(testElement["on" + eventName]) === "function";
bsw/jbe@1309 6629 })();
bsw/jbe@1309 6630 },
bsw/jbe@1309 6631
bsw/jbe@1309 6632 /**
bsw/jbe@1309 6633 * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
bsw/jbe@1309 6634 */
bsw/jbe@1309 6635 supportsEventsInIframeCorrectly: function() {
bsw/jbe@1309 6636 return !isOpera;
bsw/jbe@1309 6637 },
bsw/jbe@1309 6638
bsw/jbe@1309 6639 /**
bsw/jbe@1309 6640 * Everything below IE9 doesn't know how to treat HTML5 tags
bsw/jbe@1309 6641 *
bsw/jbe@1309 6642 * @param {Object} context The document object on which to check HTML5 support
bsw/jbe@1309 6643 *
bsw/jbe@1309 6644 * @example
bsw/jbe@1309 6645 * wysihtml.browser.supportsHTML5Tags(document);
bsw/jbe@1309 6646 */
bsw/jbe@1309 6647 supportsHTML5Tags: function(context) {
bsw/jbe@1309 6648 var element = context.createElement("div"),
bsw/jbe@1309 6649 html5 = "<article>foo</article>";
bsw/jbe@1309 6650 element.innerHTML = html5;
bsw/jbe@1309 6651 return element.innerHTML.toLowerCase() === html5;
bsw/jbe@1309 6652 },
bsw/jbe@1309 6653
bsw/jbe@1309 6654 /**
bsw/jbe@1309 6655 * Checks whether a document supports a certain queryCommand
bsw/jbe@1309 6656 * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
bsw/jbe@1309 6657 * in oder to report correct results
bsw/jbe@1309 6658 *
bsw/jbe@1309 6659 * @param {Object} doc Document object on which to check for a query command
bsw/jbe@1309 6660 * @param {String} command The query command to check for
bsw/jbe@1309 6661 * @return {Boolean}
bsw/jbe@1309 6662 *
bsw/jbe@1309 6663 * @example
bsw/jbe@1309 6664 * wysihtml.browser.supportsCommand(document, "bold");
bsw/jbe@1309 6665 */
bsw/jbe@1309 6666 supportsCommand: (function() {
bsw/jbe@1309 6667 // Following commands are supported but contain bugs in some browsers
bsw/jbe@1309 6668 // TODO: investigate if some of these bugs can be tested without altering selection on page, instead of targeting browsers and versions directly
bsw/jbe@1309 6669 var buggyCommands = {
bsw/jbe@1309 6670 // formatBlock fails with some tags (eg. <blockquote>)
bsw/jbe@1309 6671 "formatBlock": isIE(10, "<="),
bsw/jbe@1309 6672 // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
bsw/jbe@1309 6673 // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
bsw/jbe@1309 6674 // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
bsw/jbe@1309 6675 "insertUnorderedList": isIE(),
bsw/jbe@1309 6676 "insertOrderedList": isIE()
bsw/jbe@1309 6677 };
bsw/jbe@1309 6678
bsw/jbe@1309 6679 // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
bsw/jbe@1309 6680 var supported = {
bsw/jbe@1309 6681 "insertHTML": isGecko
bsw/jbe@1309 6682 };
bsw/jbe@1309 6683
bsw/jbe@1309 6684 return function(doc, command) {
bsw/jbe@1309 6685 var isBuggy = buggyCommands[command];
bsw/jbe@1309 6686 if (!isBuggy) {
bsw/jbe@1309 6687 // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
bsw/jbe@1309 6688 try {
bsw/jbe@1309 6689 return doc.queryCommandSupported(command);
bsw/jbe@1309 6690 } catch(e1) {}
bsw/jbe@1309 6691
bsw/jbe@1309 6692 try {
bsw/jbe@1309 6693 return doc.queryCommandEnabled(command);
bsw/jbe@1309 6694 } catch(e2) {
bsw/jbe@1309 6695 return !!supported[command];
bsw/jbe@1309 6696 }
bsw/jbe@1309 6697 }
bsw/jbe@1309 6698 return false;
bsw/jbe@1309 6699 };
bsw/jbe@1309 6700 })(),
bsw/jbe@1309 6701
bsw/jbe@1309 6702 /**
bsw/jbe@1309 6703 * IE: URLs starting with:
bsw/jbe@1309 6704 * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
bsw/jbe@1309 6705 * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
bsw/jbe@1309 6706 * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
bsw/jbe@1309 6707 * space bar when the caret is directly after such an url.
bsw/jbe@1309 6708 * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
bsw/jbe@1309 6709 * (related blog post on msdn
bsw/jbe@1309 6710 * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
bsw/jbe@1309 6711 */
bsw/jbe@1309 6712 doesAutoLinkingInContentEditable: function() {
bsw/jbe@1309 6713 return isIE();
bsw/jbe@1309 6714 },
bsw/jbe@1309 6715
bsw/jbe@1309 6716 /**
bsw/jbe@1309 6717 * As stated above, IE auto links urls typed into contentEditable elements
bsw/jbe@1309 6718 * Since IE9 it's possible to prevent this behavior
bsw/jbe@1309 6719 */
bsw/jbe@1309 6720 canDisableAutoLinking: function() {
bsw/jbe@1309 6721 return this.supportsCommand(document, "AutoUrlDetect");
bsw/jbe@1309 6722 },
bsw/jbe@1309 6723
bsw/jbe@1309 6724 /**
bsw/jbe@1309 6725 * IE leaves an empty paragraph in the contentEditable element after clearing it
bsw/jbe@1309 6726 * Chrome/Safari sometimes an empty <div>
bsw/jbe@1309 6727 */
bsw/jbe@1309 6728 clearsContentEditableCorrectly: function() {
bsw/jbe@1309 6729 return isGecko || isOpera || isWebKit;
bsw/jbe@1309 6730 },
bsw/jbe@1309 6731
bsw/jbe@1309 6732 /**
bsw/jbe@1309 6733 * IE gives wrong results for getAttribute
bsw/jbe@1309 6734 */
bsw/jbe@1309 6735 supportsGetAttributeCorrectly: function() {
bsw/jbe@1309 6736 var td = document.createElement("td");
bsw/jbe@1309 6737 return td.getAttribute("rowspan") != "1";
bsw/jbe@1309 6738 },
bsw/jbe@1309 6739
bsw/jbe@1309 6740 /**
bsw/jbe@1309 6741 * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
bsw/jbe@1309 6742 * Chrome and Safari both don't support this
bsw/jbe@1309 6743 */
bsw/jbe@1309 6744 canSelectImagesInContentEditable: function() {
bsw/jbe@1309 6745 return isGecko || isIE() || isOpera;
bsw/jbe@1309 6746 },
bsw/jbe@1309 6747
bsw/jbe@1309 6748 /**
bsw/jbe@1309 6749 * All browsers except Safari and Chrome automatically scroll the range/caret position into view
bsw/jbe@1309 6750 */
bsw/jbe@1309 6751 autoScrollsToCaret: function() {
bsw/jbe@1309 6752 return !isWebKit;
bsw/jbe@1309 6753 },
bsw/jbe@1309 6754
bsw/jbe@1309 6755 /**
bsw/jbe@1309 6756 * Check whether the browser automatically closes tags that don't need to be opened
bsw/jbe@1309 6757 */
bsw/jbe@1309 6758 autoClosesUnclosedTags: function() {
bsw/jbe@1309 6759 var clonedTestElement = testElement.cloneNode(false),
bsw/jbe@1309 6760 returnValue,
bsw/jbe@1309 6761 innerHTML;
bsw/jbe@1309 6762
bsw/jbe@1309 6763 clonedTestElement.innerHTML = "<p><div></div>";
bsw/jbe@1309 6764 innerHTML = clonedTestElement.innerHTML.toLowerCase();
bsw/jbe@1309 6765 returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
bsw/jbe@1309 6766
bsw/jbe@1309 6767 // Cache result by overwriting current function
bsw/jbe@1309 6768 this.autoClosesUnclosedTags = function() { return returnValue; };
bsw/jbe@1309 6769
bsw/jbe@1309 6770 return returnValue;
bsw/jbe@1309 6771 },
bsw/jbe@1309 6772
bsw/jbe@1309 6773 /**
bsw/jbe@1309 6774 * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
bsw/jbe@1309 6775 */
bsw/jbe@1309 6776 supportsNativeGetElementsByClassName: function() {
bsw/jbe@1309 6777 return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
bsw/jbe@1309 6778 },
bsw/jbe@1309 6779
bsw/jbe@1309 6780 /**
bsw/jbe@1309 6781 * As of now (19.04.2011) only supported by Firefox 4 and Chrome
bsw/jbe@1309 6782 * See https://developer.mozilla.org/en/DOM/Selection/modify
bsw/jbe@1309 6783 */
bsw/jbe@1309 6784 supportsSelectionModify: function() {
bsw/jbe@1309 6785 return "getSelection" in window && "modify" in window.getSelection();
bsw/jbe@1309 6786 },
bsw/jbe@1309 6787
bsw/jbe@1309 6788 /**
bsw/jbe@1309 6789 * Opera needs a white space after a <br> in order to position the caret correctly
bsw/jbe@1309 6790 */
bsw/jbe@1309 6791 needsSpaceAfterLineBreak: function() {
bsw/jbe@1309 6792 return isOpera;
bsw/jbe@1309 6793 },
bsw/jbe@1309 6794
bsw/jbe@1309 6795 /**
bsw/jbe@1309 6796 * Whether the browser supports the speech api on the given element
bsw/jbe@1309 6797 * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
bsw/jbe@1309 6798 *
bsw/jbe@1309 6799 * @example
bsw/jbe@1309 6800 * var input = document.createElement("input");
bsw/jbe@1309 6801 * if (wysihtml.browser.supportsSpeechApiOn(input)) {
bsw/jbe@1309 6802 * // ...
bsw/jbe@1309 6803 * }
bsw/jbe@1309 6804 */
bsw/jbe@1309 6805 supportsSpeechApiOn: function(input) {
bsw/jbe@1309 6806 var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0];
bsw/jbe@1309 6807 return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
bsw/jbe@1309 6808 },
bsw/jbe@1309 6809
bsw/jbe@1309 6810 /**
bsw/jbe@1309 6811 * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
bsw/jbe@1309 6812 * See https://connect.microsoft.com/ie/feedback/details/650112
bsw/jbe@1309 6813 * or try the POC http://tifftiff.de/ie9_crash/
bsw/jbe@1309 6814 */
bsw/jbe@1309 6815 crashesWhenDefineProperty: function(property) {
bsw/jbe@1309 6816 return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest");
bsw/jbe@1309 6817 },
bsw/jbe@1309 6818
bsw/jbe@1309 6819 /**
bsw/jbe@1309 6820 * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
bsw/jbe@1309 6821 */
bsw/jbe@1309 6822 doesAsyncFocus: function() {
bsw/jbe@1309 6823 return isIE(12, ">");
bsw/jbe@1309 6824 },
bsw/jbe@1309 6825
bsw/jbe@1309 6826 /**
bsw/jbe@1309 6827 * 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
bsw/jbe@1309 6828 */
bsw/jbe@1309 6829 hasProblemsSettingCaretAfterImg: function() {
bsw/jbe@1309 6830 return isIE();
bsw/jbe@1309 6831 },
bsw/jbe@1309 6832
bsw/jbe@1309 6833 /* In IE when deleting with caret at the begining of LI, List get broken into half instead of merging the LI with previous */
bsw/jbe@1309 6834 hasLiDeletingProblem: function() {
bsw/jbe@1309 6835 return isIE();
bsw/jbe@1309 6836 },
bsw/jbe@1309 6837
bsw/jbe@1309 6838 hasUndoInContextMenu: function() {
bsw/jbe@1309 6839 return isGecko || isChrome || isOpera;
bsw/jbe@1309 6840 },
bsw/jbe@1309 6841
bsw/jbe@1309 6842 /**
bsw/jbe@1309 6843 * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode)
bsw/jbe@1309 6844 * is used (regardless if rangy or native)
bsw/jbe@1309 6845 * This especially happens when the caret is positioned right after a <br> because then
bsw/jbe@1309 6846 * insertNode() will insert the node right before the <br>
bsw/jbe@1309 6847 */
bsw/jbe@1309 6848 hasInsertNodeIssue: function() {
bsw/jbe@1309 6849 return isOpera;
bsw/jbe@1309 6850 },
bsw/jbe@1309 6851
bsw/jbe@1309 6852 /**
bsw/jbe@1309 6853 * 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>)
bsw/jbe@1309 6854 */
bsw/jbe@1309 6855 hasIframeFocusIssue: function() {
bsw/jbe@1309 6856 return isIE();
bsw/jbe@1309 6857 },
bsw/jbe@1309 6858
bsw/jbe@1309 6859 /**
bsw/jbe@1309 6860 * Chrome + Safari create invalid nested markup after paste
bsw/jbe@1309 6861 *
bsw/jbe@1309 6862 * <p>
bsw/jbe@1309 6863 * foo
bsw/jbe@1309 6864 * <p>bar</p> <!-- BOO! -->
bsw/jbe@1309 6865 * </p>
bsw/jbe@1309 6866 */
bsw/jbe@1309 6867 createsNestedInvalidMarkupAfterPaste: function() {
bsw/jbe@1309 6868 return isWebKit;
bsw/jbe@1309 6869 },
bsw/jbe@1309 6870
bsw/jbe@1309 6871 // 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
bsw/jbe@1309 6872 // when startContainer is element.
bsw/jbe@1309 6873 hasCaretBlockElementIssue: function() {
bsw/jbe@1309 6874 return isWebKit;
bsw/jbe@1309 6875 },
bsw/jbe@1309 6876
bsw/jbe@1309 6877 supportsMutationEvents: function() {
bsw/jbe@1309 6878 return ("MutationEvent" in window);
bsw/jbe@1309 6879 },
bsw/jbe@1309 6880
bsw/jbe@1309 6881 /**
bsw/jbe@1309 6882 IE (at least up to 11) does not support clipboardData on event.
bsw/jbe@1309 6883 It is on window but cannot return text/html
bsw/jbe@1309 6884 Should actually check for clipboardData on paste event, but cannot in firefox
bsw/jbe@1309 6885 */
bsw/jbe@1309 6886 supportsModernPaste: function () {
bsw/jbe@1309 6887 return !isIE();
bsw/jbe@1309 6888 },
bsw/jbe@1309 6889
bsw/jbe@1309 6890 // Unifies the property names of element.style by returning the suitable property name for current browser
bsw/jbe@1309 6891 // Input property key must be the standard
bsw/jbe@1309 6892 fixStyleKey: function(key) {
bsw/jbe@1309 6893 if (key === "cssFloat") {
bsw/jbe@1309 6894 return ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat";
bsw/jbe@1309 6895 }
bsw/jbe@1309 6896 return key;
bsw/jbe@1309 6897 },
bsw/jbe@1309 6898
bsw/jbe@1309 6899 usesControlRanges: function() {
bsw/jbe@1309 6900 return document.body && "createControlRange" in document.body;
bsw/jbe@1309 6901 },
bsw/jbe@1309 6902
bsw/jbe@1309 6903 // Webkit browsers have an issue that when caret is at the end of link it is moved outside of link while inserting new characters,
bsw/jbe@1309 6904 // so all inserted content will be after link. Selection before inserion is reported to be in link though.
bsw/jbe@1309 6905 // This makes changing link texts from problematic to impossible (if link is just 1 characer long) for the user.
bsw/jbe@1309 6906 // TODO: needs to be tested better than just browser as it some day might get fixed
bsw/jbe@1309 6907 hasCaretAtLinkEndInsertionProblems: function() {
bsw/jbe@1309 6908 return isWebKit;
bsw/jbe@1309 6909 }
bsw/jbe@1309 6910 };
bsw/jbe@1309 6911 })();
bsw/jbe@1309 6912
bsw/jbe@1309 6913 wysihtml.lang.array = function(arr) {
bsw/jbe@1309 6914 return {
bsw/jbe@1309 6915 /**
bsw/jbe@1309 6916 * Check whether a given object exists in an array
bsw/jbe@1309 6917 *
bsw/jbe@1309 6918 * @example
bsw/jbe@1309 6919 * wysihtml.lang.array([1, 2]).contains(1);
bsw/jbe@1309 6920 * // => true
bsw/jbe@1309 6921 *
bsw/jbe@1309 6922 * Can be used to match array with array. If intersection is found true is returned
bsw/jbe@1309 6923 */
bsw/jbe@1309 6924 contains: function(needle) {
bsw/jbe@1309 6925 if (Array.isArray(needle)) {
bsw/jbe@1309 6926 for (var i = needle.length; i--;) {
bsw/jbe@1309 6927 if (wysihtml.lang.array(arr).indexOf(needle[i]) !== -1) {
bsw/jbe@1309 6928 return true;
bsw/jbe@1309 6929 }
bsw/jbe@1309 6930 }
bsw/jbe@1309 6931 return false;
bsw/jbe@1309 6932 } else {
bsw/jbe@1309 6933 return wysihtml.lang.array(arr).indexOf(needle) !== -1;
bsw/jbe@1309 6934 }
bsw/jbe@1309 6935 },
bsw/jbe@1309 6936
bsw/jbe@1309 6937 /**
bsw/jbe@1309 6938 * Check whether a given object exists in an array and return index
bsw/jbe@1309 6939 * If no elelemt found returns -1
bsw/jbe@1309 6940 *
bsw/jbe@1309 6941 * @example
bsw/jbe@1309 6942 * wysihtml.lang.array([1, 2]).indexOf(2);
bsw/jbe@1309 6943 * // => 1
bsw/jbe@1309 6944 */
bsw/jbe@1309 6945 indexOf: function(needle) {
bsw/jbe@1309 6946 if (arr.indexOf) {
bsw/jbe@1309 6947 return arr.indexOf(needle);
bsw/jbe@1309 6948 } else {
bsw/jbe@1309 6949 for (var i=0, length=arr.length; i<length; i++) {
bsw/jbe@1309 6950 if (arr[i] === needle) { return i; }
bsw/jbe@1309 6951 }
bsw/jbe@1309 6952 return -1;
bsw/jbe@1309 6953 }
bsw/jbe@1309 6954 },
bsw/jbe@1309 6955
bsw/jbe@1309 6956 /**
bsw/jbe@1309 6957 * Substract one array from another
bsw/jbe@1309 6958 *
bsw/jbe@1309 6959 * @example
bsw/jbe@1309 6960 * wysihtml.lang.array([1, 2, 3, 4]).without([3, 4]);
bsw/jbe@1309 6961 * // => [1, 2]
bsw/jbe@1309 6962 */
bsw/jbe@1309 6963 without: function(arrayToSubstract) {
bsw/jbe@1309 6964 arrayToSubstract = wysihtml.lang.array(arrayToSubstract);
bsw/jbe@1309 6965 var newArr = [],
bsw/jbe@1309 6966 i = 0,
bsw/jbe@1309 6967 length = arr.length;
bsw/jbe@1309 6968 for (; i<length; i++) {
bsw/jbe@1309 6969 if (!arrayToSubstract.contains(arr[i])) {
bsw/jbe@1309 6970 newArr.push(arr[i]);
bsw/jbe@1309 6971 }
bsw/jbe@1309 6972 }
bsw/jbe@1309 6973 return newArr;
bsw/jbe@1309 6974 },
bsw/jbe@1309 6975
bsw/jbe@1309 6976 /**
bsw/jbe@1309 6977 * Return a clean native array
bsw/jbe@1309 6978 *
bsw/jbe@1309 6979 * Following will convert a Live NodeList to a proper Array
bsw/jbe@1309 6980 * @example
bsw/jbe@1309 6981 * var childNodes = wysihtml.lang.array(document.body.childNodes).get();
bsw/jbe@1309 6982 */
bsw/jbe@1309 6983 get: function() {
bsw/jbe@1309 6984 var i = 0,
bsw/jbe@1309 6985 length = arr.length,
bsw/jbe@1309 6986 newArray = [];
bsw/jbe@1309 6987 for (; i<length; i++) {
bsw/jbe@1309 6988 newArray.push(arr[i]);
bsw/jbe@1309 6989 }
bsw/jbe@1309 6990 return newArray;
bsw/jbe@1309 6991 },
bsw/jbe@1309 6992
bsw/jbe@1309 6993 /**
bsw/jbe@1309 6994 * Creates a new array with the results of calling a provided function on every element in this array.
bsw/jbe@1309 6995 * optionally this can be provided as second argument
bsw/jbe@1309 6996 *
bsw/jbe@1309 6997 * @example
bsw/jbe@1309 6998 * var childNodes = wysihtml.lang.array([1,2,3,4]).map(function (value, index, array) {
bsw/jbe@1309 6999 return value * 2;
bsw/jbe@1309 7000 * });
bsw/jbe@1309 7001 * // => [2,4,6,8]
bsw/jbe@1309 7002 */
bsw/jbe@1309 7003 map: function(callback, thisArg) {
bsw/jbe@1309 7004 if (Array.prototype.map) {
bsw/jbe@1309 7005 return arr.map(callback, thisArg);
bsw/jbe@1309 7006 } else {
bsw/jbe@1309 7007 var len = arr.length >>> 0,
bsw/jbe@1309 7008 A = new Array(len),
bsw/jbe@1309 7009 i = 0;
bsw/jbe@1309 7010 for (; i < len; i++) {
bsw/jbe@1309 7011 A[i] = callback.call(thisArg, arr[i], i, arr);
bsw/jbe@1309 7012 }
bsw/jbe@1309 7013 return A;
bsw/jbe@1309 7014 }
bsw/jbe@1309 7015 },
bsw/jbe@1309 7016
bsw/jbe@1309 7017 /* ReturnS new array without duplicate entries
bsw/jbe@1309 7018 *
bsw/jbe@1309 7019 * @example
bsw/jbe@1309 7020 * var uniq = wysihtml.lang.array([1,2,3,2,1,4]).unique();
bsw/jbe@1309 7021 * // => [1,2,3,4]
bsw/jbe@1309 7022 */
bsw/jbe@1309 7023 unique: function() {
bsw/jbe@1309 7024 var vals = [],
bsw/jbe@1309 7025 max = arr.length,
bsw/jbe@1309 7026 idx = 0;
bsw/jbe@1309 7027
bsw/jbe@1309 7028 while (idx < max) {
bsw/jbe@1309 7029 if (!wysihtml.lang.array(vals).contains(arr[idx])) {
bsw/jbe@1309 7030 vals.push(arr[idx]);
bsw/jbe@1309 7031 }
bsw/jbe@1309 7032 idx++;
bsw/jbe@1309 7033 }
bsw/jbe@1309 7034 return vals;
bsw/jbe@1309 7035 }
bsw/jbe@1309 7036
bsw/jbe@1309 7037 };
bsw/jbe@1309 7038 };
bsw/jbe@1309 7039
bsw/jbe@1309 7040 wysihtml.lang.Dispatcher = Base.extend(
bsw/jbe@1309 7041 /** @scope wysihtml.lang.Dialog.prototype */ {
bsw/jbe@1309 7042 on: function(eventName, handler) {
bsw/jbe@1309 7043 this.events = this.events || {};
bsw/jbe@1309 7044 this.events[eventName] = this.events[eventName] || [];
bsw/jbe@1309 7045 this.events[eventName].push(handler);
bsw/jbe@1309 7046 return this;
bsw/jbe@1309 7047 },
bsw/jbe@1309 7048
bsw/jbe@1309 7049 off: function(eventName, handler) {
bsw/jbe@1309 7050 this.events = this.events || {};
bsw/jbe@1309 7051 var i = 0,
bsw/jbe@1309 7052 handlers,
bsw/jbe@1309 7053 newHandlers;
bsw/jbe@1309 7054 if (eventName) {
bsw/jbe@1309 7055 handlers = this.events[eventName] || [],
bsw/jbe@1309 7056 newHandlers = [];
bsw/jbe@1309 7057 for (; i<handlers.length; i++) {
bsw/jbe@1309 7058 if (handlers[i] !== handler && handler) {
bsw/jbe@1309 7059 newHandlers.push(handlers[i]);
bsw/jbe@1309 7060 }
bsw/jbe@1309 7061 }
bsw/jbe@1309 7062 this.events[eventName] = newHandlers;
bsw/jbe@1309 7063 } else {
bsw/jbe@1309 7064 // Clean up all events
bsw/jbe@1309 7065 this.events = {};
bsw/jbe@1309 7066 }
bsw/jbe@1309 7067 return this;
bsw/jbe@1309 7068 },
bsw/jbe@1309 7069
bsw/jbe@1309 7070 fire: function(eventName, payload) {
bsw/jbe@1309 7071 this.events = this.events || {};
bsw/jbe@1309 7072 var handlers = this.events[eventName] || [],
bsw/jbe@1309 7073 i = 0;
bsw/jbe@1309 7074 for (; i<handlers.length; i++) {
bsw/jbe@1309 7075 handlers[i].call(this, payload);
bsw/jbe@1309 7076 }
bsw/jbe@1309 7077 return this;
bsw/jbe@1309 7078 },
bsw/jbe@1309 7079
bsw/jbe@1309 7080 // deprecated, use .on()
bsw/jbe@1309 7081 observe: function() {
bsw/jbe@1309 7082 return this.on.apply(this, arguments);
bsw/jbe@1309 7083 },
bsw/jbe@1309 7084
bsw/jbe@1309 7085 // deprecated, use .off()
bsw/jbe@1309 7086 stopObserving: function() {
bsw/jbe@1309 7087 return this.off.apply(this, arguments);
bsw/jbe@1309 7088 }
bsw/jbe@1309 7089 });
bsw/jbe@1309 7090
bsw/jbe@1309 7091 wysihtml.lang.object = function(obj) {
bsw/jbe@1309 7092 return {
bsw/jbe@1309 7093 /**
bsw/jbe@1309 7094 * @example
bsw/jbe@1309 7095 * wysihtml.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
bsw/jbe@1309 7096 * // => { foo: 1, bar: 2, baz: 3 }
bsw/jbe@1309 7097 */
bsw/jbe@1309 7098 merge: function(otherObj, deep) {
bsw/jbe@1309 7099 for (var i in otherObj) {
bsw/jbe@1309 7100 if (deep && wysihtml.lang.object(otherObj[i]).isPlainObject() && (typeof obj[i] === "undefined" || wysihtml.lang.object(obj[i]).isPlainObject())) {
bsw/jbe@1309 7101 if (typeof obj[i] === "undefined") {
bsw/jbe@1309 7102 obj[i] = wysihtml.lang.object(otherObj[i]).clone(true);
bsw/jbe@1309 7103 } else {
bsw/jbe@1309 7104 wysihtml.lang.object(obj[i]).merge(wysihtml.lang.object(otherObj[i]).clone(true));
bsw/jbe@1309 7105 }
bsw/jbe@1309 7106 } else {
bsw/jbe@1309 7107 obj[i] = wysihtml.lang.object(otherObj[i]).isPlainObject() ? wysihtml.lang.object(otherObj[i]).clone(true) : otherObj[i];
bsw/jbe@1309 7108 }
bsw/jbe@1309 7109 }
bsw/jbe@1309 7110 return this;
bsw/jbe@1309 7111 },
bsw/jbe@1309 7112
bsw/jbe@1309 7113 difference: function (otherObj) {
bsw/jbe@1309 7114 var diffObj = {};
bsw/jbe@1309 7115
bsw/jbe@1309 7116 // Get old values not in comparing object
bsw/jbe@1309 7117 for (var i in obj) {
bsw/jbe@1309 7118 if (obj.hasOwnProperty(i)) {
bsw/jbe@1309 7119 if (!otherObj.hasOwnProperty(i)) {
bsw/jbe@1309 7120 diffObj[i] = obj[i];
bsw/jbe@1309 7121 }
bsw/jbe@1309 7122 }
bsw/jbe@1309 7123 }
bsw/jbe@1309 7124
bsw/jbe@1309 7125 // Get new and different values in comparing object
bsw/jbe@1309 7126 for (var o in otherObj) {
bsw/jbe@1309 7127 if (otherObj.hasOwnProperty(o)) {
bsw/jbe@1309 7128 if (!obj.hasOwnProperty(o) || obj[o] !== otherObj[o]) {
bsw/jbe@1309 7129 diffObj[0] = obj[0];
bsw/jbe@1309 7130 }
bsw/jbe@1309 7131 }
bsw/jbe@1309 7132 }
bsw/jbe@1309 7133 return diffObj;
bsw/jbe@1309 7134 },
bsw/jbe@1309 7135
bsw/jbe@1309 7136 get: function() {
bsw/jbe@1309 7137 return obj;
bsw/jbe@1309 7138 },
bsw/jbe@1309 7139
bsw/jbe@1309 7140 /**
bsw/jbe@1309 7141 * @example
bsw/jbe@1309 7142 * wysihtml.lang.object({ foo: 1 }).clone();
bsw/jbe@1309 7143 * // => { foo: 1 }
bsw/jbe@1309 7144 *
bsw/jbe@1309 7145 * v0.4.14 adds options for deep clone : wysihtml.lang.object({ foo: 1 }).clone(true);
bsw/jbe@1309 7146 */
bsw/jbe@1309 7147 clone: function(deep) {
bsw/jbe@1309 7148 var newObj = {},
bsw/jbe@1309 7149 i;
bsw/jbe@1309 7150
bsw/jbe@1309 7151 if (obj === null || !wysihtml.lang.object(obj).isPlainObject()) {
bsw/jbe@1309 7152 return obj;
bsw/jbe@1309 7153 }
bsw/jbe@1309 7154
bsw/jbe@1309 7155 for (i in obj) {
bsw/jbe@1309 7156 if(obj.hasOwnProperty(i)) {
bsw/jbe@1309 7157 if (deep) {
bsw/jbe@1309 7158 newObj[i] = wysihtml.lang.object(obj[i]).clone(deep);
bsw/jbe@1309 7159 } else {
bsw/jbe@1309 7160 newObj[i] = obj[i];
bsw/jbe@1309 7161 }
bsw/jbe@1309 7162 }
bsw/jbe@1309 7163 }
bsw/jbe@1309 7164 return newObj;
bsw/jbe@1309 7165 },
bsw/jbe@1309 7166
bsw/jbe@1309 7167 /**
bsw/jbe@1309 7168 * @example
bsw/jbe@1309 7169 * wysihtml.lang.object([]).isArray();
bsw/jbe@1309 7170 * // => true
bsw/jbe@1309 7171 */
bsw/jbe@1309 7172 isArray: function() {
bsw/jbe@1309 7173 return Object.prototype.toString.call(obj) === "[object Array]";
bsw/jbe@1309 7174 },
bsw/jbe@1309 7175
bsw/jbe@1309 7176 /**
bsw/jbe@1309 7177 * @example
bsw/jbe@1309 7178 * wysihtml.lang.object(function() {}).isFunction();
bsw/jbe@1309 7179 * // => true
bsw/jbe@1309 7180 */
bsw/jbe@1309 7181 isFunction: function() {
bsw/jbe@1309 7182 return Object.prototype.toString.call(obj) === '[object Function]';
bsw/jbe@1309 7183 },
bsw/jbe@1309 7184
bsw/jbe@1309 7185 isPlainObject: function () {
bsw/jbe@1309 7186 return obj && Object.prototype.toString.call(obj) === '[object Object]' && !(("Node" in window) ? obj instanceof Node : obj instanceof Element || obj instanceof Text);
bsw/jbe@1309 7187 },
bsw/jbe@1309 7188
bsw/jbe@1309 7189 /**
bsw/jbe@1309 7190 * @example
bsw/jbe@1309 7191 * wysihtml.lang.object({}).isEmpty();
bsw/jbe@1309 7192 * // => true
bsw/jbe@1309 7193 */
bsw/jbe@1309 7194 isEmpty: function() {
bsw/jbe@1309 7195 for (var i in obj) {
bsw/jbe@1309 7196 if (obj.hasOwnProperty(i)) {
bsw/jbe@1309 7197 return false;
bsw/jbe@1309 7198 }
bsw/jbe@1309 7199 }
bsw/jbe@1309 7200 return true;
bsw/jbe@1309 7201 }
bsw/jbe@1309 7202 };
bsw/jbe@1309 7203 };
bsw/jbe@1309 7204
bsw/jbe@1309 7205 (function() {
bsw/jbe@1309 7206 var WHITE_SPACE_START = /^\s+/,
bsw/jbe@1309 7207 WHITE_SPACE_END = /\s+$/,
bsw/jbe@1309 7208 ENTITY_REG_EXP = /[&<>\t"]/g,
bsw/jbe@1309 7209 ENTITY_MAP = {
bsw/jbe@1309 7210 '&': '&amp;',
bsw/jbe@1309 7211 '<': '&lt;',
bsw/jbe@1309 7212 '>': '&gt;',
bsw/jbe@1309 7213 '"': "&quot;",
bsw/jbe@1309 7214 '\t':"&nbsp; "
bsw/jbe@1309 7215 };
bsw/jbe@1309 7216 wysihtml.lang.string = function(str) {
bsw/jbe@1309 7217 str = String(str);
bsw/jbe@1309 7218 return {
bsw/jbe@1309 7219 /**
bsw/jbe@1309 7220 * @example
bsw/jbe@1309 7221 * wysihtml.lang.string(" foo ").trim();
bsw/jbe@1309 7222 * // => "foo"
bsw/jbe@1309 7223 */
bsw/jbe@1309 7224 trim: function() {
bsw/jbe@1309 7225 return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
bsw/jbe@1309 7226 },
bsw/jbe@1309 7227
bsw/jbe@1309 7228 /**
bsw/jbe@1309 7229 * @example
bsw/jbe@1309 7230 * wysihtml.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
bsw/jbe@1309 7231 * // => "Hello Christopher"
bsw/jbe@1309 7232 */
bsw/jbe@1309 7233 interpolate: function(vars) {
bsw/jbe@1309 7234 for (var i in vars) {
bsw/jbe@1309 7235 str = this.replace("#{" + i + "}").by(vars[i]);
bsw/jbe@1309 7236 }
bsw/jbe@1309 7237 return str;
bsw/jbe@1309 7238 },
bsw/jbe@1309 7239
bsw/jbe@1309 7240 /**
bsw/jbe@1309 7241 * @example
bsw/jbe@1309 7242 * wysihtml.lang.string("Hello Tom").replace("Tom").with("Hans");
bsw/jbe@1309 7243 * // => "Hello Hans"
bsw/jbe@1309 7244 */
bsw/jbe@1309 7245 replace: function(search) {
bsw/jbe@1309 7246 return {
bsw/jbe@1309 7247 by: function(replace) {
bsw/jbe@1309 7248 return str.split(search).join(replace);
bsw/jbe@1309 7249 }
bsw/jbe@1309 7250 };
bsw/jbe@1309 7251 },
bsw/jbe@1309 7252
bsw/jbe@1309 7253 /**
bsw/jbe@1309 7254 * @example
bsw/jbe@1309 7255 * wysihtml.lang.string("hello<br>").escapeHTML();
bsw/jbe@1309 7256 * // => "hello&lt;br&gt;"
bsw/jbe@1309 7257 */
bsw/jbe@1309 7258 escapeHTML: function(linebreaks, convertSpaces) {
bsw/jbe@1309 7259 var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; });
bsw/jbe@1309 7260 if (linebreaks) {
bsw/jbe@1309 7261 html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
bsw/jbe@1309 7262 }
bsw/jbe@1309 7263 if (convertSpaces) {
bsw/jbe@1309 7264 html = html.replace(/ /gi, "&nbsp; ");
bsw/jbe@1309 7265 }
bsw/jbe@1309 7266 return html;
bsw/jbe@1309 7267 }
bsw/jbe@1309 7268 };
bsw/jbe@1309 7269 };
bsw/jbe@1309 7270 })();
bsw/jbe@1309 7271
bsw/jbe@1309 7272 /**
bsw/jbe@1309 7273 * Find urls in descendant text nodes of an element and auto-links them
bsw/jbe@1309 7274 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
bsw/jbe@1309 7275 *
bsw/jbe@1309 7276 * @param {Element} element Container element in which to search for urls
bsw/jbe@1309 7277 *
bsw/jbe@1309 7278 * @example
bsw/jbe@1309 7279 * <div id="text-container">Please click here: www.google.com</div>
bsw/jbe@1309 7280 * <script>wysihtml.dom.autoLink(document.getElementById("text-container"));</script>
bsw/jbe@1309 7281 */
bsw/jbe@1309 7282 (function(wysihtml) {
bsw/jbe@1309 7283 var /**
bsw/jbe@1309 7284 * Don't auto-link urls that are contained in the following elements:
bsw/jbe@1309 7285 */
bsw/jbe@1309 7286 IGNORE_URLS_IN = wysihtml.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
bsw/jbe@1309 7287 /**
bsw/jbe@1309 7288 * revision 1:
bsw/jbe@1309 7289 * /(\S+\.{1}[^\s\,\.\!]+)/g
bsw/jbe@1309 7290 *
bsw/jbe@1309 7291 * revision 2:
bsw/jbe@1309 7292 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
bsw/jbe@1309 7293 *
bsw/jbe@1309 7294 * put this in the beginning if you don't wan't to match within a word
bsw/jbe@1309 7295 * (^|[\>\(\{\[\s\>])
bsw/jbe@1309 7296 */
bsw/jbe@1309 7297 URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
bsw/jbe@1309 7298 TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
bsw/jbe@1309 7299 MAX_DISPLAY_LENGTH = 100,
bsw/jbe@1309 7300 BRACKETS = { ")": "(", "]": "[", "}": "{" };
bsw/jbe@1309 7301
bsw/jbe@1309 7302 function autoLink(element, ignoreInClasses) {
bsw/jbe@1309 7303 if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
bsw/jbe@1309 7304 return element;
bsw/jbe@1309 7305 }
bsw/jbe@1309 7306
bsw/jbe@1309 7307 if (element === element.ownerDocument.documentElement) {
bsw/jbe@1309 7308 element = element.ownerDocument.body;
bsw/jbe@1309 7309 }
bsw/jbe@1309 7310
bsw/jbe@1309 7311 return _parseNode(element, ignoreInClasses);
bsw/jbe@1309 7312 }
bsw/jbe@1309 7313
bsw/jbe@1309 7314 /**
bsw/jbe@1309 7315 * This is basically a rebuild of
bsw/jbe@1309 7316 * the rails auto_link_urls text helper
bsw/jbe@1309 7317 */
bsw/jbe@1309 7318 function _convertUrlsToLinks(str) {
bsw/jbe@1309 7319 return str.replace(URL_REG_EXP, function(match, url) {
bsw/jbe@1309 7320 var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
bsw/jbe@1309 7321 opening = BRACKETS[punctuation];
bsw/jbe@1309 7322 url = url.replace(TRAILING_CHAR_REG_EXP, "");
bsw/jbe@1309 7323
bsw/jbe@1309 7324 if (url.split(opening).length > url.split(punctuation).length) {
bsw/jbe@1309 7325 url = url + punctuation;
bsw/jbe@1309 7326 punctuation = "";
bsw/jbe@1309 7327 }
bsw/jbe@1309 7328 var realUrl = url,
bsw/jbe@1309 7329 displayUrl = url;
bsw/jbe@1309 7330 if (url.length > MAX_DISPLAY_LENGTH) {
bsw/jbe@1309 7331 displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
bsw/jbe@1309 7332 }
bsw/jbe@1309 7333 // Add http prefix if necessary
bsw/jbe@1309 7334 if (realUrl.substr(0, 4) === "www.") {
bsw/jbe@1309 7335 realUrl = "http://" + realUrl;
bsw/jbe@1309 7336 }
bsw/jbe@1309 7337
bsw/jbe@1309 7338 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
bsw/jbe@1309 7339 });
bsw/jbe@1309 7340 }
bsw/jbe@1309 7341
bsw/jbe@1309 7342 /**
bsw/jbe@1309 7343 * Creates or (if already cached) returns a temp element
bsw/jbe@1309 7344 * for the given document object
bsw/jbe@1309 7345 */
bsw/jbe@1309 7346 function _getTempElement(context) {
bsw/jbe@1309 7347 var tempElement = context._wysihtml_tempElement;
bsw/jbe@1309 7348 if (!tempElement) {
bsw/jbe@1309 7349 tempElement = context._wysihtml_tempElement = context.createElement("div");
bsw/jbe@1309 7350 }
bsw/jbe@1309 7351 return tempElement;
bsw/jbe@1309 7352 }
bsw/jbe@1309 7353
bsw/jbe@1309 7354 /**
bsw/jbe@1309 7355 * Replaces the original text nodes with the newly auto-linked dom tree
bsw/jbe@1309 7356 */
bsw/jbe@1309 7357 function _wrapMatchesInNode(textNode) {
bsw/jbe@1309 7358 var parentNode = textNode.parentNode,
bsw/jbe@1309 7359 nodeValue = wysihtml.lang.string(textNode.data).escapeHTML(),
bsw/jbe@1309 7360 tempElement = _getTempElement(parentNode.ownerDocument);
bsw/jbe@1309 7361
bsw/jbe@1309 7362 // We need to insert an empty/temporary <span /> to fix IE quirks
bsw/jbe@1309 7363 // Elsewise IE would strip white space in the beginning
bsw/jbe@1309 7364 tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue);
bsw/jbe@1309 7365 tempElement.removeChild(tempElement.firstChild);
bsw/jbe@1309 7366
bsw/jbe@1309 7367 while (tempElement.firstChild) {
bsw/jbe@1309 7368 // inserts tempElement.firstChild before textNode
bsw/jbe@1309 7369 parentNode.insertBefore(tempElement.firstChild, textNode);
bsw/jbe@1309 7370 }
bsw/jbe@1309 7371 parentNode.removeChild(textNode);
bsw/jbe@1309 7372 }
bsw/jbe@1309 7373
bsw/jbe@1309 7374 function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
bsw/jbe@1309 7375 var nodeName;
bsw/jbe@1309 7376 while (node.parentNode) {
bsw/jbe@1309 7377 node = node.parentNode;
bsw/jbe@1309 7378 nodeName = node.nodeName;
bsw/jbe@1309 7379 if (node.className && wysihtml.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
bsw/jbe@1309 7380 return true;
bsw/jbe@1309 7381 }
bsw/jbe@1309 7382 if (IGNORE_URLS_IN.contains(nodeName)) {
bsw/jbe@1309 7383 return true;
bsw/jbe@1309 7384 } else if (nodeName === "body") {
bsw/jbe@1309 7385 return false;
bsw/jbe@1309 7386 }
bsw/jbe@1309 7387 }
bsw/jbe@1309 7388 return false;
bsw/jbe@1309 7389 }
bsw/jbe@1309 7390
bsw/jbe@1309 7391 function _parseNode(element, ignoreInClasses) {
bsw/jbe@1309 7392 if (IGNORE_URLS_IN.contains(element.nodeName)) {
bsw/jbe@1309 7393 return;
bsw/jbe@1309 7394 }
bsw/jbe@1309 7395
bsw/jbe@1309 7396 if (element.className && wysihtml.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
bsw/jbe@1309 7397 return;
bsw/jbe@1309 7398 }
bsw/jbe@1309 7399
bsw/jbe@1309 7400 if (element.nodeType === wysihtml.TEXT_NODE && element.data.match(URL_REG_EXP)) {
bsw/jbe@1309 7401 _wrapMatchesInNode(element);
bsw/jbe@1309 7402 return;
bsw/jbe@1309 7403 }
bsw/jbe@1309 7404
bsw/jbe@1309 7405 var childNodes = wysihtml.lang.array(element.childNodes).get(),
bsw/jbe@1309 7406 childNodesLength = childNodes.length,
bsw/jbe@1309 7407 i = 0;
bsw/jbe@1309 7408
bsw/jbe@1309 7409 for (; i<childNodesLength; i++) {
bsw/jbe@1309 7410 _parseNode(childNodes[i], ignoreInClasses);
bsw/jbe@1309 7411 }
bsw/jbe@1309 7412
bsw/jbe@1309 7413 return element;
bsw/jbe@1309 7414 }
bsw/jbe@1309 7415
bsw/jbe@1309 7416 wysihtml.dom.autoLink = autoLink;
bsw/jbe@1309 7417
bsw/jbe@1309 7418 // Reveal url reg exp to the outside
bsw/jbe@1309 7419 wysihtml.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
bsw/jbe@1309 7420 })(wysihtml);
bsw/jbe@1309 7421
bsw/jbe@1309 7422 (function(wysihtml) {
bsw/jbe@1309 7423 var api = wysihtml.dom;
bsw/jbe@1309 7424
bsw/jbe@1309 7425 api.addClass = function(element, className) {
bsw/jbe@1309 7426 var classList = element.classList;
bsw/jbe@1309 7427 if (classList) {
bsw/jbe@1309 7428 return classList.add(className);
bsw/jbe@1309 7429 }
bsw/jbe@1309 7430 if (api.hasClass(element, className)) {
bsw/jbe@1309 7431 return;
bsw/jbe@1309 7432 }
bsw/jbe@1309 7433 element.className += " " + className;
bsw/jbe@1309 7434 };
bsw/jbe@1309 7435
bsw/jbe@1309 7436 api.removeClass = function(element, className) {
bsw/jbe@1309 7437 var classList = element.classList;
bsw/jbe@1309 7438 if (classList) {
bsw/jbe@1309 7439 return classList.remove(className);
bsw/jbe@1309 7440 }
bsw/jbe@1309 7441
bsw/jbe@1309 7442 element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
bsw/jbe@1309 7443 };
bsw/jbe@1309 7444
bsw/jbe@1309 7445 api.hasClass = function(element, className) {
bsw/jbe@1309 7446 var classList = element.classList;
bsw/jbe@1309 7447 if (classList) {
bsw/jbe@1309 7448 return classList.contains(className);
bsw/jbe@1309 7449 }
bsw/jbe@1309 7450
bsw/jbe@1309 7451 var elementClassName = element.className;
bsw/jbe@1309 7452 return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
bsw/jbe@1309 7453 };
bsw/jbe@1309 7454 })(wysihtml);
bsw/jbe@1309 7455
bsw/jbe@1309 7456 wysihtml.dom.compareDocumentPosition = (function() {
bsw/jbe@1309 7457 var documentElement = document.documentElement;
bsw/jbe@1309 7458 if (documentElement.compareDocumentPosition) {
bsw/jbe@1309 7459 return function(container, element) {
bsw/jbe@1309 7460 return container.compareDocumentPosition(element);
bsw/jbe@1309 7461 };
bsw/jbe@1309 7462 } else {
bsw/jbe@1309 7463 return function( container, element ) {
bsw/jbe@1309 7464 // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license
bsw/jbe@1309 7465 var thisOwner, otherOwner;
bsw/jbe@1309 7466
bsw/jbe@1309 7467 if( container.nodeType === 9) // Node.DOCUMENT_NODE
bsw/jbe@1309 7468 thisOwner = container;
bsw/jbe@1309 7469 else
bsw/jbe@1309 7470 thisOwner = container.ownerDocument;
bsw/jbe@1309 7471
bsw/jbe@1309 7472 if( element.nodeType === 9) // Node.DOCUMENT_NODE
bsw/jbe@1309 7473 otherOwner = element;
bsw/jbe@1309 7474 else
bsw/jbe@1309 7475 otherOwner = element.ownerDocument;
bsw/jbe@1309 7476
bsw/jbe@1309 7477 if( container === element ) return 0;
bsw/jbe@1309 7478 if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
bsw/jbe@1309 7479 if( container.ownerDocument === element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
bsw/jbe@1309 7480 if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED;
bsw/jbe@1309 7481
bsw/jbe@1309 7482 // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child.
bsw/jbe@1309 7483 if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml.lang.array(container.childNodes).indexOf( element ) !== -1)
bsw/jbe@1309 7484 return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
bsw/jbe@1309 7485
bsw/jbe@1309 7486 if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml.lang.array(element.childNodes).indexOf( container ) !== -1)
bsw/jbe@1309 7487 return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
bsw/jbe@1309 7488
bsw/jbe@1309 7489 var point = container;
bsw/jbe@1309 7490 var parents = [ ];
bsw/jbe@1309 7491 var previous = null;
bsw/jbe@1309 7492 while( point ) {
bsw/jbe@1309 7493 if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
bsw/jbe@1309 7494 parents.push( point );
bsw/jbe@1309 7495 point = point.parentNode;
bsw/jbe@1309 7496 }
bsw/jbe@1309 7497 point = element;
bsw/jbe@1309 7498 previous = null;
bsw/jbe@1309 7499 while( point ) {
bsw/jbe@1309 7500 if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
bsw/jbe@1309 7501 var location_index = wysihtml.lang.array(parents).indexOf( point );
bsw/jbe@1309 7502 if( location_index !== -1) {
bsw/jbe@1309 7503 var smallest_common_ancestor = parents[ location_index ];
bsw/jbe@1309 7504 var this_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] );
bsw/jbe@1309 7505 var other_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous );
bsw/jbe@1309 7506 if( this_index > other_index ) {
bsw/jbe@1309 7507 return 2; //Node.DOCUMENT_POSITION_PRECEDING;
bsw/jbe@1309 7508 }
bsw/jbe@1309 7509 else {
bsw/jbe@1309 7510 return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
bsw/jbe@1309 7511 }
bsw/jbe@1309 7512 }
bsw/jbe@1309 7513 previous = point;
bsw/jbe@1309 7514 point = point.parentNode;
bsw/jbe@1309 7515 }
bsw/jbe@1309 7516 return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
bsw/jbe@1309 7517 };
bsw/jbe@1309 7518 }
bsw/jbe@1309 7519 })();
bsw/jbe@1309 7520
bsw/jbe@1309 7521 wysihtml.dom.contains = (function() {
bsw/jbe@1309 7522 var documentElement = document.documentElement;
bsw/jbe@1309 7523 if (documentElement.contains) {
bsw/jbe@1309 7524 return function(container, element) {
bsw/jbe@1309 7525 if (element.nodeType !== wysihtml.ELEMENT_NODE) {
bsw/jbe@1309 7526 if (element.parentNode === container) {
bsw/jbe@1309 7527 return true;
bsw/jbe@1309 7528 }
bsw/jbe@1309 7529 element = element.parentNode;
bsw/jbe@1309 7530 }
bsw/jbe@1309 7531 return container !== element && container.contains(element);
bsw/jbe@1309 7532 };
bsw/jbe@1309 7533 } else if (documentElement.compareDocumentPosition) {
bsw/jbe@1309 7534 return function(container, element) {
bsw/jbe@1309 7535 // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
bsw/jbe@1309 7536 return !!(container.compareDocumentPosition(element) & 16);
bsw/jbe@1309 7537 };
bsw/jbe@1309 7538 }
bsw/jbe@1309 7539 })();
bsw/jbe@1309 7540
bsw/jbe@1309 7541 (function(wysihtml) {
bsw/jbe@1309 7542 var doc = document;
bsw/jbe@1309 7543 wysihtml.dom.ContentEditableArea = Base.extend({
bsw/jbe@1309 7544 getContentEditable: function() {
bsw/jbe@1309 7545 return this.element;
bsw/jbe@1309 7546 },
bsw/jbe@1309 7547
bsw/jbe@1309 7548 getWindow: function() {
bsw/jbe@1309 7549 return this.element.ownerDocument.defaultView || this.element.ownerDocument.parentWindow;
bsw/jbe@1309 7550 },
bsw/jbe@1309 7551
bsw/jbe@1309 7552 getDocument: function() {
bsw/jbe@1309 7553 return this.element.ownerDocument;
bsw/jbe@1309 7554 },
bsw/jbe@1309 7555
bsw/jbe@1309 7556 constructor: function(readyCallback, config, contentEditable) {
bsw/jbe@1309 7557 this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
bsw/jbe@1309 7558 this.config = wysihtml.lang.object({}).merge(config).get();
bsw/jbe@1309 7559 if (!this.config.className) {
bsw/jbe@1309 7560 this.config.className = "wysihtml-sandbox";
bsw/jbe@1309 7561 }
bsw/jbe@1309 7562 if (contentEditable) {
bsw/jbe@1309 7563 this.element = this._bindElement(contentEditable);
bsw/jbe@1309 7564 } else {
bsw/jbe@1309 7565 this.element = this._createElement();
bsw/jbe@1309 7566 }
bsw/jbe@1309 7567 },
bsw/jbe@1309 7568
bsw/jbe@1309 7569 destroy: function() {
bsw/jbe@1309 7570
bsw/jbe@1309 7571 },
bsw/jbe@1309 7572
bsw/jbe@1309 7573 // creates a new contenteditable and initiates it
bsw/jbe@1309 7574 _createElement: function() {
bsw/jbe@1309 7575 var element = doc.createElement("div");
bsw/jbe@1309 7576 element.className = this.config.className;
bsw/jbe@1309 7577 this._loadElement(element);
bsw/jbe@1309 7578 return element;
bsw/jbe@1309 7579 },
bsw/jbe@1309 7580
bsw/jbe@1309 7581 // initiates an allready existent contenteditable
bsw/jbe@1309 7582 _bindElement: function(contentEditable) {
bsw/jbe@1309 7583 contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml-sandbox" : "wysihtml-sandbox";
bsw/jbe@1309 7584 this._loadElement(contentEditable, true);
bsw/jbe@1309 7585 return contentEditable;
bsw/jbe@1309 7586 },
bsw/jbe@1309 7587
bsw/jbe@1309 7588 _loadElement: function(element, contentExists) {
bsw/jbe@1309 7589 var that = this;
bsw/jbe@1309 7590
bsw/jbe@1309 7591 if (!contentExists) {
bsw/jbe@1309 7592 var innerHtml = this._getHtml();
bsw/jbe@1309 7593 element.innerHTML = innerHtml;
bsw/jbe@1309 7594 }
bsw/jbe@1309 7595
bsw/jbe@1309 7596 this.loaded = true;
bsw/jbe@1309 7597 // Trigger the callback
bsw/jbe@1309 7598 setTimeout(function() { that.callback(that); }, 0);
bsw/jbe@1309 7599 },
bsw/jbe@1309 7600
bsw/jbe@1309 7601 _getHtml: function(templateVars) {
bsw/jbe@1309 7602 return '';
bsw/jbe@1309 7603 }
bsw/jbe@1309 7604
bsw/jbe@1309 7605 });
bsw/jbe@1309 7606 })(wysihtml);
bsw/jbe@1309 7607
bsw/jbe@1309 7608 /**
bsw/jbe@1309 7609 * Converts an HTML fragment/element into a unordered/ordered list
bsw/jbe@1309 7610 *
bsw/jbe@1309 7611 * @param {Element} element The element which should be turned into a list
bsw/jbe@1309 7612 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
bsw/jbe@1309 7613 * @return {Element} The created list
bsw/jbe@1309 7614 *
bsw/jbe@1309 7615 * @example
bsw/jbe@1309 7616 * <!-- Assume the following dom: -->
bsw/jbe@1309 7617 * <span id="pseudo-list">
bsw/jbe@1309 7618 * eminem<br>
bsw/jbe@1309 7619 * dr. dre
bsw/jbe@1309 7620 * <div>50 Cent</div>
bsw/jbe@1309 7621 * </span>
bsw/jbe@1309 7622 *
bsw/jbe@1309 7623 * <script>
bsw/jbe@1309 7624 * wysihtml.dom.convertToList(document.getElementById("pseudo-list"), "ul");
bsw/jbe@1309 7625 * </script>
bsw/jbe@1309 7626 *
bsw/jbe@1309 7627 * <!-- Will result in: -->
bsw/jbe@1309 7628 * <ul>
bsw/jbe@1309 7629 * <li>eminem</li>
bsw/jbe@1309 7630 * <li>dr. dre</li>
bsw/jbe@1309 7631 * <li>50 Cent</li>
bsw/jbe@1309 7632 * </ul>
bsw/jbe@1309 7633 */
bsw/jbe@1309 7634 wysihtml.dom.convertToList = (function() {
bsw/jbe@1309 7635 function _createListItem(doc, list) {
bsw/jbe@1309 7636 var listItem = doc.createElement("li");
bsw/jbe@1309 7637 list.appendChild(listItem);
bsw/jbe@1309 7638 return listItem;
bsw/jbe@1309 7639 }
bsw/jbe@1309 7640
bsw/jbe@1309 7641 function _createList(doc, type) {
bsw/jbe@1309 7642 return doc.createElement(type);
bsw/jbe@1309 7643 }
bsw/jbe@1309 7644
bsw/jbe@1309 7645 function convertToList(element, listType, uneditableClass) {
bsw/jbe@1309 7646 if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
bsw/jbe@1309 7647 // Already a list
bsw/jbe@1309 7648 return element;
bsw/jbe@1309 7649 }
bsw/jbe@1309 7650
bsw/jbe@1309 7651 var doc = element.ownerDocument,
bsw/jbe@1309 7652 list = _createList(doc, listType),
bsw/jbe@1309 7653 lineBreaks = element.querySelectorAll("br"),
bsw/jbe@1309 7654 lineBreaksLength = lineBreaks.length,
bsw/jbe@1309 7655 childNodes,
bsw/jbe@1309 7656 childNodesLength,
bsw/jbe@1309 7657 childNode,
bsw/jbe@1309 7658 lineBreak,
bsw/jbe@1309 7659 parentNode,
bsw/jbe@1309 7660 isBlockElement,
bsw/jbe@1309 7661 isLineBreak,
bsw/jbe@1309 7662 currentListItem,
bsw/jbe@1309 7663 i;
bsw/jbe@1309 7664
bsw/jbe@1309 7665 // First find <br> at the end of inline elements and move them behind them
bsw/jbe@1309 7666 for (i=0; i<lineBreaksLength; i++) {
bsw/jbe@1309 7667 lineBreak = lineBreaks[i];
bsw/jbe@1309 7668 while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
bsw/jbe@1309 7669 if (wysihtml.dom.getStyle("display").from(parentNode) === "block") {
bsw/jbe@1309 7670 parentNode.removeChild(lineBreak);
bsw/jbe@1309 7671 break;
bsw/jbe@1309 7672 }
bsw/jbe@1309 7673 wysihtml.dom.insert(lineBreak).after(lineBreak.parentNode);
bsw/jbe@1309 7674 }
bsw/jbe@1309 7675 }
bsw/jbe@1309 7676
bsw/jbe@1309 7677 childNodes = wysihtml.lang.array(element.childNodes).get();
bsw/jbe@1309 7678 childNodesLength = childNodes.length;
bsw/jbe@1309 7679
bsw/jbe@1309 7680 for (i=0; i<childNodesLength; i++) {
bsw/jbe@1309 7681 currentListItem = currentListItem || _createListItem(doc, list);
bsw/jbe@1309 7682 childNode = childNodes[i];
bsw/jbe@1309 7683 isBlockElement = wysihtml.dom.getStyle("display").from(childNode) === "block";
bsw/jbe@1309 7684 isLineBreak = childNode.nodeName === "BR";
bsw/jbe@1309 7685
bsw/jbe@1309 7686 // consider uneditable as an inline element
bsw/jbe@1309 7687 if (isBlockElement && (!uneditableClass || !wysihtml.dom.hasClass(childNode, uneditableClass))) {
bsw/jbe@1309 7688 // Append blockElement to current <li> if empty, otherwise create a new one
bsw/jbe@1309 7689 currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
bsw/jbe@1309 7690 currentListItem.appendChild(childNode);
bsw/jbe@1309 7691 currentListItem = null;
bsw/jbe@1309 7692 continue;
bsw/jbe@1309 7693 }
bsw/jbe@1309 7694
bsw/jbe@1309 7695 if (isLineBreak) {
bsw/jbe@1309 7696 // Only create a new list item in the next iteration when the current one has already content
bsw/jbe@1309 7697 currentListItem = currentListItem.firstChild ? null : currentListItem;
bsw/jbe@1309 7698 continue;
bsw/jbe@1309 7699 }
bsw/jbe@1309 7700
bsw/jbe@1309 7701 currentListItem.appendChild(childNode);
bsw/jbe@1309 7702 }
bsw/jbe@1309 7703
bsw/jbe@1309 7704 if (childNodes.length === 0) {
bsw/jbe@1309 7705 _createListItem(doc, list);
bsw/jbe@1309 7706 }
bsw/jbe@1309 7707
bsw/jbe@1309 7708 element.parentNode.replaceChild(list, element);
bsw/jbe@1309 7709 return list;
bsw/jbe@1309 7710 }
bsw/jbe@1309 7711
bsw/jbe@1309 7712 return convertToList;
bsw/jbe@1309 7713 })();
bsw/jbe@1309 7714
bsw/jbe@1309 7715 /**
bsw/jbe@1309 7716 * Copy a set of attributes from one element to another
bsw/jbe@1309 7717 *
bsw/jbe@1309 7718 * @param {Array} attributesToCopy List of attributes which should be copied
bsw/jbe@1309 7719 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
bsw/jbe@1309 7720 * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
bsw/jbe@1309 7721 * with the element where to copy the attributes to (see example)
bsw/jbe@1309 7722 *
bsw/jbe@1309 7723 * @example
bsw/jbe@1309 7724 * var textarea = document.querySelector("textarea"),
bsw/jbe@1309 7725 * div = document.querySelector("div[contenteditable=true]"),
bsw/jbe@1309 7726 * anotherDiv = document.querySelector("div.preview");
bsw/jbe@1309 7727 * wysihtml.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
bsw/jbe@1309 7728 *
bsw/jbe@1309 7729 */
bsw/jbe@1309 7730 wysihtml.dom.copyAttributes = function(attributesToCopy) {
bsw/jbe@1309 7731 return {
bsw/jbe@1309 7732 from: function(elementToCopyFrom) {
bsw/jbe@1309 7733 return {
bsw/jbe@1309 7734 to: function pasteElementAttributesTo(elementToCopyTo) {
bsw/jbe@1309 7735 var attribute,
bsw/jbe@1309 7736 i = 0,
bsw/jbe@1309 7737 length = attributesToCopy.length;
bsw/jbe@1309 7738 for (; i<length; i++) {
bsw/jbe@1309 7739 attribute = attributesToCopy[i];
bsw/jbe@1309 7740 if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
bsw/jbe@1309 7741 elementToCopyTo[attribute] = elementToCopyFrom[attribute];
bsw/jbe@1309 7742 }
bsw/jbe@1309 7743 }
bsw/jbe@1309 7744 return { andTo: pasteElementAttributesTo };
bsw/jbe@1309 7745 }
bsw/jbe@1309 7746 };
bsw/jbe@1309 7747 }
bsw/jbe@1309 7748 };
bsw/jbe@1309 7749 };
bsw/jbe@1309 7750
bsw/jbe@1309 7751 /**
bsw/jbe@1309 7752 * Copy a set of styles from one element to another
bsw/jbe@1309 7753 * Please note that this only works properly across browsers when the element from which to copy the styles
bsw/jbe@1309 7754 * is in the dom
bsw/jbe@1309 7755 *
bsw/jbe@1309 7756 * Interesting article on how to copy styles
bsw/jbe@1309 7757 *
bsw/jbe@1309 7758 * @param {Array} stylesToCopy List of styles which should be copied
bsw/jbe@1309 7759 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
bsw/jbe@1309 7760 * copy the styles from., this again returns an object which provides a method named "to" which can be invoked
bsw/jbe@1309 7761 * with the element where to copy the styles to (see example)
bsw/jbe@1309 7762 *
bsw/jbe@1309 7763 * @example
bsw/jbe@1309 7764 * var textarea = document.querySelector("textarea"),
bsw/jbe@1309 7765 * div = document.querySelector("div[contenteditable=true]"),
bsw/jbe@1309 7766 * anotherDiv = document.querySelector("div.preview");
bsw/jbe@1309 7767 * wysihtml.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
bsw/jbe@1309 7768 *
bsw/jbe@1309 7769 */
bsw/jbe@1309 7770 (function(dom) {
bsw/jbe@1309 7771
bsw/jbe@1309 7772 /**
bsw/jbe@1309 7773 * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
bsw/jbe@1309 7774 * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
bsw/jbe@1309 7775 * its computed css width will be 198px
bsw/jbe@1309 7776 *
bsw/jbe@1309 7777 * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992
bsw/jbe@1309 7778 */
bsw/jbe@1309 7779 var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
bsw/jbe@1309 7780
bsw/jbe@1309 7781 var shouldIgnoreBoxSizingBorderBox = function(element) {
bsw/jbe@1309 7782 if (hasBoxSizingBorderBox(element)) {
bsw/jbe@1309 7783 return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
bsw/jbe@1309 7784 }
bsw/jbe@1309 7785 return false;
bsw/jbe@1309 7786 };
bsw/jbe@1309 7787
bsw/jbe@1309 7788 var hasBoxSizingBorderBox = function(element) {
bsw/jbe@1309 7789 var i = 0,
bsw/jbe@1309 7790 length = BOX_SIZING_PROPERTIES.length;
bsw/jbe@1309 7791 for (; i<length; i++) {
bsw/jbe@1309 7792 if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
bsw/jbe@1309 7793 return BOX_SIZING_PROPERTIES[i];
bsw/jbe@1309 7794 }
bsw/jbe@1309 7795 }
bsw/jbe@1309 7796 };
bsw/jbe@1309 7797
bsw/jbe@1309 7798 dom.copyStyles = function(stylesToCopy) {
bsw/jbe@1309 7799 return {
bsw/jbe@1309 7800 from: function(element) {
bsw/jbe@1309 7801 if (shouldIgnoreBoxSizingBorderBox(element)) {
bsw/jbe@1309 7802 stylesToCopy = wysihtml.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
bsw/jbe@1309 7803 }
bsw/jbe@1309 7804
bsw/jbe@1309 7805 var cssText = "",
bsw/jbe@1309 7806 length = stylesToCopy.length,
bsw/jbe@1309 7807 i = 0,
bsw/jbe@1309 7808 property;
bsw/jbe@1309 7809 for (; i<length; i++) {
bsw/jbe@1309 7810 property = stylesToCopy[i];
bsw/jbe@1309 7811 cssText += property + ":" + dom.getStyle(property).from(element) + ";";
bsw/jbe@1309 7812 }
bsw/jbe@1309 7813
bsw/jbe@1309 7814 return {
bsw/jbe@1309 7815 to: function pasteStylesTo(element) {
bsw/jbe@1309 7816 dom.setStyles(cssText).on(element);
bsw/jbe@1309 7817 return { andTo: pasteStylesTo };
bsw/jbe@1309 7818 }
bsw/jbe@1309 7819 };
bsw/jbe@1309 7820 }
bsw/jbe@1309 7821 };
bsw/jbe@1309 7822 };
bsw/jbe@1309 7823 })(wysihtml.dom);
bsw/jbe@1309 7824
bsw/jbe@1309 7825 /**
bsw/jbe@1309 7826 * Event Delegation
bsw/jbe@1309 7827 *
bsw/jbe@1309 7828 * @example
bsw/jbe@1309 7829 * wysihtml.dom.delegate(document.body, "a", "click", function() {
bsw/jbe@1309 7830 * // foo
bsw/jbe@1309 7831 * });
bsw/jbe@1309 7832 */
bsw/jbe@1309 7833 (function(wysihtml) {
bsw/jbe@1309 7834 wysihtml.dom.delegate = function(container, selector, eventName, handler) {
bsw/jbe@1309 7835 var callback = function(event) {
bsw/jbe@1309 7836 var target = event.target,
bsw/jbe@1309 7837 element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes
bsw/jbe@1309 7838 matches = container.querySelectorAll(selector);
bsw/jbe@1309 7839
bsw/jbe@1309 7840 for (var i = 0, max = matches.length; i < max; i++) {
bsw/jbe@1309 7841 if (matches[i].contains(element)) {
bsw/jbe@1309 7842 handler.call(matches[i], event);
bsw/jbe@1309 7843 }
bsw/jbe@1309 7844 }
bsw/jbe@1309 7845 };
bsw/jbe@1309 7846
bsw/jbe@1309 7847 container.addEventListener(eventName, callback, false);
bsw/jbe@1309 7848 return {
bsw/jbe@1309 7849 stop: function() {
bsw/jbe@1309 7850 container.removeEventListener(eventName, callback, false);
bsw/jbe@1309 7851 }
bsw/jbe@1309 7852 };
bsw/jbe@1309 7853 };
bsw/jbe@1309 7854 })(wysihtml);
bsw/jbe@1309 7855
bsw/jbe@1309 7856 // TODO: Refactor dom tree traversing here
bsw/jbe@1309 7857 (function(wysihtml) {
bsw/jbe@1309 7858
bsw/jbe@1309 7859 // Finds parents of a node, returning the outermost node first in Array
bsw/jbe@1309 7860 // if contain node is given parents search is stopped at the container
bsw/jbe@1309 7861 function parents(node, container) {
bsw/jbe@1309 7862 var nodes = [node], n = node;
bsw/jbe@1309 7863
bsw/jbe@1309 7864 // iterate parents while parent exists and it is not container element
bsw/jbe@1309 7865 while((container && n && n !== container) || (!container && n)) {
bsw/jbe@1309 7866 nodes.unshift(n);
bsw/jbe@1309 7867 n = n.parentNode;
bsw/jbe@1309 7868 }
bsw/jbe@1309 7869 return nodes;
bsw/jbe@1309 7870 }
bsw/jbe@1309 7871
bsw/jbe@1309 7872 wysihtml.dom.domNode = function(node) {
bsw/jbe@1309 7873 var defaultNodeTypes = [wysihtml.ELEMENT_NODE, wysihtml.TEXT_NODE];
bsw/jbe@1309 7874
bsw/jbe@1309 7875 return {
bsw/jbe@1309 7876
bsw/jbe@1309 7877 is: {
bsw/jbe@1309 7878 emptyTextNode: function(ignoreWhitespace) {
bsw/jbe@1309 7879 var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g);
bsw/jbe@1309 7880 return node && node.nodeType === wysihtml.TEXT_NODE && (regx).test(node.data);
bsw/jbe@1309 7881 },
bsw/jbe@1309 7882
bsw/jbe@1309 7883 // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
bsw/jbe@1309 7884 rangyBookmark: function() {
bsw/jbe@1309 7885 return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary');
bsw/jbe@1309 7886 },
bsw/jbe@1309 7887
bsw/jbe@1309 7888 visible: function() {
bsw/jbe@1309 7889 var isVisible = !(/^\s*$/g).test(wysihtml.dom.getTextContent(node));
bsw/jbe@1309 7890
bsw/jbe@1309 7891 if (!isVisible) {
bsw/jbe@1309 7892 if (node.nodeType === 1 && node.querySelector('img, br, hr, object, embed, canvas, input, textarea')) {
bsw/jbe@1309 7893 isVisible = true;
bsw/jbe@1309 7894 }
bsw/jbe@1309 7895 }
bsw/jbe@1309 7896 return isVisible;
bsw/jbe@1309 7897 },
bsw/jbe@1309 7898 lineBreak: function() {
bsw/jbe@1309 7899 return node && node.nodeType === 1 && node.nodeName === "BR";
bsw/jbe@1309 7900 },
bsw/jbe@1309 7901 block: function() {
bsw/jbe@1309 7902 return node && node.nodeType === 1 && node.ownerDocument.defaultView.getComputedStyle(node).display === "block";
bsw/jbe@1309 7903 },
bsw/jbe@1309 7904 // Void elements are elemens that can not have content
bsw/jbe@1309 7905 // In most cases browsers should solve the cases for you when you try to insert content into those,
bsw/jbe@1309 7906 // but IE does not and it is not nice to do so anyway.
bsw/jbe@1309 7907 voidElement: function() {
bsw/jbe@1309 7908 return wysihtml.dom.domNode(node).test({
bsw/jbe@1309 7909 query: wysihtml.VOID_ELEMENTS
bsw/jbe@1309 7910 });
bsw/jbe@1309 7911 }
bsw/jbe@1309 7912 },
bsw/jbe@1309 7913
bsw/jbe@1309 7914 // var node = wysihtml.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 7915 prev: function(options) {
bsw/jbe@1309 7916 var prevNode = node.previousSibling,
bsw/jbe@1309 7917 types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
bsw/jbe@1309 7918
bsw/jbe@1309 7919 if (!prevNode) {
bsw/jbe@1309 7920 return null;
bsw/jbe@1309 7921 }
bsw/jbe@1309 7922
bsw/jbe@1309 7923 if (
bsw/jbe@1309 7924 wysihtml.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass)
bsw/jbe@1309 7925 (!wysihtml.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
bsw/jbe@1309 7926 (options && options.ignoreBlankTexts && wysihtml.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
bsw/jbe@1309 7927 ) {
bsw/jbe@1309 7928 return wysihtml.dom.domNode(prevNode).prev(options);
bsw/jbe@1309 7929 }
bsw/jbe@1309 7930
bsw/jbe@1309 7931 return prevNode;
bsw/jbe@1309 7932 },
bsw/jbe@1309 7933
bsw/jbe@1309 7934 // var node = wysihtml.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 7935 next: function(options) {
bsw/jbe@1309 7936 var nextNode = node.nextSibling,
bsw/jbe@1309 7937 types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
bsw/jbe@1309 7938
bsw/jbe@1309 7939 if (!nextNode) {
bsw/jbe@1309 7940 return null;
bsw/jbe@1309 7941 }
bsw/jbe@1309 7942
bsw/jbe@1309 7943 if (
bsw/jbe@1309 7944 wysihtml.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass)
bsw/jbe@1309 7945 (!wysihtml.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
bsw/jbe@1309 7946 (options && options.ignoreBlankTexts && wysihtml.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
bsw/jbe@1309 7947 ) {
bsw/jbe@1309 7948 return wysihtml.dom.domNode(nextNode).next(options);
bsw/jbe@1309 7949 }
bsw/jbe@1309 7950
bsw/jbe@1309 7951 return nextNode;
bsw/jbe@1309 7952 },
bsw/jbe@1309 7953
bsw/jbe@1309 7954 // Finds the common acnestor container of two nodes
bsw/jbe@1309 7955 // If container given stops search at the container
bsw/jbe@1309 7956 // If no common ancestor found returns null
bsw/jbe@1309 7957 // var node = wysihtml.dom.domNode(element).commonAncestor(node2, container);
bsw/jbe@1309 7958 commonAncestor: function(node2, container) {
bsw/jbe@1309 7959 var parents1 = parents(node, container),
bsw/jbe@1309 7960 parents2 = parents(node2, container);
bsw/jbe@1309 7961
bsw/jbe@1309 7962 // Ensure we have found a common ancestor, which will be the first one if anything
bsw/jbe@1309 7963 if (parents1[0] != parents2[0]) {
bsw/jbe@1309 7964 return null;
bsw/jbe@1309 7965 }
bsw/jbe@1309 7966
bsw/jbe@1309 7967 // Traverse up the hierarchy of parents until we reach where they're no longer
bsw/jbe@1309 7968 // the same. Then return previous which was the common ancestor.
bsw/jbe@1309 7969 for (var i = 0; i < parents1.length; i++) {
bsw/jbe@1309 7970 if (parents1[i] != parents2[i]) {
bsw/jbe@1309 7971 return parents1[i - 1];
bsw/jbe@1309 7972 }
bsw/jbe@1309 7973 }
bsw/jbe@1309 7974
bsw/jbe@1309 7975 return null;
bsw/jbe@1309 7976 },
bsw/jbe@1309 7977
bsw/jbe@1309 7978 // Traverses a node for last children and their chidren (including itself), and finds the last node that has no children.
bsw/jbe@1309 7979 // Array of classes for forced last-leaves (ex: uneditable-container) can be defined (options = {leafClasses: [...]})
bsw/jbe@1309 7980 // Useful for finding the actually visible element before cursor
bsw/jbe@1309 7981 lastLeafNode: function(options) {
bsw/jbe@1309 7982 var lastChild;
bsw/jbe@1309 7983
bsw/jbe@1309 7984 // Returns non-element nodes
bsw/jbe@1309 7985 if (node.nodeType !== 1) {
bsw/jbe@1309 7986 return node;
bsw/jbe@1309 7987 }
bsw/jbe@1309 7988
bsw/jbe@1309 7989 // Returns if element is leaf
bsw/jbe@1309 7990 lastChild = node.lastChild;
bsw/jbe@1309 7991 if (!lastChild) {
bsw/jbe@1309 7992 return node;
bsw/jbe@1309 7993 }
bsw/jbe@1309 7994
bsw/jbe@1309 7995 // Returns if element is of of options.leafClasses leaf
bsw/jbe@1309 7996 if (options && options.leafClasses) {
bsw/jbe@1309 7997 for (var i = options.leafClasses.length; i--;) {
bsw/jbe@1309 7998 if (wysihtml.dom.hasClass(node, options.leafClasses[i])) {
bsw/jbe@1309 7999 return node;
bsw/jbe@1309 8000 }
bsw/jbe@1309 8001 }
bsw/jbe@1309 8002 }
bsw/jbe@1309 8003
bsw/jbe@1309 8004 return wysihtml.dom.domNode(lastChild).lastLeafNode(options);
bsw/jbe@1309 8005 },
bsw/jbe@1309 8006
bsw/jbe@1309 8007 // Splits element at childnode and extracts the childNode out of the element context
bsw/jbe@1309 8008 // Example:
bsw/jbe@1309 8009 // var node = wysihtml.dom.domNode(node).escapeParent(parentNode);
bsw/jbe@1309 8010 escapeParent: function(element, newWrapper) {
bsw/jbe@1309 8011 var parent, split2, nodeWrap,
bsw/jbe@1309 8012 curNode = node;
bsw/jbe@1309 8013
bsw/jbe@1309 8014 // Stop if node is not a descendant of element
bsw/jbe@1309 8015 if (!wysihtml.dom.contains(element, node)) {
bsw/jbe@1309 8016 throw new Error("Child is not a descendant of node.");
bsw/jbe@1309 8017 }
bsw/jbe@1309 8018
bsw/jbe@1309 8019 // Climb up the node tree untill node is reached
bsw/jbe@1309 8020 do {
bsw/jbe@1309 8021 // Get current parent of node
bsw/jbe@1309 8022 parent = curNode.parentNode;
bsw/jbe@1309 8023
bsw/jbe@1309 8024 // Move after nodes to new clone wrapper
bsw/jbe@1309 8025 split2 = parent.cloneNode(false);
bsw/jbe@1309 8026 while (parent.lastChild && parent.lastChild !== curNode) {
bsw/jbe@1309 8027 split2.insertBefore(parent.lastChild, split2.firstChild);
bsw/jbe@1309 8028 }
bsw/jbe@1309 8029
bsw/jbe@1309 8030 // 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
bsw/jbe@1309 8031 if (parent !== element) {
bsw/jbe@1309 8032 nodeWrap = parent.cloneNode(false);
bsw/jbe@1309 8033 nodeWrap.appendChild(curNode);
bsw/jbe@1309 8034 curNode = nodeWrap;
bsw/jbe@1309 8035 }
bsw/jbe@1309 8036 parent.parentNode.insertBefore(curNode, parent.nextSibling);
bsw/jbe@1309 8037
bsw/jbe@1309 8038 // Add after nodes (unless empty)
bsw/jbe@1309 8039 if (split2.innerHTML !== '') {
bsw/jbe@1309 8040 // if contents are empty insert without wrap
bsw/jbe@1309 8041 if ((/^\s+$/).test(split2.innerHTML)) {
bsw/jbe@1309 8042 while (split2.lastChild) {
bsw/jbe@1309 8043 parent.parentNode.insertBefore(split2.lastChild, curNode.nextSibling);
bsw/jbe@1309 8044 }
bsw/jbe@1309 8045 } else {
bsw/jbe@1309 8046 parent.parentNode.insertBefore(split2, curNode.nextSibling);
bsw/jbe@1309 8047 }
bsw/jbe@1309 8048 }
bsw/jbe@1309 8049
bsw/jbe@1309 8050 // If the node left behind before the split (parent) is now empty then remove
bsw/jbe@1309 8051 if (parent.innerHTML === '') {
bsw/jbe@1309 8052 parent.parentNode.removeChild(parent);
bsw/jbe@1309 8053 } else if ((/^\s+$/).test(parent.innerHTML)) {
bsw/jbe@1309 8054 while (parent.firstChild) {
bsw/jbe@1309 8055 parent.parentNode.insertBefore(parent.firstChild, parent);
bsw/jbe@1309 8056 }
bsw/jbe@1309 8057 parent.parentNode.removeChild(parent);
bsw/jbe@1309 8058 }
bsw/jbe@1309 8059
bsw/jbe@1309 8060 } while (parent && parent !== element);
bsw/jbe@1309 8061
bsw/jbe@1309 8062 if (newWrapper && curNode) {
bsw/jbe@1309 8063 curNode.parentNode.insertBefore(newWrapper, curNode);
bsw/jbe@1309 8064 newWrapper.appendChild(curNode);
bsw/jbe@1309 8065 }
bsw/jbe@1309 8066 },
bsw/jbe@1309 8067
bsw/jbe@1309 8068 transferContentTo: function(targetNode, removeOldWrapper) {
bsw/jbe@1309 8069 if (node.nodeType === 1) {
bsw/jbe@1309 8070 if (wysihtml.dom.domNode(targetNode).is.voidElement() || targetNode.nodeType === 3) {
bsw/jbe@1309 8071 while (node.lastChild) {
bsw/jbe@1309 8072 targetNode.parentNode.insertBefore(node.lastChild, targetNode.nextSibling);
bsw/jbe@1309 8073 }
bsw/jbe@1309 8074 } else {
bsw/jbe@1309 8075 while (node.firstChild) {
bsw/jbe@1309 8076 targetNode.appendChild(node.firstChild);
bsw/jbe@1309 8077 }
bsw/jbe@1309 8078 }
bsw/jbe@1309 8079 if (removeOldWrapper) {
bsw/jbe@1309 8080 node.parentNode.removeChild(node);
bsw/jbe@1309 8081 }
bsw/jbe@1309 8082 } else if (node.nodeType === 3 || node.nodeType === 8){
bsw/jbe@1309 8083 if (wysihtml.dom.domNode(targetNode).is.voidElement()) {
bsw/jbe@1309 8084 targetNode.parentNode.insertBefore(node, targetNode.nextSibling);
bsw/jbe@1309 8085 } else {
bsw/jbe@1309 8086 targetNode.appendChild(node);
bsw/jbe@1309 8087 }
bsw/jbe@1309 8088 }
bsw/jbe@1309 8089 },
bsw/jbe@1309 8090
bsw/jbe@1309 8091 /*
bsw/jbe@1309 8092 Tests a node against properties, and returns true if matches.
bsw/jbe@1309 8093 Tests on principle that all properties defined must have at least one match.
bsw/jbe@1309 8094 styleValue parameter works in context of styleProperty and has no effect otherwise.
bsw/jbe@1309 8095 Returns true if element matches and false if it does not.
bsw/jbe@1309 8096
bsw/jbe@1309 8097 Properties for filtering element:
bsw/jbe@1309 8098 {
bsw/jbe@1309 8099 query: selector string,
bsw/jbe@1309 8100 nodeName: string (uppercase),
bsw/jbe@1309 8101 className: string,
bsw/jbe@1309 8102 classRegExp: regex,
bsw/jbe@1309 8103 styleProperty: string or [],
bsw/jbe@1309 8104 styleValue: string, [] or regex
bsw/jbe@1309 8105 }
bsw/jbe@1309 8106
bsw/jbe@1309 8107 Example:
bsw/jbe@1309 8108 var node = wysihtml.dom.domNode(element).test({})
bsw/jbe@1309 8109 */
bsw/jbe@1309 8110 test: function(properties) {
bsw/jbe@1309 8111 var prop;
bsw/jbe@1309 8112
bsw/jbe@1309 8113 // return false if properties object is not defined
bsw/jbe@1309 8114 if (!properties) {
bsw/jbe@1309 8115 return false;
bsw/jbe@1309 8116 }
bsw/jbe@1309 8117
bsw/jbe@1309 8118 // Only element nodes can be tested for these properties
bsw/jbe@1309 8119 if (node.nodeType !== 1) {
bsw/jbe@1309 8120 return false;
bsw/jbe@1309 8121 }
bsw/jbe@1309 8122
bsw/jbe@1309 8123 if (properties.query) {
bsw/jbe@1309 8124 if (!node.matches(properties.query)) {
bsw/jbe@1309 8125 return false;
bsw/jbe@1309 8126 }
bsw/jbe@1309 8127 }
bsw/jbe@1309 8128
bsw/jbe@1309 8129 if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) {
bsw/jbe@1309 8130 return false;
bsw/jbe@1309 8131 }
bsw/jbe@1309 8132
bsw/jbe@1309 8133 if (properties.className && !node.classList.contains(properties.className)) {
bsw/jbe@1309 8134 return false;
bsw/jbe@1309 8135 }
bsw/jbe@1309 8136
bsw/jbe@1309 8137 // classRegExp check (useful for classname begins with logic)
bsw/jbe@1309 8138 if (properties.classRegExp) {
bsw/jbe@1309 8139 var matches = (node.className || "").match(properties.classRegExp) || [];
bsw/jbe@1309 8140 if (matches.length === 0) {
bsw/jbe@1309 8141 return false;
bsw/jbe@1309 8142 }
bsw/jbe@1309 8143 }
bsw/jbe@1309 8144
bsw/jbe@1309 8145 // styleProperty check
bsw/jbe@1309 8146 if (properties.styleProperty && properties.styleProperty.length > 0) {
bsw/jbe@1309 8147 var hasOneStyle = false,
bsw/jbe@1309 8148 styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty];
bsw/jbe@1309 8149 for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) {
bsw/jbe@1309 8150 // Some old IE-s have different property name for cssFloat
bsw/jbe@1309 8151 prop = wysihtml.browser.fixStyleKey(styles[j]);
bsw/jbe@1309 8152 if (node.style[prop]) {
bsw/jbe@1309 8153 if (properties.styleValue) {
bsw/jbe@1309 8154 // Style value as additional parameter
bsw/jbe@1309 8155 if (properties.styleValue instanceof RegExp) {
bsw/jbe@1309 8156 // style value as Regexp
bsw/jbe@1309 8157 if (node.style[prop].trim().match(properties.styleValue).length > 0) {
bsw/jbe@1309 8158 hasOneStyle = true;
bsw/jbe@1309 8159 break;
bsw/jbe@1309 8160 }
bsw/jbe@1309 8161 } else if (Array.isArray(properties.styleValue)) {
bsw/jbe@1309 8162 // style value as array
bsw/jbe@1309 8163 if (properties.styleValue.indexOf(node.style[prop].trim())) {
bsw/jbe@1309 8164 hasOneStyle = true;
bsw/jbe@1309 8165 break;
bsw/jbe@1309 8166 }
bsw/jbe@1309 8167 } else {
bsw/jbe@1309 8168 // style value as string
bsw/jbe@1309 8169 if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) {
bsw/jbe@1309 8170 hasOneStyle = true;
bsw/jbe@1309 8171 break;
bsw/jbe@1309 8172 }
bsw/jbe@1309 8173 }
bsw/jbe@1309 8174 } else {
bsw/jbe@1309 8175 hasOneStyle = true;
bsw/jbe@1309 8176 break;
bsw/jbe@1309 8177 }
bsw/jbe@1309 8178 }
bsw/jbe@1309 8179 if (!hasOneStyle) {
bsw/jbe@1309 8180 return false;
bsw/jbe@1309 8181 }
bsw/jbe@1309 8182 }
bsw/jbe@1309 8183 }
bsw/jbe@1309 8184
bsw/jbe@1309 8185 if (properties.attribute) {
bsw/jbe@1309 8186 var attr = wysihtml.dom.getAttributes(node),
bsw/jbe@1309 8187 attrList = [],
bsw/jbe@1309 8188 hasOneAttribute = false;
bsw/jbe@1309 8189
bsw/jbe@1309 8190 if (Array.isArray(properties.attribute)) {
bsw/jbe@1309 8191 attrList = properties.attribute;
bsw/jbe@1309 8192 } else {
bsw/jbe@1309 8193 attrList[properties.attribute] = properties.attributeValue;
bsw/jbe@1309 8194 }
bsw/jbe@1309 8195
bsw/jbe@1309 8196 for (var a in attrList) {
bsw/jbe@1309 8197 if (attrList.hasOwnProperty(a)) {
bsw/jbe@1309 8198 if (typeof attrList[a] === "undefined") {
bsw/jbe@1309 8199 if (typeof attr[a] !== "undefined") {
bsw/jbe@1309 8200 hasOneAttribute = true;
bsw/jbe@1309 8201 break;
bsw/jbe@1309 8202 }
bsw/jbe@1309 8203 } else if (attr[a] === attrList[a]) {
bsw/jbe@1309 8204 hasOneAttribute = true;
bsw/jbe@1309 8205 break;
bsw/jbe@1309 8206 }
bsw/jbe@1309 8207 }
bsw/jbe@1309 8208 }
bsw/jbe@1309 8209
bsw/jbe@1309 8210 if (!hasOneAttribute) {
bsw/jbe@1309 8211 return false;
bsw/jbe@1309 8212 }
bsw/jbe@1309 8213
bsw/jbe@1309 8214 }
bsw/jbe@1309 8215
bsw/jbe@1309 8216 return true;
bsw/jbe@1309 8217 }
bsw/jbe@1309 8218
bsw/jbe@1309 8219 };
bsw/jbe@1309 8220 };
bsw/jbe@1309 8221 })(wysihtml);
bsw/jbe@1309 8222
bsw/jbe@1309 8223 /**
bsw/jbe@1309 8224 * Returns the given html wrapped in a div element
bsw/jbe@1309 8225 *
bsw/jbe@1309 8226 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
bsw/jbe@1309 8227 * when inserted via innerHTML
bsw/jbe@1309 8228 *
bsw/jbe@1309 8229 * @param {String} html The html which should be wrapped in a dom element
bsw/jbe@1309 8230 * @param {Obejct} [context] Document object of the context the html belongs to
bsw/jbe@1309 8231 *
bsw/jbe@1309 8232 * @example
bsw/jbe@1309 8233 * wysihtml.dom.getAsDom("<article>foo</article>");
bsw/jbe@1309 8234 */
bsw/jbe@1309 8235 wysihtml.dom.getAsDom = (function() {
bsw/jbe@1309 8236
bsw/jbe@1309 8237 var _innerHTMLShiv = function(html, context) {
bsw/jbe@1309 8238 var tempElement = context.createElement("div");
bsw/jbe@1309 8239 tempElement.style.display = "none";
bsw/jbe@1309 8240 context.body.appendChild(tempElement);
bsw/jbe@1309 8241 // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
bsw/jbe@1309 8242 try { tempElement.innerHTML = html; } catch(e) {}
bsw/jbe@1309 8243 context.body.removeChild(tempElement);
bsw/jbe@1309 8244 return tempElement;
bsw/jbe@1309 8245 };
bsw/jbe@1309 8246
bsw/jbe@1309 8247 /**
bsw/jbe@1309 8248 * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
bsw/jbe@1309 8249 */
bsw/jbe@1309 8250 var _ensureHTML5Compatibility = function(context) {
bsw/jbe@1309 8251 if (context._wysihtml_supportsHTML5Tags) {
bsw/jbe@1309 8252 return;
bsw/jbe@1309 8253 }
bsw/jbe@1309 8254 for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
bsw/jbe@1309 8255 context.createElement(HTML5_ELEMENTS[i]);
bsw/jbe@1309 8256 }
bsw/jbe@1309 8257 context._wysihtml_supportsHTML5Tags = true;
bsw/jbe@1309 8258 };
bsw/jbe@1309 8259
bsw/jbe@1309 8260
bsw/jbe@1309 8261 /**
bsw/jbe@1309 8262 * List of html5 tags
bsw/jbe@1309 8263 * taken from http://simon.html5.org/html5-elements
bsw/jbe@1309 8264 */
bsw/jbe@1309 8265 var HTML5_ELEMENTS = [
bsw/jbe@1309 8266 "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
bsw/jbe@1309 8267 "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
bsw/jbe@1309 8268 "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
bsw/jbe@1309 8269 ];
bsw/jbe@1309 8270
bsw/jbe@1309 8271 return function(html, context) {
bsw/jbe@1309 8272 context = context || document;
bsw/jbe@1309 8273 var tempElement;
bsw/jbe@1309 8274 if (typeof(html) === "object" && html.nodeType) {
bsw/jbe@1309 8275 tempElement = context.createElement("div");
bsw/jbe@1309 8276 tempElement.appendChild(html);
bsw/jbe@1309 8277 } else if (wysihtml.browser.supportsHTML5Tags(context)) {
bsw/jbe@1309 8278 tempElement = context.createElement("div");
bsw/jbe@1309 8279 tempElement.innerHTML = html;
bsw/jbe@1309 8280 } else {
bsw/jbe@1309 8281 _ensureHTML5Compatibility(context);
bsw/jbe@1309 8282 tempElement = _innerHTMLShiv(html, context);
bsw/jbe@1309 8283 }
bsw/jbe@1309 8284 return tempElement;
bsw/jbe@1309 8285 };
bsw/jbe@1309 8286 })();
bsw/jbe@1309 8287
bsw/jbe@1309 8288 /**
bsw/jbe@1309 8289 * Get a set of attribute from one element
bsw/jbe@1309 8290 *
bsw/jbe@1309 8291 * IE gives wrong results for hasAttribute/getAttribute, for example:
bsw/jbe@1309 8292 * var td = document.createElement("td");
bsw/jbe@1309 8293 * td.getAttribute("rowspan"); // => "1" in IE
bsw/jbe@1309 8294 *
bsw/jbe@1309 8295 * Therefore we have to check the element's outerHTML for the attribute
bsw/jbe@1309 8296 */
bsw/jbe@1309 8297
bsw/jbe@1309 8298 wysihtml.dom.getAttribute = function(node, attributeName) {
bsw/jbe@1309 8299 var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly();
bsw/jbe@1309 8300 attributeName = attributeName.toLowerCase();
bsw/jbe@1309 8301 var nodeName = node.nodeName;
bsw/jbe@1309 8302 if (nodeName == "IMG" && attributeName == "src" && wysihtml.dom.isLoadedImage(node) === true) {
bsw/jbe@1309 8303 // Get 'src' attribute value via object property since this will always contain the
bsw/jbe@1309 8304 // full absolute url (http://...)
bsw/jbe@1309 8305 // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
bsw/jbe@1309 8306 // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
bsw/jbe@1309 8307 return node.src;
bsw/jbe@1309 8308 } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
bsw/jbe@1309 8309 // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
bsw/jbe@1309 8310 var outerHTML = node.outerHTML.toLowerCase(),
bsw/jbe@1309 8311 // TODO: This might not work for attributes without value: <input disabled>
bsw/jbe@1309 8312 hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1;
bsw/jbe@1309 8313
bsw/jbe@1309 8314 return hasAttribute ? node.getAttribute(attributeName) : null;
bsw/jbe@1309 8315 } else{
bsw/jbe@1309 8316 return node.getAttribute(attributeName);
bsw/jbe@1309 8317 }
bsw/jbe@1309 8318 };
bsw/jbe@1309 8319
bsw/jbe@1309 8320 /**
bsw/jbe@1309 8321 * Get all attributes of an element
bsw/jbe@1309 8322 *
bsw/jbe@1309 8323 * IE gives wrong results for hasAttribute/getAttribute, for example:
bsw/jbe@1309 8324 * var td = document.createElement("td");
bsw/jbe@1309 8325 * td.getAttribute("rowspan"); // => "1" in IE
bsw/jbe@1309 8326 *
bsw/jbe@1309 8327 * Therefore we have to check the element's outerHTML for the attribute
bsw/jbe@1309 8328 */
bsw/jbe@1309 8329
bsw/jbe@1309 8330 wysihtml.dom.getAttributes = function(node) {
bsw/jbe@1309 8331 var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(),
bsw/jbe@1309 8332 nodeName = node.nodeName,
bsw/jbe@1309 8333 attributes = [],
bsw/jbe@1309 8334 attr;
bsw/jbe@1309 8335
bsw/jbe@1309 8336 for (attr in node.attributes) {
bsw/jbe@1309 8337 if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr))) {
bsw/jbe@1309 8338 if (node.attributes[attr].specified) {
bsw/jbe@1309 8339 if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml.dom.isLoadedImage(node) === true) {
bsw/jbe@1309 8340 attributes['src'] = node.src;
bsw/jbe@1309 8341 } else if (wysihtml.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) {
bsw/jbe@1309 8342 if (node.attributes[attr].value !== 1) {
bsw/jbe@1309 8343 attributes[node.attributes[attr].name] = node.attributes[attr].value;
bsw/jbe@1309 8344 }
bsw/jbe@1309 8345 } else {
bsw/jbe@1309 8346 attributes[node.attributes[attr].name] = node.attributes[attr].value;
bsw/jbe@1309 8347 }
bsw/jbe@1309 8348 }
bsw/jbe@1309 8349 }
bsw/jbe@1309 8350 }
bsw/jbe@1309 8351 return attributes;
bsw/jbe@1309 8352 };
bsw/jbe@1309 8353
bsw/jbe@1309 8354 /**
bsw/jbe@1309 8355 * Walks the dom tree from the given node up until it finds a match
bsw/jbe@1309 8356 *
bsw/jbe@1309 8357 * @param {Element} node The from which to check the parent nodes
bsw/jbe@1309 8358 * @param {Object} matchingSet Object to match against, Properties for filtering element:
bsw/jbe@1309 8359 * {
bsw/jbe@1309 8360 * query: selector string,
bsw/jbe@1309 8361 * classRegExp: regex,
bsw/jbe@1309 8362 * styleProperty: string or [],
bsw/jbe@1309 8363 * styleValue: string, [] or regex
bsw/jbe@1309 8364 * }
bsw/jbe@1309 8365 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
bsw/jbe@1309 8366 * @param {Element} Optional, defines the container that limits the search
bsw/jbe@1309 8367 *
bsw/jbe@1309 8368 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
bsw/jbe@1309 8369 */
bsw/jbe@1309 8370
bsw/jbe@1309 8371 wysihtml.dom.getParentElement = (function() {
bsw/jbe@1309 8372
bsw/jbe@1309 8373 return function(node, properties, levels, container) {
bsw/jbe@1309 8374 levels = levels || 50;
bsw/jbe@1309 8375 while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
bsw/jbe@1309 8376 if (wysihtml.dom.domNode(node).test(properties)) {
bsw/jbe@1309 8377 return node;
bsw/jbe@1309 8378 }
bsw/jbe@1309 8379 node = node.parentNode;
bsw/jbe@1309 8380 }
bsw/jbe@1309 8381 return null;
bsw/jbe@1309 8382 };
bsw/jbe@1309 8383
bsw/jbe@1309 8384 })();
bsw/jbe@1309 8385
bsw/jbe@1309 8386 /*
bsw/jbe@1309 8387 * Methods for fetching pasted html before it gets inserted into content
bsw/jbe@1309 8388 **/
bsw/jbe@1309 8389
bsw/jbe@1309 8390 /* Modern event.clipboardData driven approach.
bsw/jbe@1309 8391 * Advantage is that it does not have to loose selection or modify dom to catch the data.
bsw/jbe@1309 8392 * IE does not support though.
bsw/jbe@1309 8393 **/
bsw/jbe@1309 8394 wysihtml.dom.getPastedHtml = function(event) {
bsw/jbe@1309 8395 var html;
bsw/jbe@1309 8396 if (wysihtml.browser.supportsModernPaste() && event.clipboardData) {
bsw/jbe@1309 8397 if (wysihtml.lang.array(event.clipboardData.types).contains('text/html')) {
bsw/jbe@1309 8398 html = event.clipboardData.getData('text/html');
bsw/jbe@1309 8399 } else if (wysihtml.lang.array(event.clipboardData.types).contains('text/plain')) {
bsw/jbe@1309 8400 html = wysihtml.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true);
bsw/jbe@1309 8401 }
bsw/jbe@1309 8402 }
bsw/jbe@1309 8403 return html;
bsw/jbe@1309 8404 };
bsw/jbe@1309 8405
bsw/jbe@1309 8406 /* Older temprorary contenteditable as paste source catcher method for fallbacks */
bsw/jbe@1309 8407 wysihtml.dom.getPastedHtmlWithDiv = function (composer, f) {
bsw/jbe@1309 8408 var selBookmark = composer.selection.getBookmark(),
bsw/jbe@1309 8409 doc = composer.element.ownerDocument,
bsw/jbe@1309 8410 cleanerDiv = doc.createElement('DIV'),
bsw/jbe@1309 8411 scrollPos = composer.getScrollPos();
bsw/jbe@1309 8412
bsw/jbe@1309 8413 doc.body.appendChild(cleanerDiv);
bsw/jbe@1309 8414
bsw/jbe@1309 8415 cleanerDiv.style.width = "1px";
bsw/jbe@1309 8416 cleanerDiv.style.height = "1px";
bsw/jbe@1309 8417 cleanerDiv.style.overflow = "hidden";
bsw/jbe@1309 8418 cleanerDiv.style.position = "absolute";
bsw/jbe@1309 8419 cleanerDiv.style.top = scrollPos.y + "px";
bsw/jbe@1309 8420 cleanerDiv.style.left = scrollPos.x + "px";
bsw/jbe@1309 8421
bsw/jbe@1309 8422 cleanerDiv.setAttribute('contenteditable', 'true');
bsw/jbe@1309 8423 cleanerDiv.focus();
bsw/jbe@1309 8424
bsw/jbe@1309 8425 setTimeout(function () {
bsw/jbe@1309 8426 var html;
bsw/jbe@1309 8427
bsw/jbe@1309 8428 composer.selection.setBookmark(selBookmark);
bsw/jbe@1309 8429 html = cleanerDiv.innerHTML;
bsw/jbe@1309 8430 if (html && (/^<br\/?>$/i).test(html.trim())) {
bsw/jbe@1309 8431 html = false;
bsw/jbe@1309 8432 }
bsw/jbe@1309 8433 f(html);
bsw/jbe@1309 8434 cleanerDiv.parentNode.removeChild(cleanerDiv);
bsw/jbe@1309 8435 }, 0);
bsw/jbe@1309 8436 };
bsw/jbe@1309 8437
bsw/jbe@1309 8438 /**
bsw/jbe@1309 8439 * Get element's style for a specific css property
bsw/jbe@1309 8440 *
bsw/jbe@1309 8441 * @param {Element} element The element on which to retrieve the style
bsw/jbe@1309 8442 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
bsw/jbe@1309 8443 *
bsw/jbe@1309 8444 * @example
bsw/jbe@1309 8445 * wysihtml.dom.getStyle("display").from(document.body);
bsw/jbe@1309 8446 * // => "block"
bsw/jbe@1309 8447 */
bsw/jbe@1309 8448 wysihtml.dom.getStyle = (function() {
bsw/jbe@1309 8449 var stylePropertyMapping = {
bsw/jbe@1309 8450 "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
bsw/jbe@1309 8451 },
bsw/jbe@1309 8452 REG_EXP_CAMELIZE = /\-[a-z]/g;
bsw/jbe@1309 8453
bsw/jbe@1309 8454 function camelize(str) {
bsw/jbe@1309 8455 return str.replace(REG_EXP_CAMELIZE, function(match) {
bsw/jbe@1309 8456 return match.charAt(1).toUpperCase();
bsw/jbe@1309 8457 });
bsw/jbe@1309 8458 }
bsw/jbe@1309 8459
bsw/jbe@1309 8460 return function(property) {
bsw/jbe@1309 8461 return {
bsw/jbe@1309 8462 from: function(element) {
bsw/jbe@1309 8463 if (element.nodeType !== wysihtml.ELEMENT_NODE) {
bsw/jbe@1309 8464 return;
bsw/jbe@1309 8465 }
bsw/jbe@1309 8466
bsw/jbe@1309 8467 var doc = element.ownerDocument,
bsw/jbe@1309 8468 camelizedProperty = stylePropertyMapping[property] || camelize(property),
bsw/jbe@1309 8469 style = element.style,
bsw/jbe@1309 8470 currentStyle = element.currentStyle,
bsw/jbe@1309 8471 styleValue = style[camelizedProperty];
bsw/jbe@1309 8472 if (styleValue) {
bsw/jbe@1309 8473 return styleValue;
bsw/jbe@1309 8474 }
bsw/jbe@1309 8475
bsw/jbe@1309 8476 // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
bsw/jbe@1309 8477 // window.getComputedStyle, since it returns css property values in their original unit:
bsw/jbe@1309 8478 // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
bsw/jbe@1309 8479 // gives you the original "50%".
bsw/jbe@1309 8480 // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
bsw/jbe@1309 8481 if (currentStyle) {
bsw/jbe@1309 8482 try {
bsw/jbe@1309 8483 return currentStyle[camelizedProperty];
bsw/jbe@1309 8484 } catch(e) {
bsw/jbe@1309 8485 //ie will occasionally fail for unknown reasons. swallowing exception
bsw/jbe@1309 8486 }
bsw/jbe@1309 8487 }
bsw/jbe@1309 8488
bsw/jbe@1309 8489 var win = doc.defaultView || doc.parentWindow,
bsw/jbe@1309 8490 needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
bsw/jbe@1309 8491 originalOverflow,
bsw/jbe@1309 8492 returnValue;
bsw/jbe@1309 8493
bsw/jbe@1309 8494 if (win.getComputedStyle) {
bsw/jbe@1309 8495 // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
bsw/jbe@1309 8496 // therfore we remove and restore the scrollbar and calculate the value in between
bsw/jbe@1309 8497 if (needsOverflowReset) {
bsw/jbe@1309 8498 originalOverflow = style.overflow;
bsw/jbe@1309 8499 style.overflow = "hidden";
bsw/jbe@1309 8500 }
bsw/jbe@1309 8501 returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
bsw/jbe@1309 8502 if (needsOverflowReset) {
bsw/jbe@1309 8503 style.overflow = originalOverflow || "";
bsw/jbe@1309 8504 }
bsw/jbe@1309 8505 return returnValue;
bsw/jbe@1309 8506 }
bsw/jbe@1309 8507 }
bsw/jbe@1309 8508 };
bsw/jbe@1309 8509 };
bsw/jbe@1309 8510 })();
bsw/jbe@1309 8511
bsw/jbe@1309 8512 wysihtml.dom.getTextNodes = function(node, ingoreEmpty){
bsw/jbe@1309 8513 var all = [];
bsw/jbe@1309 8514 for (node=node.firstChild;node;node=node.nextSibling){
bsw/jbe@1309 8515 if (node.nodeType == 3) {
bsw/jbe@1309 8516 if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
bsw/jbe@1309 8517 all.push(node);
bsw/jbe@1309 8518 }
bsw/jbe@1309 8519 } else {
bsw/jbe@1309 8520 all = all.concat(wysihtml.dom.getTextNodes(node, ingoreEmpty));
bsw/jbe@1309 8521 }
bsw/jbe@1309 8522 }
bsw/jbe@1309 8523 return all;
bsw/jbe@1309 8524 };
bsw/jbe@1309 8525
bsw/jbe@1309 8526 /**
bsw/jbe@1309 8527 * High performant way to check whether an element with a specific class name is in the given document
bsw/jbe@1309 8528 * Optimized for being heavily executed
bsw/jbe@1309 8529 * Unleashes the power of live node lists
bsw/jbe@1309 8530 *
bsw/jbe@1309 8531 * @param {Object} doc The document object of the context where to check
bsw/jbe@1309 8532 * @param {String} tagName Upper cased tag name
bsw/jbe@1309 8533 * @example
bsw/jbe@1309 8534 * wysihtml.dom.hasElementWithClassName(document, "foobar");
bsw/jbe@1309 8535 */
bsw/jbe@1309 8536 (function(wysihtml) {
bsw/jbe@1309 8537 var LIVE_CACHE = {},
bsw/jbe@1309 8538 DOCUMENT_IDENTIFIER = 1;
bsw/jbe@1309 8539
bsw/jbe@1309 8540 function _getDocumentIdentifier(doc) {
bsw/jbe@1309 8541 return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
bsw/jbe@1309 8542 }
bsw/jbe@1309 8543
bsw/jbe@1309 8544 wysihtml.dom.hasElementWithClassName = function(doc, className) {
bsw/jbe@1309 8545 // getElementsByClassName is not supported by IE<9
bsw/jbe@1309 8546 // but is sometimes mocked via library code (which then doesn't return live node lists)
bsw/jbe@1309 8547 if (!wysihtml.browser.supportsNativeGetElementsByClassName()) {
bsw/jbe@1309 8548 return !!doc.querySelector("." + className);
bsw/jbe@1309 8549 }
bsw/jbe@1309 8550
bsw/jbe@1309 8551 var key = _getDocumentIdentifier(doc) + ":" + className,
bsw/jbe@1309 8552 cacheEntry = LIVE_CACHE[key];
bsw/jbe@1309 8553 if (!cacheEntry) {
bsw/jbe@1309 8554 cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
bsw/jbe@1309 8555 }
bsw/jbe@1309 8556
bsw/jbe@1309 8557 return cacheEntry.length > 0;
bsw/jbe@1309 8558 };
bsw/jbe@1309 8559 })(wysihtml);
bsw/jbe@1309 8560
bsw/jbe@1309 8561 /**
bsw/jbe@1309 8562 * High performant way to check whether an element with a specific tag name is in the given document
bsw/jbe@1309 8563 * Optimized for being heavily executed
bsw/jbe@1309 8564 * Unleashes the power of live node lists
bsw/jbe@1309 8565 *
bsw/jbe@1309 8566 * @param {Object} doc The document object of the context where to check
bsw/jbe@1309 8567 * @param {String} tagName Upper cased tag name
bsw/jbe@1309 8568 * @example
bsw/jbe@1309 8569 * wysihtml.dom.hasElementWithTagName(document, "IMG");
bsw/jbe@1309 8570 */
bsw/jbe@1309 8571 wysihtml.dom.hasElementWithTagName = (function() {
bsw/jbe@1309 8572 var LIVE_CACHE = {},
bsw/jbe@1309 8573 DOCUMENT_IDENTIFIER = 1;
bsw/jbe@1309 8574
bsw/jbe@1309 8575 function _getDocumentIdentifier(doc) {
bsw/jbe@1309 8576 return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
bsw/jbe@1309 8577 }
bsw/jbe@1309 8578
bsw/jbe@1309 8579 return function(doc, tagName) {
bsw/jbe@1309 8580 var key = _getDocumentIdentifier(doc) + ":" + tagName,
bsw/jbe@1309 8581 cacheEntry = LIVE_CACHE[key];
bsw/jbe@1309 8582 if (!cacheEntry) {
bsw/jbe@1309 8583 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
bsw/jbe@1309 8584 }
bsw/jbe@1309 8585
bsw/jbe@1309 8586 return cacheEntry.length > 0;
bsw/jbe@1309 8587 };
bsw/jbe@1309 8588 })();
bsw/jbe@1309 8589
bsw/jbe@1309 8590 wysihtml.dom.insert = function(elementToInsert) {
bsw/jbe@1309 8591 return {
bsw/jbe@1309 8592 after: function(element) {
bsw/jbe@1309 8593 element.parentNode.insertBefore(elementToInsert, element.nextSibling);
bsw/jbe@1309 8594 },
bsw/jbe@1309 8595
bsw/jbe@1309 8596 before: function(element) {
bsw/jbe@1309 8597 element.parentNode.insertBefore(elementToInsert, element);
bsw/jbe@1309 8598 },
bsw/jbe@1309 8599
bsw/jbe@1309 8600 into: function(element) {
bsw/jbe@1309 8601 element.appendChild(elementToInsert);
bsw/jbe@1309 8602 }
bsw/jbe@1309 8603 };
bsw/jbe@1309 8604 };
bsw/jbe@1309 8605
bsw/jbe@1309 8606 wysihtml.dom.insertCSS = function(rules) {
bsw/jbe@1309 8607 rules = rules.join("\n");
bsw/jbe@1309 8608
bsw/jbe@1309 8609 return {
bsw/jbe@1309 8610 into: function(doc) {
bsw/jbe@1309 8611 var styleElement = doc.createElement("style");
bsw/jbe@1309 8612 styleElement.type = "text/css";
bsw/jbe@1309 8613
bsw/jbe@1309 8614 if (styleElement.styleSheet) {
bsw/jbe@1309 8615 styleElement.styleSheet.cssText = rules;
bsw/jbe@1309 8616 } else {
bsw/jbe@1309 8617 styleElement.appendChild(doc.createTextNode(rules));
bsw/jbe@1309 8618 }
bsw/jbe@1309 8619
bsw/jbe@1309 8620 var link = doc.querySelector("head link");
bsw/jbe@1309 8621 if (link) {
bsw/jbe@1309 8622 link.parentNode.insertBefore(styleElement, link);
bsw/jbe@1309 8623 return;
bsw/jbe@1309 8624 } else {
bsw/jbe@1309 8625 var head = doc.querySelector("head");
bsw/jbe@1309 8626 if (head) {
bsw/jbe@1309 8627 head.appendChild(styleElement);
bsw/jbe@1309 8628 }
bsw/jbe@1309 8629 }
bsw/jbe@1309 8630 }
bsw/jbe@1309 8631 };
bsw/jbe@1309 8632 };
bsw/jbe@1309 8633
bsw/jbe@1309 8634 /**
bsw/jbe@1309 8635 * Check whether the given node is a proper loaded image
bsw/jbe@1309 8636 * FIXME: Returns undefined when unknown (Chrome, Safari)
bsw/jbe@1309 8637 */
bsw/jbe@1309 8638
bsw/jbe@1309 8639 wysihtml.dom.isLoadedImage = function (node) {
bsw/jbe@1309 8640 try {
bsw/jbe@1309 8641 return node.complete && !node.mozMatchesSelector(":-moz-broken");
bsw/jbe@1309 8642 } catch(e) {
bsw/jbe@1309 8643 if (node.complete && node.readyState === "complete") {
bsw/jbe@1309 8644 return true;
bsw/jbe@1309 8645 }
bsw/jbe@1309 8646 }
bsw/jbe@1309 8647 };
bsw/jbe@1309 8648
bsw/jbe@1309 8649 // TODO: Refactor dom tree traversing here
bsw/jbe@1309 8650 (function(wysihtml) {
bsw/jbe@1309 8651 wysihtml.dom.lineBreaks = function(node) {
bsw/jbe@1309 8652
bsw/jbe@1309 8653 function _isLineBreak(n) {
bsw/jbe@1309 8654 return n.nodeName === "BR";
bsw/jbe@1309 8655 }
bsw/jbe@1309 8656
bsw/jbe@1309 8657 /**
bsw/jbe@1309 8658 * Checks whether the elment causes a visual line break
bsw/jbe@1309 8659 * (<br> or block elements)
bsw/jbe@1309 8660 */
bsw/jbe@1309 8661 function _isLineBreakOrBlockElement(element) {
bsw/jbe@1309 8662 if (_isLineBreak(element)) {
bsw/jbe@1309 8663 return true;
bsw/jbe@1309 8664 }
bsw/jbe@1309 8665
bsw/jbe@1309 8666 if (wysihtml.dom.getStyle("display").from(element) === "block") {
bsw/jbe@1309 8667 return true;
bsw/jbe@1309 8668 }
bsw/jbe@1309 8669
bsw/jbe@1309 8670 return false;
bsw/jbe@1309 8671 }
bsw/jbe@1309 8672
bsw/jbe@1309 8673 return {
bsw/jbe@1309 8674
bsw/jbe@1309 8675 /* wysihtml.dom.lineBreaks(element).add();
bsw/jbe@1309 8676 *
bsw/jbe@1309 8677 * Adds line breaks before and after the given node if the previous and next siblings
bsw/jbe@1309 8678 * aren't already causing a visual line break (block element or <br>)
bsw/jbe@1309 8679 */
bsw/jbe@1309 8680 add: function(options) {
bsw/jbe@1309 8681 var doc = node.ownerDocument,
bsw/jbe@1309 8682 nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
bsw/jbe@1309 8683 previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
bsw/jbe@1309 8684
bsw/jbe@1309 8685 if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
bsw/jbe@1309 8686 wysihtml.dom.insert(doc.createElement("br")).after(node);
bsw/jbe@1309 8687 }
bsw/jbe@1309 8688 if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
bsw/jbe@1309 8689 wysihtml.dom.insert(doc.createElement("br")).before(node);
bsw/jbe@1309 8690 }
bsw/jbe@1309 8691 },
bsw/jbe@1309 8692
bsw/jbe@1309 8693 /* wysihtml.dom.lineBreaks(element).remove();
bsw/jbe@1309 8694 *
bsw/jbe@1309 8695 * Removes line breaks before and after the given node
bsw/jbe@1309 8696 */
bsw/jbe@1309 8697 remove: function(options) {
bsw/jbe@1309 8698 var nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
bsw/jbe@1309 8699 previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
bsw/jbe@1309 8700
bsw/jbe@1309 8701 if (nextSibling && _isLineBreak(nextSibling)) {
bsw/jbe@1309 8702 nextSibling.parentNode.removeChild(nextSibling);
bsw/jbe@1309 8703 }
bsw/jbe@1309 8704 if (previousSibling && _isLineBreak(previousSibling)) {
bsw/jbe@1309 8705 previousSibling.parentNode.removeChild(previousSibling);
bsw/jbe@1309 8706 }
bsw/jbe@1309 8707 }
bsw/jbe@1309 8708 };
bsw/jbe@1309 8709 };
bsw/jbe@1309 8710 })(wysihtml);
bsw/jbe@1309 8711 /**
bsw/jbe@1309 8712 * Method to set dom events
bsw/jbe@1309 8713 *
bsw/jbe@1309 8714 * @example
bsw/jbe@1309 8715 * wysihtml.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
bsw/jbe@1309 8716 */
bsw/jbe@1309 8717 wysihtml.dom.observe = function(element, eventNames, handler) {
bsw/jbe@1309 8718 eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
bsw/jbe@1309 8719
bsw/jbe@1309 8720 var handlerWrapper,
bsw/jbe@1309 8721 eventName,
bsw/jbe@1309 8722 i = 0,
bsw/jbe@1309 8723 length = eventNames.length;
bsw/jbe@1309 8724
bsw/jbe@1309 8725 for (; i<length; i++) {
bsw/jbe@1309 8726 eventName = eventNames[i];
bsw/jbe@1309 8727 if (element.addEventListener) {
bsw/jbe@1309 8728 element.addEventListener(eventName, handler, false);
bsw/jbe@1309 8729 } else {
bsw/jbe@1309 8730 handlerWrapper = function(event) {
bsw/jbe@1309 8731 if (!("target" in event)) {
bsw/jbe@1309 8732 event.target = event.srcElement;
bsw/jbe@1309 8733 }
bsw/jbe@1309 8734 event.preventDefault = event.preventDefault || function() {
bsw/jbe@1309 8735 this.returnValue = false;
bsw/jbe@1309 8736 };
bsw/jbe@1309 8737 event.stopPropagation = event.stopPropagation || function() {
bsw/jbe@1309 8738 this.cancelBubble = true;
bsw/jbe@1309 8739 };
bsw/jbe@1309 8740 handler.call(element, event);
bsw/jbe@1309 8741 };
bsw/jbe@1309 8742 element.attachEvent("on" + eventName, handlerWrapper);
bsw/jbe@1309 8743 }
bsw/jbe@1309 8744 }
bsw/jbe@1309 8745
bsw/jbe@1309 8746 return {
bsw/jbe@1309 8747 stop: function() {
bsw/jbe@1309 8748 var eventName,
bsw/jbe@1309 8749 i = 0,
bsw/jbe@1309 8750 length = eventNames.length;
bsw/jbe@1309 8751 for (; i<length; i++) {
bsw/jbe@1309 8752 eventName = eventNames[i];
bsw/jbe@1309 8753 if (element.removeEventListener) {
bsw/jbe@1309 8754 element.removeEventListener(eventName, handler, false);
bsw/jbe@1309 8755 } else {
bsw/jbe@1309 8756 element.detachEvent("on" + eventName, handlerWrapper);
bsw/jbe@1309 8757 }
bsw/jbe@1309 8758 }
bsw/jbe@1309 8759 }
bsw/jbe@1309 8760 };
bsw/jbe@1309 8761 };
bsw/jbe@1309 8762
bsw/jbe@1309 8763 /**
bsw/jbe@1309 8764 * HTML Sanitizer
bsw/jbe@1309 8765 * Rewrites the HTML based on given rules
bsw/jbe@1309 8766 *
bsw/jbe@1309 8767 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
bsw/jbe@1309 8768 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
bsw/jbe@1309 8769 * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
bsw/jbe@1309 8770 * desired substitution.
bsw/jbe@1309 8771 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
bsw/jbe@1309 8772 *
bsw/jbe@1309 8773 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
bsw/jbe@1309 8774 *
bsw/jbe@1309 8775 * @example
bsw/jbe@1309 8776 * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
bsw/jbe@1309 8777 * wysihtml.dom.parse(userHTML, {
bsw/jbe@1309 8778 * tags {
bsw/jbe@1309 8779 * p: "div", // Rename p tags to div tags
bsw/jbe@1309 8780 * font: "span" // Rename font tags to span tags
bsw/jbe@1309 8781 * div: true, // Keep them, also possible (same result when passing: "div" or true)
bsw/jbe@1309 8782 * script: undefined // Remove script elements
bsw/jbe@1309 8783 * }
bsw/jbe@1309 8784 * });
bsw/jbe@1309 8785 * // => <div><div><span>foo bar</span></div></div>
bsw/jbe@1309 8786 *
bsw/jbe@1309 8787 * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
bsw/jbe@1309 8788 * wysihtml.dom.parse(userHTML);
bsw/jbe@1309 8789 * // => '<span><span><span><span>I'm a table!</span></span></span></span>'
bsw/jbe@1309 8790 *
bsw/jbe@1309 8791 * var userHTML = '<div>foobar<br>foobar</div>';
bsw/jbe@1309 8792 * wysihtml.dom.parse(userHTML, {
bsw/jbe@1309 8793 * tags: {
bsw/jbe@1309 8794 * div: undefined,
bsw/jbe@1309 8795 * br: true
bsw/jbe@1309 8796 * }
bsw/jbe@1309 8797 * });
bsw/jbe@1309 8798 * // => ''
bsw/jbe@1309 8799 *
bsw/jbe@1309 8800 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
bsw/jbe@1309 8801 * wysihtml.dom.parse(userHTML, {
bsw/jbe@1309 8802 * classes: {
bsw/jbe@1309 8803 * red: 1,
bsw/jbe@1309 8804 * green: 1
bsw/jbe@1309 8805 * },
bsw/jbe@1309 8806 * tags: {
bsw/jbe@1309 8807 * div: {
bsw/jbe@1309 8808 * rename_tag: "p"
bsw/jbe@1309 8809 * }
bsw/jbe@1309 8810 * }
bsw/jbe@1309 8811 * });
bsw/jbe@1309 8812 * // => '<p class="red">foo</p><p>bar</p>'
bsw/jbe@1309 8813 */
bsw/jbe@1309 8814
bsw/jbe@1309 8815 wysihtml.dom.parse = function(elementOrHtml_current, config_current) {
bsw/jbe@1309 8816 /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors.
bsw/jbe@1309 8817 * Refactor whole code as this method while workind is kind of awkward too */
bsw/jbe@1309 8818
bsw/jbe@1309 8819 /**
bsw/jbe@1309 8820 * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
bsw/jbe@1309 8821 * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
bsw/jbe@1309 8822 * node isn't closed
bsw/jbe@1309 8823 *
bsw/jbe@1309 8824 * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
bsw/jbe@1309 8825 */
bsw/jbe@1309 8826 var NODE_TYPE_MAPPING = {
bsw/jbe@1309 8827 "1": _handleElement,
bsw/jbe@1309 8828 "3": _handleText,
bsw/jbe@1309 8829 "8": _handleComment
bsw/jbe@1309 8830 },
bsw/jbe@1309 8831 // Rename unknown tags to this
bsw/jbe@1309 8832 DEFAULT_NODE_NAME = "span",
bsw/jbe@1309 8833 WHITE_SPACE_REG_EXP = /\s+/,
bsw/jbe@1309 8834 defaultRules = { tags: {}, classes: {} },
bsw/jbe@1309 8835 currentRules = {},
bsw/jbe@1309 8836 blockElements = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" ,
bsw/jbe@1309 8837 "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU",
bsw/jbe@1309 8838 "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"];
bsw/jbe@1309 8839
bsw/jbe@1309 8840 /**
bsw/jbe@1309 8841 * Iterates over all childs of the element, recreates them, appends them into a document fragment
bsw/jbe@1309 8842 * which later replaces the entire body content
bsw/jbe@1309 8843 */
bsw/jbe@1309 8844 function parse(elementOrHtml, config) {
bsw/jbe@1309 8845 wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
bsw/jbe@1309 8846
bsw/jbe@1309 8847 var context = config.context || elementOrHtml.ownerDocument || document,
bsw/jbe@1309 8848 fragment = context.createDocumentFragment(),
bsw/jbe@1309 8849 isString = typeof(elementOrHtml) === "string",
bsw/jbe@1309 8850 clearInternals = false,
bsw/jbe@1309 8851 element,
bsw/jbe@1309 8852 newNode,
bsw/jbe@1309 8853 firstChild;
bsw/jbe@1309 8854
bsw/jbe@1309 8855 if (config.clearInternals === true) {
bsw/jbe@1309 8856 clearInternals = true;
bsw/jbe@1309 8857 }
bsw/jbe@1309 8858
bsw/jbe@1309 8859 if (isString) {
bsw/jbe@1309 8860 element = wysihtml.dom.getAsDom(elementOrHtml, context);
bsw/jbe@1309 8861 } else {
bsw/jbe@1309 8862 element = elementOrHtml;
bsw/jbe@1309 8863 }
bsw/jbe@1309 8864
bsw/jbe@1309 8865 if (currentRules.selectors) {
bsw/jbe@1309 8866 _applySelectorRules(element, currentRules.selectors);
bsw/jbe@1309 8867 }
bsw/jbe@1309 8868
bsw/jbe@1309 8869 while (element.firstChild) {
bsw/jbe@1309 8870 firstChild = element.firstChild;
bsw/jbe@1309 8871 newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
bsw/jbe@1309 8872 if (newNode) {
bsw/jbe@1309 8873 fragment.appendChild(newNode);
bsw/jbe@1309 8874 }
bsw/jbe@1309 8875 if (firstChild !== newNode) {
bsw/jbe@1309 8876 element.removeChild(firstChild);
bsw/jbe@1309 8877 }
bsw/jbe@1309 8878 }
bsw/jbe@1309 8879
bsw/jbe@1309 8880 if (config.unjoinNbsps) {
bsw/jbe@1309 8881 // replace joined non-breakable spaces with unjoined
bsw/jbe@1309 8882 var txtnodes = wysihtml.dom.getTextNodes(fragment);
bsw/jbe@1309 8883 for (var n = txtnodes.length; n--;) {
bsw/jbe@1309 8884 txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
bsw/jbe@1309 8885 }
bsw/jbe@1309 8886 }
bsw/jbe@1309 8887
bsw/jbe@1309 8888 // Clear element contents
bsw/jbe@1309 8889 element.innerHTML = "";
bsw/jbe@1309 8890
bsw/jbe@1309 8891 // Insert new DOM tree
bsw/jbe@1309 8892 element.appendChild(fragment);
bsw/jbe@1309 8893
bsw/jbe@1309 8894 return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element;
bsw/jbe@1309 8895 }
bsw/jbe@1309 8896
bsw/jbe@1309 8897 function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
bsw/jbe@1309 8898 var oldNodeType = oldNode.nodeType,
bsw/jbe@1309 8899 oldChilds = oldNode.childNodes,
bsw/jbe@1309 8900 oldChildsLength = oldChilds.length,
bsw/jbe@1309 8901 method = NODE_TYPE_MAPPING[oldNodeType],
bsw/jbe@1309 8902 i = 0,
bsw/jbe@1309 8903 fragment,
bsw/jbe@1309 8904 newNode,
bsw/jbe@1309 8905 newChild,
bsw/jbe@1309 8906 nodeDisplay;
bsw/jbe@1309 8907
bsw/jbe@1309 8908 // Passes directly elemets with uneditable class
bsw/jbe@1309 8909 if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) {
bsw/jbe@1309 8910 return oldNode;
bsw/jbe@1309 8911 }
bsw/jbe@1309 8912
bsw/jbe@1309 8913 newNode = method && method(oldNode, clearInternals);
bsw/jbe@1309 8914
bsw/jbe@1309 8915 // Remove or unwrap node in case of return value null or false
bsw/jbe@1309 8916 if (!newNode) {
bsw/jbe@1309 8917 if (newNode === false) {
bsw/jbe@1309 8918 // false defines that tag should be removed but contents should remain (unwrap)
bsw/jbe@1309 8919 fragment = oldNode.ownerDocument.createDocumentFragment();
bsw/jbe@1309 8920
bsw/jbe@1309 8921 for (i = oldChildsLength; i--;) {
bsw/jbe@1309 8922 if (oldChilds[i]) {
bsw/jbe@1309 8923 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
bsw/jbe@1309 8924 if (newChild) {
bsw/jbe@1309 8925 if (oldChilds[i] === newChild) {
bsw/jbe@1309 8926 i--;
bsw/jbe@1309 8927 }
bsw/jbe@1309 8928 fragment.insertBefore(newChild, fragment.firstChild);
bsw/jbe@1309 8929 }
bsw/jbe@1309 8930 }
bsw/jbe@1309 8931 }
bsw/jbe@1309 8932
bsw/jbe@1309 8933 nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode);
bsw/jbe@1309 8934
bsw/jbe@1309 8935 if (nodeDisplay === '') {
bsw/jbe@1309 8936 // Handle display style when element not in dom
bsw/jbe@1309 8937 nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : "";
bsw/jbe@1309 8938 }
bsw/jbe@1309 8939 if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) {
bsw/jbe@1309 8940 fragment.appendChild(oldNode.ownerDocument.createElement("br"));
bsw/jbe@1309 8941 }
bsw/jbe@1309 8942
bsw/jbe@1309 8943 // TODO: try to minimize surplus spaces
bsw/jbe@1309 8944 if (wysihtml.lang.array([
bsw/jbe@1309 8945 "div", "pre", "p",
bsw/jbe@1309 8946 "table", "td", "th",
bsw/jbe@1309 8947 "ul", "ol", "li",
bsw/jbe@1309 8948 "dd", "dl",
bsw/jbe@1309 8949 "footer", "header", "section",
bsw/jbe@1309 8950 "h1", "h2", "h3", "h4", "h5", "h6"
bsw/jbe@1309 8951 ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
bsw/jbe@1309 8952 // add space at first when unwraping non-textflow elements
bsw/jbe@1309 8953 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
bsw/jbe@1309 8954 fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
bsw/jbe@1309 8955 }
bsw/jbe@1309 8956 }
bsw/jbe@1309 8957
bsw/jbe@1309 8958 if (fragment.normalize) {
bsw/jbe@1309 8959 fragment.normalize();
bsw/jbe@1309 8960 }
bsw/jbe@1309 8961 return fragment;
bsw/jbe@1309 8962 } else {
bsw/jbe@1309 8963 // Remove
bsw/jbe@1309 8964 return null;
bsw/jbe@1309 8965 }
bsw/jbe@1309 8966 }
bsw/jbe@1309 8967
bsw/jbe@1309 8968 // Converts all childnodes
bsw/jbe@1309 8969 for (i=0; i<oldChildsLength; i++) {
bsw/jbe@1309 8970 if (oldChilds[i]) {
bsw/jbe@1309 8971 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
bsw/jbe@1309 8972 if (newChild) {
bsw/jbe@1309 8973 if (oldChilds[i] === newChild) {
bsw/jbe@1309 8974 i--;
bsw/jbe@1309 8975 }
bsw/jbe@1309 8976 newNode.appendChild(newChild);
bsw/jbe@1309 8977 }
bsw/jbe@1309 8978 }
bsw/jbe@1309 8979 }
bsw/jbe@1309 8980
bsw/jbe@1309 8981 // Cleanup senseless <span> elements
bsw/jbe@1309 8982 if (cleanUp &&
bsw/jbe@1309 8983 newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
bsw/jbe@1309 8984 (!newNode.childNodes.length ||
bsw/jbe@1309 8985 ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) ||
bsw/jbe@1309 8986 !newNode.attributes.length)
bsw/jbe@1309 8987 ) {
bsw/jbe@1309 8988 fragment = newNode.ownerDocument.createDocumentFragment();
bsw/jbe@1309 8989 while (newNode.firstChild) {
bsw/jbe@1309 8990 fragment.appendChild(newNode.firstChild);
bsw/jbe@1309 8991 }
bsw/jbe@1309 8992 if (fragment.normalize) {
bsw/jbe@1309 8993 fragment.normalize();
bsw/jbe@1309 8994 }
bsw/jbe@1309 8995 return fragment;
bsw/jbe@1309 8996 }
bsw/jbe@1309 8997
bsw/jbe@1309 8998 if (newNode.normalize) {
bsw/jbe@1309 8999 newNode.normalize();
bsw/jbe@1309 9000 }
bsw/jbe@1309 9001 return newNode;
bsw/jbe@1309 9002 }
bsw/jbe@1309 9003
bsw/jbe@1309 9004 function _applySelectorRules (element, selectorRules) {
bsw/jbe@1309 9005 var sel, method, els;
bsw/jbe@1309 9006
bsw/jbe@1309 9007 for (sel in selectorRules) {
bsw/jbe@1309 9008 if (selectorRules.hasOwnProperty(sel)) {
bsw/jbe@1309 9009 if (wysihtml.lang.object(selectorRules[sel]).isFunction()) {
bsw/jbe@1309 9010 method = selectorRules[sel];
bsw/jbe@1309 9011 } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) {
bsw/jbe@1309 9012 method = elementHandlingMethods[selectorRules[sel]];
bsw/jbe@1309 9013 }
bsw/jbe@1309 9014 els = element.querySelectorAll(sel);
bsw/jbe@1309 9015 for (var i = els.length; i--;) {
bsw/jbe@1309 9016 method(els[i]);
bsw/jbe@1309 9017 }
bsw/jbe@1309 9018 }
bsw/jbe@1309 9019 }
bsw/jbe@1309 9020 }
bsw/jbe@1309 9021
bsw/jbe@1309 9022 function _handleElement(oldNode, clearInternals) {
bsw/jbe@1309 9023 var rule,
bsw/jbe@1309 9024 newNode,
bsw/jbe@1309 9025 tagRules = currentRules.tags,
bsw/jbe@1309 9026 nodeName = oldNode.nodeName.toLowerCase(),
bsw/jbe@1309 9027 scopeName = oldNode.scopeName,
bsw/jbe@1309 9028 renameTag;
bsw/jbe@1309 9029
bsw/jbe@1309 9030 /**
bsw/jbe@1309 9031 * We already parsed that element
bsw/jbe@1309 9032 * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
bsw/jbe@1309 9033 */
bsw/jbe@1309 9034 if (oldNode._wysihtml) {
bsw/jbe@1309 9035 return null;
bsw/jbe@1309 9036 }
bsw/jbe@1309 9037 oldNode._wysihtml = 1;
bsw/jbe@1309 9038
bsw/jbe@1309 9039 if (oldNode.className === "wysihtml-temp") {
bsw/jbe@1309 9040 return null;
bsw/jbe@1309 9041 }
bsw/jbe@1309 9042
bsw/jbe@1309 9043 /**
bsw/jbe@1309 9044 * IE is the only browser who doesn't include the namespace in the
bsw/jbe@1309 9045 * nodeName, that's why we have to prepend it by ourselves
bsw/jbe@1309 9046 * scopeName is a proprietary IE feature
bsw/jbe@1309 9047 * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
bsw/jbe@1309 9048 */
bsw/jbe@1309 9049 if (scopeName && scopeName != "HTML") {
bsw/jbe@1309 9050 nodeName = scopeName + ":" + nodeName;
bsw/jbe@1309 9051 }
bsw/jbe@1309 9052 /**
bsw/jbe@1309 9053 * Repair node
bsw/jbe@1309 9054 * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
bsw/jbe@1309 9055 * 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
bsw/jbe@1309 9056 */
bsw/jbe@1309 9057 if ("outerHTML" in oldNode) {
bsw/jbe@1309 9058 if (!wysihtml.browser.autoClosesUnclosedTags() &&
bsw/jbe@1309 9059 oldNode.nodeName === "P" &&
bsw/jbe@1309 9060 oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
bsw/jbe@1309 9061 nodeName = "div";
bsw/jbe@1309 9062 }
bsw/jbe@1309 9063 }
bsw/jbe@1309 9064
bsw/jbe@1309 9065 if (nodeName in tagRules) {
bsw/jbe@1309 9066 rule = tagRules[nodeName];
bsw/jbe@1309 9067 if (!rule || rule.remove) {
bsw/jbe@1309 9068 return null;
bsw/jbe@1309 9069 } else if (rule.unwrap) {
bsw/jbe@1309 9070 return false;
bsw/jbe@1309 9071 }
bsw/jbe@1309 9072 rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
bsw/jbe@1309 9073 } else if (oldNode.firstChild) {
bsw/jbe@1309 9074 rule = { rename_tag: DEFAULT_NODE_NAME };
bsw/jbe@1309 9075 } else {
bsw/jbe@1309 9076 // Remove empty unknown elements
bsw/jbe@1309 9077 return null;
bsw/jbe@1309 9078 }
bsw/jbe@1309 9079
bsw/jbe@1309 9080 // tests if type condition is met or node should be removed/unwrapped/renamed
bsw/jbe@1309 9081 if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) {
bsw/jbe@1309 9082 if (rule.remove_action) {
bsw/jbe@1309 9083 if (rule.remove_action === "unwrap") {
bsw/jbe@1309 9084 return false;
bsw/jbe@1309 9085 } else if (rule.remove_action === "rename") {
bsw/jbe@1309 9086 renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
bsw/jbe@1309 9087 } else {
bsw/jbe@1309 9088 return null;
bsw/jbe@1309 9089 }
bsw/jbe@1309 9090 } else {
bsw/jbe@1309 9091 return null;
bsw/jbe@1309 9092 }
bsw/jbe@1309 9093 }
bsw/jbe@1309 9094
bsw/jbe@1309 9095 newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
bsw/jbe@1309 9096 _handleAttributes(oldNode, newNode, rule, clearInternals);
bsw/jbe@1309 9097 _handleStyles(oldNode, newNode, rule);
bsw/jbe@1309 9098
bsw/jbe@1309 9099 oldNode = null;
bsw/jbe@1309 9100
bsw/jbe@1309 9101 if (newNode.normalize) { newNode.normalize(); }
bsw/jbe@1309 9102 return newNode;
bsw/jbe@1309 9103 }
bsw/jbe@1309 9104
bsw/jbe@1309 9105 function _testTypes(oldNode, rules, types, clearInternals) {
bsw/jbe@1309 9106 var definition, type;
bsw/jbe@1309 9107
bsw/jbe@1309 9108 // do not interfere with placeholder span or pasting caret position is not maintained
bsw/jbe@1309 9109 if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) {
bsw/jbe@1309 9110 return true;
bsw/jbe@1309 9111 }
bsw/jbe@1309 9112
bsw/jbe@1309 9113 for (type in types) {
bsw/jbe@1309 9114 if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) {
bsw/jbe@1309 9115 definition = rules.type_definitions[type];
bsw/jbe@1309 9116 if (_testType(oldNode, definition)) {
bsw/jbe@1309 9117 return true;
bsw/jbe@1309 9118 }
bsw/jbe@1309 9119 }
bsw/jbe@1309 9120 }
bsw/jbe@1309 9121 return false;
bsw/jbe@1309 9122 }
bsw/jbe@1309 9123
bsw/jbe@1309 9124 function array_contains(a, obj) {
bsw/jbe@1309 9125 var i = a.length;
bsw/jbe@1309 9126 while (i--) {
bsw/jbe@1309 9127 if (a[i] === obj) {
bsw/jbe@1309 9128 return true;
bsw/jbe@1309 9129 }
bsw/jbe@1309 9130 }
bsw/jbe@1309 9131 return false;
bsw/jbe@1309 9132 }
bsw/jbe@1309 9133
bsw/jbe@1309 9134 function _testType(oldNode, definition) {
bsw/jbe@1309 9135
bsw/jbe@1309 9136 var nodeClasses = oldNode.getAttribute("class"),
bsw/jbe@1309 9137 nodeStyles = oldNode.getAttribute("style"),
bsw/jbe@1309 9138 classesLength, s, s_corrected, a, attr, currentClass, styleProp;
bsw/jbe@1309 9139
bsw/jbe@1309 9140 // test for methods
bsw/jbe@1309 9141 if (definition.methods) {
bsw/jbe@1309 9142 for (var m in definition.methods) {
bsw/jbe@1309 9143 if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
bsw/jbe@1309 9144
bsw/jbe@1309 9145 if (typeCeckMethods[m](oldNode)) {
bsw/jbe@1309 9146 return true;
bsw/jbe@1309 9147 }
bsw/jbe@1309 9148 }
bsw/jbe@1309 9149 }
bsw/jbe@1309 9150 }
bsw/jbe@1309 9151
bsw/jbe@1309 9152 // test for classes, if one found return true
bsw/jbe@1309 9153 if (nodeClasses && definition.classes) {
bsw/jbe@1309 9154 nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP);
bsw/jbe@1309 9155 classesLength = nodeClasses.length;
bsw/jbe@1309 9156 for (var i = 0; i < classesLength; i++) {
bsw/jbe@1309 9157 if (definition.classes[nodeClasses[i]]) {
bsw/jbe@1309 9158 return true;
bsw/jbe@1309 9159 }
bsw/jbe@1309 9160 }
bsw/jbe@1309 9161 }
bsw/jbe@1309 9162
bsw/jbe@1309 9163 // test for styles, if one found return true
bsw/jbe@1309 9164 if (nodeStyles && definition.styles) {
bsw/jbe@1309 9165
bsw/jbe@1309 9166 nodeStyles = nodeStyles.split(';');
bsw/jbe@1309 9167 for (s in definition.styles) {
bsw/jbe@1309 9168 if (definition.styles.hasOwnProperty(s)) {
bsw/jbe@1309 9169 for (var sp = nodeStyles.length; sp--;) {
bsw/jbe@1309 9170 styleProp = nodeStyles[sp].split(':');
bsw/jbe@1309 9171
bsw/jbe@1309 9172 if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) {
bsw/jbe@1309 9173 if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) {
bsw/jbe@1309 9174 return true;
bsw/jbe@1309 9175 }
bsw/jbe@1309 9176 }
bsw/jbe@1309 9177 }
bsw/jbe@1309 9178 }
bsw/jbe@1309 9179 }
bsw/jbe@1309 9180 }
bsw/jbe@1309 9181
bsw/jbe@1309 9182 // test for attributes in general against regex match
bsw/jbe@1309 9183 if (definition.attrs) {
bsw/jbe@1309 9184 for (a in definition.attrs) {
bsw/jbe@1309 9185 if (definition.attrs.hasOwnProperty(a)) {
bsw/jbe@1309 9186 attr = wysihtml.dom.getAttribute(oldNode, a);
bsw/jbe@1309 9187 if (typeof(attr) === "string") {
bsw/jbe@1309 9188 if (attr.search(definition.attrs[a]) > -1) {
bsw/jbe@1309 9189 return true;
bsw/jbe@1309 9190 }
bsw/jbe@1309 9191 }
bsw/jbe@1309 9192 }
bsw/jbe@1309 9193 }
bsw/jbe@1309 9194 }
bsw/jbe@1309 9195 return false;
bsw/jbe@1309 9196 }
bsw/jbe@1309 9197
bsw/jbe@1309 9198 function _handleStyles(oldNode, newNode, rule) {
bsw/jbe@1309 9199 var s, v;
bsw/jbe@1309 9200 if(rule && rule.keep_styles) {
bsw/jbe@1309 9201 for (s in rule.keep_styles) {
bsw/jbe@1309 9202 if (rule.keep_styles.hasOwnProperty(s)) {
bsw/jbe@1309 9203 v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s];
bsw/jbe@1309 9204 // value can be regex and if so should match or style skipped
bsw/jbe@1309 9205 if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) {
bsw/jbe@1309 9206 continue;
bsw/jbe@1309 9207 }
bsw/jbe@1309 9208 if (s === "float") {
bsw/jbe@1309 9209 // IE compability
bsw/jbe@1309 9210 newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
bsw/jbe@1309 9211 } else if (oldNode.style[s]) {
bsw/jbe@1309 9212 newNode.style[s] = v;
bsw/jbe@1309 9213 }
bsw/jbe@1309 9214 }
bsw/jbe@1309 9215 }
bsw/jbe@1309 9216 }
bsw/jbe@1309 9217 };
bsw/jbe@1309 9218
bsw/jbe@1309 9219 function _getAttributesBeginningWith(beginning, attributes) {
bsw/jbe@1309 9220 var returnAttributes = [];
bsw/jbe@1309 9221 for (var attr in attributes) {
bsw/jbe@1309 9222 if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) {
bsw/jbe@1309 9223 returnAttributes.push(attr);
bsw/jbe@1309 9224 }
bsw/jbe@1309 9225 }
bsw/jbe@1309 9226 return returnAttributes;
bsw/jbe@1309 9227 }
bsw/jbe@1309 9228
bsw/jbe@1309 9229 function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
bsw/jbe@1309 9230 var method = wysihtml.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName],
bsw/jbe@1309 9231 newAttributeValue;
bsw/jbe@1309 9232
bsw/jbe@1309 9233 if (method) {
bsw/jbe@1309 9234 newAttributeValue = method(attributeValue, nodeName);
bsw/jbe@1309 9235 if (typeof(newAttributeValue) === "string") {
bsw/jbe@1309 9236 return newAttributeValue;
bsw/jbe@1309 9237 }
bsw/jbe@1309 9238 }
bsw/jbe@1309 9239
bsw/jbe@1309 9240 return false;
bsw/jbe@1309 9241 }
bsw/jbe@1309 9242
bsw/jbe@1309 9243 function _checkAttributes(oldNode, local_attributes) {
bsw/jbe@1309 9244 var globalAttributes = wysihtml.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes
bsw/jbe@1309 9245 checkAttributes = wysihtml.lang.object(globalAttributes).merge( wysihtml.lang.object(local_attributes || {}).clone()).get(),
bsw/jbe@1309 9246 attributes = {},
bsw/jbe@1309 9247 oldAttributes = wysihtml.dom.getAttributes(oldNode),
bsw/jbe@1309 9248 attributeName, newValue, matchingAttributes;
bsw/jbe@1309 9249
bsw/jbe@1309 9250 for (attributeName in checkAttributes) {
bsw/jbe@1309 9251 if ((/\*$/).test(attributeName)) {
bsw/jbe@1309 9252
bsw/jbe@1309 9253 matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
bsw/jbe@1309 9254 for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
bsw/jbe@1309 9255
bsw/jbe@1309 9256 newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
bsw/jbe@1309 9257 if (newValue !== false) {
bsw/jbe@1309 9258 attributes[matchingAttributes[i]] = newValue;
bsw/jbe@1309 9259 }
bsw/jbe@1309 9260 }
bsw/jbe@1309 9261 } else {
bsw/jbe@1309 9262 newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
bsw/jbe@1309 9263 if (newValue !== false) {
bsw/jbe@1309 9264 attributes[attributeName] = newValue;
bsw/jbe@1309 9265 }
bsw/jbe@1309 9266 }
bsw/jbe@1309 9267 }
bsw/jbe@1309 9268
bsw/jbe@1309 9269 return attributes;
bsw/jbe@1309 9270 }
bsw/jbe@1309 9271
bsw/jbe@1309 9272 // TODO: refactor. Too long to read
bsw/jbe@1309 9273 function _handleAttributes(oldNode, newNode, rule, clearInternals) {
bsw/jbe@1309 9274 var attributes = {}, // fresh new set of attributes to set on newNode
bsw/jbe@1309 9275 setClass = rule.set_class, // classes to set
bsw/jbe@1309 9276 addClass = rule.add_class, // add classes based on existing attributes
bsw/jbe@1309 9277 addStyle = rule.add_style, // add styles based on existing attributes
bsw/jbe@1309 9278 setAttributes = rule.set_attributes, // attributes to set on the current node
bsw/jbe@1309 9279 allowedClasses = currentRules.classes,
bsw/jbe@1309 9280 i = 0,
bsw/jbe@1309 9281 classes = [],
bsw/jbe@1309 9282 styles = [],
bsw/jbe@1309 9283 newClasses = [],
bsw/jbe@1309 9284 oldClasses = [],
bsw/jbe@1309 9285 classesLength,
bsw/jbe@1309 9286 newClassesLength,
bsw/jbe@1309 9287 currentClass,
bsw/jbe@1309 9288 newClass,
bsw/jbe@1309 9289 attributeName,
bsw/jbe@1309 9290 method;
bsw/jbe@1309 9291
bsw/jbe@1309 9292 if (setAttributes) {
bsw/jbe@1309 9293 attributes = wysihtml.lang.object(setAttributes).clone();
bsw/jbe@1309 9294 }
bsw/jbe@1309 9295
bsw/jbe@1309 9296 // check/convert values of attributes
bsw/jbe@1309 9297 attributes = wysihtml.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get();
bsw/jbe@1309 9298
bsw/jbe@1309 9299 if (setClass) {
bsw/jbe@1309 9300 classes.push(setClass);
bsw/jbe@1309 9301 }
bsw/jbe@1309 9302
bsw/jbe@1309 9303 if (addClass) {
bsw/jbe@1309 9304 for (attributeName in addClass) {
bsw/jbe@1309 9305 method = addClassMethods[addClass[attributeName]];
bsw/jbe@1309 9306 if (!method) {
bsw/jbe@1309 9307 continue;
bsw/jbe@1309 9308 }
bsw/jbe@1309 9309 newClass = method(wysihtml.dom.getAttribute(oldNode, attributeName));
bsw/jbe@1309 9310 if (typeof(newClass) === "string") {
bsw/jbe@1309 9311 classes.push(newClass);
bsw/jbe@1309 9312 }
bsw/jbe@1309 9313 }
bsw/jbe@1309 9314 }
bsw/jbe@1309 9315
bsw/jbe@1309 9316 if (addStyle) {
bsw/jbe@1309 9317 for (attributeName in addStyle) {
bsw/jbe@1309 9318 method = addStyleMethods[addStyle[attributeName]];
bsw/jbe@1309 9319 if (!method) {
bsw/jbe@1309 9320 continue;
bsw/jbe@1309 9321 }
bsw/jbe@1309 9322
bsw/jbe@1309 9323 newStyle = method(wysihtml.dom.getAttribute(oldNode, attributeName));
bsw/jbe@1309 9324 if (typeof(newStyle) === "string") {
bsw/jbe@1309 9325 styles.push(newStyle);
bsw/jbe@1309 9326 }
bsw/jbe@1309 9327 }
bsw/jbe@1309 9328 }
bsw/jbe@1309 9329
bsw/jbe@1309 9330
bsw/jbe@1309 9331 if (typeof(allowedClasses) === "string" && allowedClasses === "any") {
bsw/jbe@1309 9332 if (oldNode.getAttribute("class")) {
bsw/jbe@1309 9333 if (currentRules.classes_blacklist) {
bsw/jbe@1309 9334 oldClasses = oldNode.getAttribute("class");
bsw/jbe@1309 9335 if (oldClasses) {
bsw/jbe@1309 9336 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
bsw/jbe@1309 9337 }
bsw/jbe@1309 9338
bsw/jbe@1309 9339 classesLength = classes.length;
bsw/jbe@1309 9340 for (; i<classesLength; i++) {
bsw/jbe@1309 9341 currentClass = classes[i];
bsw/jbe@1309 9342 if (!currentRules.classes_blacklist[currentClass]) {
bsw/jbe@1309 9343 newClasses.push(currentClass);
bsw/jbe@1309 9344 }
bsw/jbe@1309 9345 }
bsw/jbe@1309 9346
bsw/jbe@1309 9347 if (newClasses.length) {
bsw/jbe@1309 9348 attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" ");
bsw/jbe@1309 9349 }
bsw/jbe@1309 9350
bsw/jbe@1309 9351 } else {
bsw/jbe@1309 9352 attributes["class"] = oldNode.getAttribute("class");
bsw/jbe@1309 9353 }
bsw/jbe@1309 9354 } else {
bsw/jbe@1309 9355 if(classes && classes.length > 0) {
bsw/jbe@1309 9356 attributes["class"] = wysihtml.lang.array(classes).unique().join(" ");
bsw/jbe@1309 9357 }
bsw/jbe@1309 9358 }
bsw/jbe@1309 9359 } else {
bsw/jbe@1309 9360 // make sure that wysihtml temp class doesn't get stripped out
bsw/jbe@1309 9361 if (!clearInternals) {
bsw/jbe@1309 9362 allowedClasses["_wysihtml-temp-placeholder"] = 1;
bsw/jbe@1309 9363 allowedClasses["_rangySelectionBoundary"] = 1;
bsw/jbe@1309 9364 allowedClasses["wysiwyg-tmp-selected-cell"] = 1;
bsw/jbe@1309 9365 }
bsw/jbe@1309 9366
bsw/jbe@1309 9367 // add old classes last
bsw/jbe@1309 9368 oldClasses = oldNode.getAttribute("class");
bsw/jbe@1309 9369 if (oldClasses) {
bsw/jbe@1309 9370 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
bsw/jbe@1309 9371 }
bsw/jbe@1309 9372 classesLength = classes.length;
bsw/jbe@1309 9373 for (; i<classesLength; i++) {
bsw/jbe@1309 9374 currentClass = classes[i];
bsw/jbe@1309 9375 if (allowedClasses[currentClass]) {
bsw/jbe@1309 9376 newClasses.push(currentClass);
bsw/jbe@1309 9377 }
bsw/jbe@1309 9378 }
bsw/jbe@1309 9379
bsw/jbe@1309 9380 if (newClasses.length) {
bsw/jbe@1309 9381 attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" ");
bsw/jbe@1309 9382 }
bsw/jbe@1309 9383 }
bsw/jbe@1309 9384
bsw/jbe@1309 9385 // remove table selection class if present
bsw/jbe@1309 9386 if (attributes["class"] && clearInternals) {
bsw/jbe@1309 9387 attributes["class"] = attributes["class"].replace("wysiwyg-tmp-selected-cell", "");
bsw/jbe@1309 9388 if ((/^\s*$/g).test(attributes["class"])) {
bsw/jbe@1309 9389 delete attributes["class"];
bsw/jbe@1309 9390 }
bsw/jbe@1309 9391 }
bsw/jbe@1309 9392
bsw/jbe@1309 9393 if (styles.length) {
bsw/jbe@1309 9394 attributes["style"] = wysihtml.lang.array(styles).unique().join(" ");
bsw/jbe@1309 9395 }
bsw/jbe@1309 9396
bsw/jbe@1309 9397 // set attributes on newNode
bsw/jbe@1309 9398 for (attributeName in attributes) {
bsw/jbe@1309 9399 // Setting attributes can cause a js error in IE under certain circumstances
bsw/jbe@1309 9400 // eg. on a <img> under https when it's new attribute value is non-https
bsw/jbe@1309 9401 // TODO: Investigate this further and check for smarter handling
bsw/jbe@1309 9402 try {
bsw/jbe@1309 9403 newNode.setAttribute(attributeName, attributes[attributeName]);
bsw/jbe@1309 9404 } catch(e) {}
bsw/jbe@1309 9405 }
bsw/jbe@1309 9406
bsw/jbe@1309 9407 // IE8 sometimes loses the width/height attributes when those are set before the "src"
bsw/jbe@1309 9408 // so we make sure to set them again
bsw/jbe@1309 9409 if (attributes.src) {
bsw/jbe@1309 9410 if (typeof(attributes.width) !== "undefined") {
bsw/jbe@1309 9411 newNode.setAttribute("width", attributes.width);
bsw/jbe@1309 9412 }
bsw/jbe@1309 9413 if (typeof(attributes.height) !== "undefined") {
bsw/jbe@1309 9414 newNode.setAttribute("height", attributes.height);
bsw/jbe@1309 9415 }
bsw/jbe@1309 9416 }
bsw/jbe@1309 9417 }
bsw/jbe@1309 9418
bsw/jbe@1309 9419 function _handleText(oldNode) {
bsw/jbe@1309 9420 var nextSibling = oldNode.nextSibling;
bsw/jbe@1309 9421 if (nextSibling && nextSibling.nodeType === wysihtml.TEXT_NODE) {
bsw/jbe@1309 9422 // Concatenate text nodes
bsw/jbe@1309 9423 nextSibling.data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309 9424 } else {
bsw/jbe@1309 9425 // \uFEFF = wysihtml.INVISIBLE_SPACE (used as a hack in certain rich text editing situations)
bsw/jbe@1309 9426 var data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309 9427 return oldNode.ownerDocument.createTextNode(data);
bsw/jbe@1309 9428 }
bsw/jbe@1309 9429 }
bsw/jbe@1309 9430
bsw/jbe@1309 9431 function _handleComment(oldNode) {
bsw/jbe@1309 9432 if (currentRules.comments) {
bsw/jbe@1309 9433 return oldNode.ownerDocument.createComment(oldNode.nodeValue);
bsw/jbe@1309 9434 }
bsw/jbe@1309 9435 }
bsw/jbe@1309 9436
bsw/jbe@1309 9437 // ------------ attribute checks ------------ \\
bsw/jbe@1309 9438 var attributeCheckMethods = {
bsw/jbe@1309 9439 url: (function() {
bsw/jbe@1309 9440 var REG_EXP = /^https?:\/\//i;
bsw/jbe@1309 9441 return function(attributeValue) {
bsw/jbe@1309 9442 if (!attributeValue || !attributeValue.match(REG_EXP)) {
bsw/jbe@1309 9443 return null;
bsw/jbe@1309 9444 }
bsw/jbe@1309 9445 return attributeValue.replace(REG_EXP, function(match) {
bsw/jbe@1309 9446 return match.toLowerCase();
bsw/jbe@1309 9447 });
bsw/jbe@1309 9448 };
bsw/jbe@1309 9449 })(),
bsw/jbe@1309 9450
bsw/jbe@1309 9451 src: (function() {
bsw/jbe@1309 9452 var REG_EXP = /^(\/|https?:\/\/)/i;
bsw/jbe@1309 9453 return function(attributeValue) {
bsw/jbe@1309 9454 if (!attributeValue || !attributeValue.match(REG_EXP)) {
bsw/jbe@1309 9455 return null;
bsw/jbe@1309 9456 }
bsw/jbe@1309 9457 return attributeValue.replace(REG_EXP, function(match) {
bsw/jbe@1309 9458 return match.toLowerCase();
bsw/jbe@1309 9459 });
bsw/jbe@1309 9460 };
bsw/jbe@1309 9461 })(),
bsw/jbe@1309 9462
bsw/jbe@1309 9463 href: (function() {
bsw/jbe@1309 9464 var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i;
bsw/jbe@1309 9465 return function(attributeValue) {
bsw/jbe@1309 9466 if (!attributeValue || !attributeValue.match(REG_EXP)) {
bsw/jbe@1309 9467 return null;
bsw/jbe@1309 9468 }
bsw/jbe@1309 9469 return attributeValue.replace(REG_EXP, function(match) {
bsw/jbe@1309 9470 return match.toLowerCase();
bsw/jbe@1309 9471 });
bsw/jbe@1309 9472 };
bsw/jbe@1309 9473 })(),
bsw/jbe@1309 9474
bsw/jbe@1309 9475 alt: (function() {
bsw/jbe@1309 9476 var REG_EXP = /[^ a-z0-9_\-]/gi;
bsw/jbe@1309 9477 return function(attributeValue, nodeName) {
bsw/jbe@1309 9478 if (!attributeValue) {
bsw/jbe@1309 9479 if (nodeName === "IMG") {
bsw/jbe@1309 9480 return "";
bsw/jbe@1309 9481 } else {
bsw/jbe@1309 9482 return null;
bsw/jbe@1309 9483 }
bsw/jbe@1309 9484 }
bsw/jbe@1309 9485 return attributeValue.replace(REG_EXP, "");
bsw/jbe@1309 9486 };
bsw/jbe@1309 9487 })(),
bsw/jbe@1309 9488
bsw/jbe@1309 9489 // Integers. Does not work with floating point numbers and units
bsw/jbe@1309 9490 numbers: (function() {
bsw/jbe@1309 9491 var REG_EXP = /\D/g;
bsw/jbe@1309 9492 return function(attributeValue) {
bsw/jbe@1309 9493 attributeValue = (attributeValue || "").replace(REG_EXP, "");
bsw/jbe@1309 9494 return attributeValue || null;
bsw/jbe@1309 9495 };
bsw/jbe@1309 9496 })(),
bsw/jbe@1309 9497
bsw/jbe@1309 9498 // Useful for with/height attributes where floating points and percentages are allowed
bsw/jbe@1309 9499 dimension: (function() {
bsw/jbe@1309 9500 var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/;
bsw/jbe@1309 9501 return function(attributeValue) {
bsw/jbe@1309 9502 attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3");
bsw/jbe@1309 9503 return attributeValue || null;
bsw/jbe@1309 9504 };
bsw/jbe@1309 9505 })(),
bsw/jbe@1309 9506
bsw/jbe@1309 9507 any: (function() {
bsw/jbe@1309 9508 return function(attributeValue) {
bsw/jbe@1309 9509 if (!attributeValue) {
bsw/jbe@1309 9510 return null;
bsw/jbe@1309 9511 }
bsw/jbe@1309 9512 return attributeValue;
bsw/jbe@1309 9513 };
bsw/jbe@1309 9514 })()
bsw/jbe@1309 9515 };
bsw/jbe@1309 9516
bsw/jbe@1309 9517 // ------------ style converter (converts an html attribute to a style) ------------ \\
bsw/jbe@1309 9518 var addStyleMethods = {
bsw/jbe@1309 9519 align_text: (function() {
bsw/jbe@1309 9520 var mapping = {
bsw/jbe@1309 9521 left: "text-align: left;",
bsw/jbe@1309 9522 right: "text-align: right;",
bsw/jbe@1309 9523 center: "text-align: center;"
bsw/jbe@1309 9524 };
bsw/jbe@1309 9525 return function(attributeValue) {
bsw/jbe@1309 9526 return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309 9527 };
bsw/jbe@1309 9528 })(),
bsw/jbe@1309 9529 };
bsw/jbe@1309 9530
bsw/jbe@1309 9531 // ------------ class converter (converts an html attribute to a class name) ------------ \\
bsw/jbe@1309 9532 var addClassMethods = {
bsw/jbe@1309 9533 align_img: (function() {
bsw/jbe@1309 9534 var mapping = {
bsw/jbe@1309 9535 left: "wysiwyg-float-left",
bsw/jbe@1309 9536 right: "wysiwyg-float-right"
bsw/jbe@1309 9537 };
bsw/jbe@1309 9538 return function(attributeValue) {
bsw/jbe@1309 9539 return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309 9540 };
bsw/jbe@1309 9541 })(),
bsw/jbe@1309 9542
bsw/jbe@1309 9543 align_text: (function() {
bsw/jbe@1309 9544 var mapping = {
bsw/jbe@1309 9545 left: "wysiwyg-text-align-left",
bsw/jbe@1309 9546 right: "wysiwyg-text-align-right",
bsw/jbe@1309 9547 center: "wysiwyg-text-align-center",
bsw/jbe@1309 9548 justify: "wysiwyg-text-align-justify"
bsw/jbe@1309 9549 };
bsw/jbe@1309 9550 return function(attributeValue) {
bsw/jbe@1309 9551 return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309 9552 };
bsw/jbe@1309 9553 })(),
bsw/jbe@1309 9554
bsw/jbe@1309 9555 clear_br: (function() {
bsw/jbe@1309 9556 var mapping = {
bsw/jbe@1309 9557 left: "wysiwyg-clear-left",
bsw/jbe@1309 9558 right: "wysiwyg-clear-right",
bsw/jbe@1309 9559 both: "wysiwyg-clear-both",
bsw/jbe@1309 9560 all: "wysiwyg-clear-both"
bsw/jbe@1309 9561 };
bsw/jbe@1309 9562 return function(attributeValue) {
bsw/jbe@1309 9563 return mapping[String(attributeValue).toLowerCase()];
bsw/jbe@1309 9564 };
bsw/jbe@1309 9565 })(),
bsw/jbe@1309 9566
bsw/jbe@1309 9567 size_font: (function() {
bsw/jbe@1309 9568 var mapping = {
bsw/jbe@1309 9569 "1": "wysiwyg-font-size-xx-small",
bsw/jbe@1309 9570 "2": "wysiwyg-font-size-small",
bsw/jbe@1309 9571 "3": "wysiwyg-font-size-medium",
bsw/jbe@1309 9572 "4": "wysiwyg-font-size-large",
bsw/jbe@1309 9573 "5": "wysiwyg-font-size-x-large",
bsw/jbe@1309 9574 "6": "wysiwyg-font-size-xx-large",
bsw/jbe@1309 9575 "7": "wysiwyg-font-size-xx-large",
bsw/jbe@1309 9576 "-": "wysiwyg-font-size-smaller",
bsw/jbe@1309 9577 "+": "wysiwyg-font-size-larger"
bsw/jbe@1309 9578 };
bsw/jbe@1309 9579 return function(attributeValue) {
bsw/jbe@1309 9580 return mapping[String(attributeValue).charAt(0)];
bsw/jbe@1309 9581 };
bsw/jbe@1309 9582 })()
bsw/jbe@1309 9583 };
bsw/jbe@1309 9584
bsw/jbe@1309 9585 // checks if element is possibly visible
bsw/jbe@1309 9586 var typeCeckMethods = {
bsw/jbe@1309 9587 has_visible_contet: (function() {
bsw/jbe@1309 9588 var txt,
bsw/jbe@1309 9589 isVisible = false,
bsw/jbe@1309 9590 visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
bsw/jbe@1309 9591 'style', 'table', 'iframe', 'object', 'embed', 'audio',
bsw/jbe@1309 9592 'svg', 'input', 'button', 'select','textarea', 'canvas'];
bsw/jbe@1309 9593
bsw/jbe@1309 9594 return function(el) {
bsw/jbe@1309 9595
bsw/jbe@1309 9596 // has visible innertext. so is visible
bsw/jbe@1309 9597 txt = (el.innerText || el.textContent).replace(/\s/g, '');
bsw/jbe@1309 9598 if (txt && txt.length > 0) {
bsw/jbe@1309 9599 return true;
bsw/jbe@1309 9600 }
bsw/jbe@1309 9601
bsw/jbe@1309 9602 // matches list of visible dimensioned elements
bsw/jbe@1309 9603 for (var i = visibleElements.length; i--;) {
bsw/jbe@1309 9604 if (el.querySelector(visibleElements[i])) {
bsw/jbe@1309 9605 return true;
bsw/jbe@1309 9606 }
bsw/jbe@1309 9607 }
bsw/jbe@1309 9608
bsw/jbe@1309 9609 // try to measure dimesions in last resort. (can find only of elements in dom)
bsw/jbe@1309 9610 if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) {
bsw/jbe@1309 9611 return true;
bsw/jbe@1309 9612 }
bsw/jbe@1309 9613
bsw/jbe@1309 9614 return false;
bsw/jbe@1309 9615 };
bsw/jbe@1309 9616 })()
bsw/jbe@1309 9617 };
bsw/jbe@1309 9618
bsw/jbe@1309 9619 var elementHandlingMethods = {
bsw/jbe@1309 9620 unwrap: function (element) {
bsw/jbe@1309 9621 wysihtml.dom.unwrap(element);
bsw/jbe@1309 9622 },
bsw/jbe@1309 9623
bsw/jbe@1309 9624 remove: function (element) {
bsw/jbe@1309 9625 element.parentNode.removeChild(element);
bsw/jbe@1309 9626 }
bsw/jbe@1309 9627 };
bsw/jbe@1309 9628
bsw/jbe@1309 9629 return parse(elementOrHtml_current, config_current);
bsw/jbe@1309 9630 };
bsw/jbe@1309 9631
bsw/jbe@1309 9632 // does a selector query on element or array of elements
bsw/jbe@1309 9633 wysihtml.dom.query = function(elements, query) {
bsw/jbe@1309 9634 var ret = [],
bsw/jbe@1309 9635 q;
bsw/jbe@1309 9636
bsw/jbe@1309 9637 if (elements.nodeType) {
bsw/jbe@1309 9638 elements = [elements];
bsw/jbe@1309 9639 }
bsw/jbe@1309 9640
bsw/jbe@1309 9641 for (var e = 0, len = elements.length; e < len; e++) {
bsw/jbe@1309 9642 q = elements[e].querySelectorAll(query);
bsw/jbe@1309 9643 if (q) {
bsw/jbe@1309 9644 for(var i = q.length; i--; ret.unshift(q[i]));
bsw/jbe@1309 9645 }
bsw/jbe@1309 9646 }
bsw/jbe@1309 9647 return ret;
bsw/jbe@1309 9648 };
bsw/jbe@1309 9649
bsw/jbe@1309 9650 /**
bsw/jbe@1309 9651 * Checks for empty text node childs and removes them
bsw/jbe@1309 9652 *
bsw/jbe@1309 9653 * @param {Element} node The element in which to cleanup
bsw/jbe@1309 9654 * @example
bsw/jbe@1309 9655 * wysihtml.dom.removeEmptyTextNodes(element);
bsw/jbe@1309 9656 */
bsw/jbe@1309 9657 wysihtml.dom.removeEmptyTextNodes = function(node) {
bsw/jbe@1309 9658 var childNode,
bsw/jbe@1309 9659 childNodes = wysihtml.lang.array(node.childNodes).get(),
bsw/jbe@1309 9660 childNodesLength = childNodes.length,
bsw/jbe@1309 9661 i = 0;
bsw/jbe@1309 9662
bsw/jbe@1309 9663 for (; i<childNodesLength; i++) {
bsw/jbe@1309 9664 childNode = childNodes[i];
bsw/jbe@1309 9665 if (childNode.nodeType === wysihtml.TEXT_NODE && (/^[\n\r]*$/).test(childNode.data)) {
bsw/jbe@1309 9666 childNode.parentNode.removeChild(childNode);
bsw/jbe@1309 9667 }
bsw/jbe@1309 9668 }
bsw/jbe@1309 9669 };
bsw/jbe@1309 9670
bsw/jbe@1309 9671 wysihtml.dom.removeInvisibleSpaces = function(node) {
bsw/jbe@1309 9672 var textNodes = wysihtml.dom.getTextNodes(node);
bsw/jbe@1309 9673 for (var n = textNodes.length; n--;) {
bsw/jbe@1309 9674 textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309 9675 }
bsw/jbe@1309 9676 };
bsw/jbe@1309 9677
bsw/jbe@1309 9678 /**
bsw/jbe@1309 9679 * Renames an element (eg. a <div> to a <p>) and keeps its childs
bsw/jbe@1309 9680 *
bsw/jbe@1309 9681 * @param {Element} element The list element which should be renamed
bsw/jbe@1309 9682 * @param {Element} newNodeName The desired tag name
bsw/jbe@1309 9683 *
bsw/jbe@1309 9684 * @example
bsw/jbe@1309 9685 * <!-- Assume the following dom: -->
bsw/jbe@1309 9686 * <ul id="list">
bsw/jbe@1309 9687 * <li>eminem</li>
bsw/jbe@1309 9688 * <li>dr. dre</li>
bsw/jbe@1309 9689 * <li>50 Cent</li>
bsw/jbe@1309 9690 * </ul>
bsw/jbe@1309 9691 *
bsw/jbe@1309 9692 * <script>
bsw/jbe@1309 9693 * wysihtml.dom.renameElement(document.getElementById("list"), "ol");
bsw/jbe@1309 9694 * </script>
bsw/jbe@1309 9695 *
bsw/jbe@1309 9696 * <!-- Will result in: -->
bsw/jbe@1309 9697 * <ol>
bsw/jbe@1309 9698 * <li>eminem</li>
bsw/jbe@1309 9699 * <li>dr. dre</li>
bsw/jbe@1309 9700 * <li>50 Cent</li>
bsw/jbe@1309 9701 * </ol>
bsw/jbe@1309 9702 */
bsw/jbe@1309 9703 wysihtml.dom.renameElement = function(element, newNodeName) {
bsw/jbe@1309 9704 var newElement = element.ownerDocument.createElement(newNodeName),
bsw/jbe@1309 9705 firstChild;
bsw/jbe@1309 9706 while (firstChild = element.firstChild) {
bsw/jbe@1309 9707 newElement.appendChild(firstChild);
bsw/jbe@1309 9708 }
bsw/jbe@1309 9709 wysihtml.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
bsw/jbe@1309 9710
bsw/jbe@1309 9711 if (element.parentNode) {
bsw/jbe@1309 9712 element.parentNode.replaceChild(newElement, element);
bsw/jbe@1309 9713 }
bsw/jbe@1309 9714
bsw/jbe@1309 9715 return newElement;
bsw/jbe@1309 9716 };
bsw/jbe@1309 9717
bsw/jbe@1309 9718 /**
bsw/jbe@1309 9719 * Takes an element, removes it and replaces it with it's childs
bsw/jbe@1309 9720 *
bsw/jbe@1309 9721 * @param {Object} node The node which to replace with it's child nodes
bsw/jbe@1309 9722 * @example
bsw/jbe@1309 9723 * <div id="foo">
bsw/jbe@1309 9724 * <span>hello</span>
bsw/jbe@1309 9725 * </div>
bsw/jbe@1309 9726 * <script>
bsw/jbe@1309 9727 * // Remove #foo and replace with it's children
bsw/jbe@1309 9728 * wysihtml.dom.replaceWithChildNodes(document.getElementById("foo"));
bsw/jbe@1309 9729 * </script>
bsw/jbe@1309 9730 */
bsw/jbe@1309 9731 wysihtml.dom.replaceWithChildNodes = function(node) {
bsw/jbe@1309 9732 if (!node.parentNode) {
bsw/jbe@1309 9733 return;
bsw/jbe@1309 9734 }
bsw/jbe@1309 9735
bsw/jbe@1309 9736 while (node.firstChild) {
bsw/jbe@1309 9737 node.parentNode.insertBefore(node.firstChild, node);
bsw/jbe@1309 9738 }
bsw/jbe@1309 9739 node.parentNode.removeChild(node);
bsw/jbe@1309 9740 };
bsw/jbe@1309 9741
bsw/jbe@1309 9742 /**
bsw/jbe@1309 9743 * Unwraps an unordered/ordered list
bsw/jbe@1309 9744 *
bsw/jbe@1309 9745 * @param {Element} element The list element which should be unwrapped
bsw/jbe@1309 9746 *
bsw/jbe@1309 9747 * @example
bsw/jbe@1309 9748 * <!-- Assume the following dom: -->
bsw/jbe@1309 9749 * <ul id="list">
bsw/jbe@1309 9750 * <li>eminem</li>
bsw/jbe@1309 9751 * <li>dr. dre</li>
bsw/jbe@1309 9752 * <li>50 Cent</li>
bsw/jbe@1309 9753 * </ul>
bsw/jbe@1309 9754 *
bsw/jbe@1309 9755 * <script>
bsw/jbe@1309 9756 * wysihtml.dom.resolveList(document.getElementById("list"));
bsw/jbe@1309 9757 * </script>
bsw/jbe@1309 9758 *
bsw/jbe@1309 9759 * <!-- Will result in: -->
bsw/jbe@1309 9760 * eminem<br>
bsw/jbe@1309 9761 * dr. dre<br>
bsw/jbe@1309 9762 * 50 Cent<br>
bsw/jbe@1309 9763 */
bsw/jbe@1309 9764 (function(dom) {
bsw/jbe@1309 9765 function _isBlockElement(node) {
bsw/jbe@1309 9766 return dom.getStyle("display").from(node) === "block";
bsw/jbe@1309 9767 }
bsw/jbe@1309 9768
bsw/jbe@1309 9769 function _isLineBreak(node) {
bsw/jbe@1309 9770 return node.nodeName === "BR";
bsw/jbe@1309 9771 }
bsw/jbe@1309 9772
bsw/jbe@1309 9773 function _appendLineBreak(element) {
bsw/jbe@1309 9774 var lineBreak = element.ownerDocument.createElement("br");
bsw/jbe@1309 9775 element.appendChild(lineBreak);
bsw/jbe@1309 9776 }
bsw/jbe@1309 9777
bsw/jbe@1309 9778 function resolveList(list, useLineBreaks) {
bsw/jbe@1309 9779 if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
bsw/jbe@1309 9780 return;
bsw/jbe@1309 9781 }
bsw/jbe@1309 9782
bsw/jbe@1309 9783 var doc = list.ownerDocument,
bsw/jbe@1309 9784 fragment = doc.createDocumentFragment(),
bsw/jbe@1309 9785 previousSibling = wysihtml.dom.domNode(list).prev({ignoreBlankTexts: true}),
bsw/jbe@1309 9786 nextSibling = wysihtml.dom.domNode(list).next({ignoreBlankTexts: true}),
bsw/jbe@1309 9787 firstChild,
bsw/jbe@1309 9788 lastChild,
bsw/jbe@1309 9789 isLastChild,
bsw/jbe@1309 9790 shouldAppendLineBreak,
bsw/jbe@1309 9791 paragraph,
bsw/jbe@1309 9792 listItem,
bsw/jbe@1309 9793 lastListItem = list.lastElementChild || list.lastChild,
bsw/jbe@1309 9794 isLastItem;
bsw/jbe@1309 9795
bsw/jbe@1309 9796 if (useLineBreaks) {
bsw/jbe@1309 9797 // Insert line break if list is after a non-block element
bsw/jbe@1309 9798 if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) {
bsw/jbe@1309 9799 _appendLineBreak(fragment);
bsw/jbe@1309 9800 }
bsw/jbe@1309 9801
bsw/jbe@1309 9802 while (listItem = (list.firstElementChild || list.firstChild)) {
bsw/jbe@1309 9803 lastChild = listItem.lastChild;
bsw/jbe@1309 9804 isLastItem = listItem === lastListItem;
bsw/jbe@1309 9805 while (firstChild = listItem.firstChild) {
bsw/jbe@1309 9806 isLastChild = firstChild === lastChild;
bsw/jbe@1309 9807 // This needs to be done before appending it to the fragment, as it otherwise will lose style information
bsw/jbe@1309 9808 shouldAppendLineBreak = (!isLastItem || (nextSibling && !_isBlockElement(nextSibling))) && isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
bsw/jbe@1309 9809 fragment.appendChild(firstChild);
bsw/jbe@1309 9810 if (shouldAppendLineBreak) {
bsw/jbe@1309 9811 _appendLineBreak(fragment);
bsw/jbe@1309 9812 }
bsw/jbe@1309 9813 }
bsw/jbe@1309 9814
bsw/jbe@1309 9815 listItem.parentNode.removeChild(listItem);
bsw/jbe@1309 9816 }
bsw/jbe@1309 9817 } else {
bsw/jbe@1309 9818 while (listItem = (list.firstElementChild || list.firstChild)) {
bsw/jbe@1309 9819 if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) {
bsw/jbe@1309 9820 while (firstChild = listItem.firstChild) {
bsw/jbe@1309 9821 fragment.appendChild(firstChild);
bsw/jbe@1309 9822 }
bsw/jbe@1309 9823 } else {
bsw/jbe@1309 9824 paragraph = doc.createElement("p");
bsw/jbe@1309 9825 while (firstChild = listItem.firstChild) {
bsw/jbe@1309 9826 paragraph.appendChild(firstChild);
bsw/jbe@1309 9827 }
bsw/jbe@1309 9828 fragment.appendChild(paragraph);
bsw/jbe@1309 9829 }
bsw/jbe@1309 9830 listItem.parentNode.removeChild(listItem);
bsw/jbe@1309 9831 }
bsw/jbe@1309 9832 }
bsw/jbe@1309 9833
bsw/jbe@1309 9834 list.parentNode.replaceChild(fragment, list);
bsw/jbe@1309 9835 }
bsw/jbe@1309 9836
bsw/jbe@1309 9837 dom.resolveList = resolveList;
bsw/jbe@1309 9838 })(wysihtml.dom);
bsw/jbe@1309 9839
bsw/jbe@1309 9840 /**
bsw/jbe@1309 9841 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
bsw/jbe@1309 9842 *
bsw/jbe@1309 9843 * Browser Compatibility:
bsw/jbe@1309 9844 * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
bsw/jbe@1309 9845 * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
bsw/jbe@1309 9846 *
bsw/jbe@1309 9847 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
bsw/jbe@1309 9848 * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
bsw/jbe@1309 9849 * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
bsw/jbe@1309 9850 * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
bsw/jbe@1309 9851 * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
bsw/jbe@1309 9852 * can do anything as if the sandbox attribute wasn't set
bsw/jbe@1309 9853 *
bsw/jbe@1309 9854 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
bsw/jbe@1309 9855 * @param {Object} [config] Optional parameters
bsw/jbe@1309 9856 *
bsw/jbe@1309 9857 * @example
bsw/jbe@1309 9858 * new wysihtml.dom.Sandbox(function(sandbox) {
bsw/jbe@1309 9859 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
bsw/jbe@1309 9860 * });
bsw/jbe@1309 9861 */
bsw/jbe@1309 9862 (function(wysihtml) {
bsw/jbe@1309 9863 var /**
bsw/jbe@1309 9864 * Default configuration
bsw/jbe@1309 9865 */
bsw/jbe@1309 9866 doc = document,
bsw/jbe@1309 9867 /**
bsw/jbe@1309 9868 * Properties to unset/protect on the window object
bsw/jbe@1309 9869 */
bsw/jbe@1309 9870 windowProperties = [
bsw/jbe@1309 9871 "parent", "top", "opener", "frameElement", "frames",
bsw/jbe@1309 9872 "localStorage", "globalStorage", "sessionStorage", "indexedDB"
bsw/jbe@1309 9873 ],
bsw/jbe@1309 9874 /**
bsw/jbe@1309 9875 * Properties on the window object which are set to an empty function
bsw/jbe@1309 9876 */
bsw/jbe@1309 9877 windowProperties2 = [
bsw/jbe@1309 9878 "open", "close", "openDialog", "showModalDialog",
bsw/jbe@1309 9879 "alert", "confirm", "prompt",
bsw/jbe@1309 9880 "openDatabase", "postMessage",
bsw/jbe@1309 9881 "XMLHttpRequest", "XDomainRequest"
bsw/jbe@1309 9882 ],
bsw/jbe@1309 9883 /**
bsw/jbe@1309 9884 * Properties to unset/protect on the document object
bsw/jbe@1309 9885 */
bsw/jbe@1309 9886 documentProperties = [
bsw/jbe@1309 9887 "referrer",
bsw/jbe@1309 9888 "write", "open", "close"
bsw/jbe@1309 9889 ];
bsw/jbe@1309 9890
bsw/jbe@1309 9891 wysihtml.dom.Sandbox = Base.extend(
bsw/jbe@1309 9892 /** @scope wysihtml.dom.Sandbox.prototype */ {
bsw/jbe@1309 9893
bsw/jbe@1309 9894 constructor: function(readyCallback, config) {
bsw/jbe@1309 9895 this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
bsw/jbe@1309 9896 this.config = wysihtml.lang.object({}).merge(config).get();
bsw/jbe@1309 9897 if (!this.config.className) {
bsw/jbe@1309 9898 this.config.className = "wysihtml-sandbox";
bsw/jbe@1309 9899 }
bsw/jbe@1309 9900 this.editableArea = this._createIframe();
bsw/jbe@1309 9901 },
bsw/jbe@1309 9902
bsw/jbe@1309 9903 insertInto: function(element) {
bsw/jbe@1309 9904 if (typeof(element) === "string") {
bsw/jbe@1309 9905 element = doc.getElementById(element);
bsw/jbe@1309 9906 }
bsw/jbe@1309 9907
bsw/jbe@1309 9908 element.appendChild(this.editableArea);
bsw/jbe@1309 9909 },
bsw/jbe@1309 9910
bsw/jbe@1309 9911 getIframe: function() {
bsw/jbe@1309 9912 return this.editableArea;
bsw/jbe@1309 9913 },
bsw/jbe@1309 9914
bsw/jbe@1309 9915 getWindow: function() {
bsw/jbe@1309 9916 this._readyError();
bsw/jbe@1309 9917 },
bsw/jbe@1309 9918
bsw/jbe@1309 9919 getDocument: function() {
bsw/jbe@1309 9920 this._readyError();
bsw/jbe@1309 9921 },
bsw/jbe@1309 9922
bsw/jbe@1309 9923 destroy: function() {
bsw/jbe@1309 9924 var iframe = this.getIframe();
bsw/jbe@1309 9925 iframe.parentNode.removeChild(iframe);
bsw/jbe@1309 9926 },
bsw/jbe@1309 9927
bsw/jbe@1309 9928 _readyError: function() {
bsw/jbe@1309 9929 throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet");
bsw/jbe@1309 9930 },
bsw/jbe@1309 9931
bsw/jbe@1309 9932 /**
bsw/jbe@1309 9933 * Creates the sandbox iframe
bsw/jbe@1309 9934 *
bsw/jbe@1309 9935 * Some important notes:
bsw/jbe@1309 9936 * - We can't use HTML5 sandbox for now:
bsw/jbe@1309 9937 * setting it causes that the iframe's dom can't be accessed from the outside
bsw/jbe@1309 9938 * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
bsw/jbe@1309 9939 * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
bsw/jbe@1309 9940 * In order to make this happen we need to set the "allow-scripts" flag.
bsw/jbe@1309 9941 * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
bsw/jbe@1309 9942 * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
bsw/jbe@1309 9943 * - IE needs to have the security="restricted" attribute set before the iframe is
bsw/jbe@1309 9944 * inserted into the dom tree
bsw/jbe@1309 9945 * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
bsw/jbe@1309 9946 * though it supports it
bsw/jbe@1309 9947 * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
bsw/jbe@1309 9948 * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
bsw/jbe@1309 9949 * on the onreadystatechange event
bsw/jbe@1309 9950 */
bsw/jbe@1309 9951 _createIframe: function() {
bsw/jbe@1309 9952 var that = this,
bsw/jbe@1309 9953 iframe = doc.createElement("iframe");
bsw/jbe@1309 9954 iframe.className = this.config.className;
bsw/jbe@1309 9955 wysihtml.dom.setAttributes({
bsw/jbe@1309 9956 "security": "restricted",
bsw/jbe@1309 9957 "allowtransparency": "true",
bsw/jbe@1309 9958 "frameborder": 0,
bsw/jbe@1309 9959 "width": 0,
bsw/jbe@1309 9960 "height": 0,
bsw/jbe@1309 9961 "marginwidth": 0,
bsw/jbe@1309 9962 "marginheight": 0
bsw/jbe@1309 9963 }).on(iframe);
bsw/jbe@1309 9964
bsw/jbe@1309 9965 // Setting the src like this prevents ssl warnings in IE6
bsw/jbe@1309 9966 if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
bsw/jbe@1309 9967 iframe.src = "javascript:'<html></html>'";
bsw/jbe@1309 9968 }
bsw/jbe@1309 9969
bsw/jbe@1309 9970 iframe.onload = function() {
bsw/jbe@1309 9971 iframe.onreadystatechange = iframe.onload = null;
bsw/jbe@1309 9972 that._onLoadIframe(iframe);
bsw/jbe@1309 9973 };
bsw/jbe@1309 9974
bsw/jbe@1309 9975 iframe.onreadystatechange = function() {
bsw/jbe@1309 9976 if (/loaded|complete/.test(iframe.readyState)) {
bsw/jbe@1309 9977 iframe.onreadystatechange = iframe.onload = null;
bsw/jbe@1309 9978 that._onLoadIframe(iframe);
bsw/jbe@1309 9979 }
bsw/jbe@1309 9980 };
bsw/jbe@1309 9981
bsw/jbe@1309 9982 return iframe;
bsw/jbe@1309 9983 },
bsw/jbe@1309 9984
bsw/jbe@1309 9985 /**
bsw/jbe@1309 9986 * Callback for when the iframe has finished loading
bsw/jbe@1309 9987 */
bsw/jbe@1309 9988 _onLoadIframe: function(iframe) {
bsw/jbe@1309 9989 // don't resume when the iframe got unloaded (eg. by removing it from the dom)
bsw/jbe@1309 9990 if (!wysihtml.dom.contains(doc.documentElement, iframe)) {
bsw/jbe@1309 9991 return;
bsw/jbe@1309 9992 }
bsw/jbe@1309 9993
bsw/jbe@1309 9994 var that = this,
bsw/jbe@1309 9995 iframeWindow = iframe.contentWindow,
bsw/jbe@1309 9996 iframeDocument = iframe.contentWindow.document,
bsw/jbe@1309 9997 charset = doc.characterSet || doc.charset || "utf-8",
bsw/jbe@1309 9998 sandboxHtml = this._getHtml({
bsw/jbe@1309 9999 charset: charset,
bsw/jbe@1309 10000 stylesheets: this.config.stylesheets
bsw/jbe@1309 10001 });
bsw/jbe@1309 10002
bsw/jbe@1309 10003 // Create the basic dom tree including proper DOCTYPE and charset
bsw/jbe@1309 10004 iframeDocument.open("text/html", "replace");
bsw/jbe@1309 10005 iframeDocument.write(sandboxHtml);
bsw/jbe@1309 10006 iframeDocument.close();
bsw/jbe@1309 10007
bsw/jbe@1309 10008 this.getWindow = function() { return iframe.contentWindow; };
bsw/jbe@1309 10009 this.getDocument = function() { return iframe.contentWindow.document; };
bsw/jbe@1309 10010
bsw/jbe@1309 10011 // Catch js errors and pass them to the parent's onerror event
bsw/jbe@1309 10012 // addEventListener("error") doesn't work properly in some browsers
bsw/jbe@1309 10013 // TODO: apparently this doesn't work in IE9!
bsw/jbe@1309 10014 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
bsw/jbe@1309 10015 throw new Error("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber);
bsw/jbe@1309 10016 };
bsw/jbe@1309 10017
bsw/jbe@1309 10018 if (!wysihtml.browser.supportsSandboxedIframes()) {
bsw/jbe@1309 10019 // Unset a bunch of sensitive variables
bsw/jbe@1309 10020 // Please note: This isn't hack safe!
bsw/jbe@1309 10021 // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
bsw/jbe@1309 10022 // IE is secure though, which is the most important thing, since IE is the only browser, who
bsw/jbe@1309 10023 // takes over scripts & styles into contentEditable elements when copied from external websites
bsw/jbe@1309 10024 // or applications (Microsoft Word, ...)
bsw/jbe@1309 10025 var i, length;
bsw/jbe@1309 10026 for (i=0, length=windowProperties.length; i<length; i++) {
bsw/jbe@1309 10027 this._unset(iframeWindow, windowProperties[i]);
bsw/jbe@1309 10028 }
bsw/jbe@1309 10029 for (i=0, length=windowProperties2.length; i<length; i++) {
bsw/jbe@1309 10030 this._unset(iframeWindow, windowProperties2[i], wysihtml.EMPTY_FUNCTION);
bsw/jbe@1309 10031 }
bsw/jbe@1309 10032 for (i=0, length=documentProperties.length; i<length; i++) {
bsw/jbe@1309 10033 this._unset(iframeDocument, documentProperties[i]);
bsw/jbe@1309 10034 }
bsw/jbe@1309 10035 // This doesn't work in Safari 5
bsw/jbe@1309 10036 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
bsw/jbe@1309 10037 this._unset(iframeDocument, "cookie", "", true);
bsw/jbe@1309 10038 }
bsw/jbe@1309 10039
bsw/jbe@1309 10040 if (wysihtml.polyfills) {
bsw/jbe@1309 10041 wysihtml.polyfills(iframeWindow, iframeDocument).apply();
bsw/jbe@1309 10042 }
bsw/jbe@1309 10043
bsw/jbe@1309 10044 this.loaded = true;
bsw/jbe@1309 10045
bsw/jbe@1309 10046 // Trigger the callback
bsw/jbe@1309 10047 setTimeout(function() { that.callback(that); }, 0);
bsw/jbe@1309 10048 },
bsw/jbe@1309 10049
bsw/jbe@1309 10050 _getHtml: function(templateVars) {
bsw/jbe@1309 10051 var stylesheets = templateVars.stylesheets,
bsw/jbe@1309 10052 html = "",
bsw/jbe@1309 10053 i = 0,
bsw/jbe@1309 10054 length;
bsw/jbe@1309 10055 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
bsw/jbe@1309 10056 if (stylesheets) {
bsw/jbe@1309 10057 length = stylesheets.length;
bsw/jbe@1309 10058 for (; i<length; i++) {
bsw/jbe@1309 10059 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
bsw/jbe@1309 10060 }
bsw/jbe@1309 10061 }
bsw/jbe@1309 10062 templateVars.stylesheets = html;
bsw/jbe@1309 10063
bsw/jbe@1309 10064 return wysihtml.lang.string(
bsw/jbe@1309 10065 '<!DOCTYPE html><html><head>'
bsw/jbe@1309 10066 + '<meta charset="#{charset}">#{stylesheets}</head>'
bsw/jbe@1309 10067 + '<body></body></html>'
bsw/jbe@1309 10068 ).interpolate(templateVars);
bsw/jbe@1309 10069 },
bsw/jbe@1309 10070
bsw/jbe@1309 10071 /**
bsw/jbe@1309 10072 * Method to unset/override existing variables
bsw/jbe@1309 10073 * @example
bsw/jbe@1309 10074 * // Make cookie unreadable and unwritable
bsw/jbe@1309 10075 * this._unset(document, "cookie", "", true);
bsw/jbe@1309 10076 */
bsw/jbe@1309 10077 _unset: function(object, property, value, setter) {
bsw/jbe@1309 10078 try { object[property] = value; } catch(e) {}
bsw/jbe@1309 10079
bsw/jbe@1309 10080 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
bsw/jbe@1309 10081 if (setter) {
bsw/jbe@1309 10082 try { object.__defineSetter__(property, function() {}); } catch(e) {}
bsw/jbe@1309 10083 }
bsw/jbe@1309 10084
bsw/jbe@1309 10085 if (!wysihtml.browser.crashesWhenDefineProperty(property)) {
bsw/jbe@1309 10086 try {
bsw/jbe@1309 10087 var config = {
bsw/jbe@1309 10088 get: function() { return value; }
bsw/jbe@1309 10089 };
bsw/jbe@1309 10090 if (setter) {
bsw/jbe@1309 10091 config.set = function() {};
bsw/jbe@1309 10092 }
bsw/jbe@1309 10093 Object.defineProperty(object, property, config);
bsw/jbe@1309 10094 } catch(e) {}
bsw/jbe@1309 10095 }
bsw/jbe@1309 10096 }
bsw/jbe@1309 10097 });
bsw/jbe@1309 10098 })(wysihtml);
bsw/jbe@1309 10099
bsw/jbe@1309 10100 (function() {
bsw/jbe@1309 10101 var mapping = {
bsw/jbe@1309 10102 "className": "class"
bsw/jbe@1309 10103 };
bsw/jbe@1309 10104 wysihtml.dom.setAttributes = function(attributes) {
bsw/jbe@1309 10105 return {
bsw/jbe@1309 10106 on: function(element) {
bsw/jbe@1309 10107 for (var i in attributes) {
bsw/jbe@1309 10108 element.setAttribute(mapping[i] || i, attributes[i]);
bsw/jbe@1309 10109 }
bsw/jbe@1309 10110 }
bsw/jbe@1309 10111 };
bsw/jbe@1309 10112 };
bsw/jbe@1309 10113 })();
bsw/jbe@1309 10114
bsw/jbe@1309 10115 wysihtml.dom.setStyles = function(styles) {
bsw/jbe@1309 10116 return {
bsw/jbe@1309 10117 on: function(element) {
bsw/jbe@1309 10118 var style = element.style;
bsw/jbe@1309 10119 if (typeof(styles) === "string") {
bsw/jbe@1309 10120 style.cssText += ";" + styles;
bsw/jbe@1309 10121 return;
bsw/jbe@1309 10122 }
bsw/jbe@1309 10123 for (var i in styles) {
bsw/jbe@1309 10124 if (i === "float") {
bsw/jbe@1309 10125 style.cssFloat = styles[i];
bsw/jbe@1309 10126 style.styleFloat = styles[i];
bsw/jbe@1309 10127 } else {
bsw/jbe@1309 10128 style[i] = styles[i];
bsw/jbe@1309 10129 }
bsw/jbe@1309 10130 }
bsw/jbe@1309 10131 }
bsw/jbe@1309 10132 };
bsw/jbe@1309 10133 };
bsw/jbe@1309 10134
bsw/jbe@1309 10135 /**
bsw/jbe@1309 10136 * Simulate HTML5 placeholder attribute
bsw/jbe@1309 10137 *
bsw/jbe@1309 10138 * Needed since
bsw/jbe@1309 10139 * - div[contentEditable] elements don't support it
bsw/jbe@1309 10140 * - older browsers (such as IE8 and Firefox 3.6) don't support it at all
bsw/jbe@1309 10141 *
bsw/jbe@1309 10142 * @param {Object} parent Instance of main wysihtml.Editor class
bsw/jbe@1309 10143 * @param {Element} view Instance of wysihtml.views.* class
bsw/jbe@1309 10144 * @param {String} placeholderText
bsw/jbe@1309 10145 *
bsw/jbe@1309 10146 * @example
bsw/jbe@1309 10147 * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
bsw/jbe@1309 10148 */
bsw/jbe@1309 10149 (function(dom) {
bsw/jbe@1309 10150 dom.simulatePlaceholder = function(editor, view, placeholderText, placeholderClassName) {
bsw/jbe@1309 10151 var CLASS_NAME = placeholderClassName || "wysihtml-placeholder",
bsw/jbe@1309 10152 unset = function() {
bsw/jbe@1309 10153 var composerIsVisible = view.element.offsetWidth > 0 && view.element.offsetHeight > 0;
bsw/jbe@1309 10154 if (view.hasPlaceholderSet()) {
bsw/jbe@1309 10155 view.clear();
bsw/jbe@1309 10156 view.element.focus();
bsw/jbe@1309 10157 if (composerIsVisible ) {
bsw/jbe@1309 10158 setTimeout(function() {
bsw/jbe@1309 10159 var sel = view.selection.getSelection();
bsw/jbe@1309 10160 if (!sel.focusNode || !sel.anchorNode) {
bsw/jbe@1309 10161 view.selection.selectNode(view.element.firstChild || view.element);
bsw/jbe@1309 10162 }
bsw/jbe@1309 10163 }, 0);
bsw/jbe@1309 10164 }
bsw/jbe@1309 10165 }
bsw/jbe@1309 10166 view.placeholderSet = false;
bsw/jbe@1309 10167 dom.removeClass(view.element, CLASS_NAME);
bsw/jbe@1309 10168 },
bsw/jbe@1309 10169 set = function() {
bsw/jbe@1309 10170 if (view.isEmpty() && !view.placeholderSet) {
bsw/jbe@1309 10171 view.placeholderSet = true;
bsw/jbe@1309 10172 view.setValue(placeholderText, false);
bsw/jbe@1309 10173 dom.addClass(view.element, CLASS_NAME);
bsw/jbe@1309 10174 }
bsw/jbe@1309 10175 };
bsw/jbe@1309 10176
bsw/jbe@1309 10177 editor
bsw/jbe@1309 10178 .on("set_placeholder", set)
bsw/jbe@1309 10179 .on("unset_placeholder", unset)
bsw/jbe@1309 10180 .on("focus:composer", unset)
bsw/jbe@1309 10181 .on("paste:composer", unset)
bsw/jbe@1309 10182 .on("blur:composer", set);
bsw/jbe@1309 10183
bsw/jbe@1309 10184 set();
bsw/jbe@1309 10185 };
bsw/jbe@1309 10186 })(wysihtml.dom);
bsw/jbe@1309 10187
bsw/jbe@1309 10188 (function(dom) {
bsw/jbe@1309 10189 var documentElement = document.documentElement;
bsw/jbe@1309 10190 if ("textContent" in documentElement) {
bsw/jbe@1309 10191 dom.setTextContent = function(element, text) {
bsw/jbe@1309 10192 element.textContent = text;
bsw/jbe@1309 10193 };
bsw/jbe@1309 10194
bsw/jbe@1309 10195 dom.getTextContent = function(element) {
bsw/jbe@1309 10196 return element.textContent;
bsw/jbe@1309 10197 };
bsw/jbe@1309 10198 } else if ("innerText" in documentElement) {
bsw/jbe@1309 10199 dom.setTextContent = function(element, text) {
bsw/jbe@1309 10200 element.innerText = text;
bsw/jbe@1309 10201 };
bsw/jbe@1309 10202
bsw/jbe@1309 10203 dom.getTextContent = function(element) {
bsw/jbe@1309 10204 return element.innerText;
bsw/jbe@1309 10205 };
bsw/jbe@1309 10206 } else {
bsw/jbe@1309 10207 dom.setTextContent = function(element, text) {
bsw/jbe@1309 10208 element.nodeValue = text;
bsw/jbe@1309 10209 };
bsw/jbe@1309 10210
bsw/jbe@1309 10211 dom.getTextContent = function(element) {
bsw/jbe@1309 10212 return element.nodeValue;
bsw/jbe@1309 10213 };
bsw/jbe@1309 10214 }
bsw/jbe@1309 10215 })(wysihtml.dom);
bsw/jbe@1309 10216
bsw/jbe@1309 10217 /* Unwraps element and returns list of childNodes that the node contained.
bsw/jbe@1309 10218 *
bsw/jbe@1309 10219 * Example:
bsw/jbe@1309 10220 * var childnodes = wysihtml.dom.unwrap(document.querySelector('.unwrap-me'));
bsw/jbe@1309 10221 */
bsw/jbe@1309 10222
bsw/jbe@1309 10223 wysihtml.dom.unwrap = function(node) {
bsw/jbe@1309 10224 var children = [];
bsw/jbe@1309 10225 if (node.parentNode) {
bsw/jbe@1309 10226 while (node.lastChild) {
bsw/jbe@1309 10227 children.unshift(node.lastChild);
bsw/jbe@1309 10228 wysihtml.dom.insert(node.lastChild).after(node);
bsw/jbe@1309 10229 }
bsw/jbe@1309 10230 node.parentNode.removeChild(node);
bsw/jbe@1309 10231 }
bsw/jbe@1309 10232 return children;
bsw/jbe@1309 10233 };
bsw/jbe@1309 10234
bsw/jbe@1309 10235 /**
bsw/jbe@1309 10236 * Fix most common html formatting misbehaviors of browsers implementation when inserting
bsw/jbe@1309 10237 * content via copy & paste contentEditable
bsw/jbe@1309 10238 *
bsw/jbe@1309 10239 * @author Christopher Blum
bsw/jbe@1309 10240 */
bsw/jbe@1309 10241 wysihtml.quirks.cleanPastedHTML = (function() {
bsw/jbe@1309 10242
bsw/jbe@1309 10243 var styleToRegex = function (styleStr) {
bsw/jbe@1309 10244 var trimmedStr = wysihtml.lang.string(styleStr).trim(),
bsw/jbe@1309 10245 escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
bsw/jbe@1309 10246
bsw/jbe@1309 10247 return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
bsw/jbe@1309 10248 };
bsw/jbe@1309 10249
bsw/jbe@1309 10250 var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
bsw/jbe@1309 10251 var newRules = wysihtml.lang.object(rules).clone(true),
bsw/jbe@1309 10252 tag, style;
bsw/jbe@1309 10253
bsw/jbe@1309 10254 for (tag in newRules.tags) {
bsw/jbe@1309 10255
bsw/jbe@1309 10256 if (newRules.tags.hasOwnProperty(tag)) {
bsw/jbe@1309 10257 if (newRules.tags[tag].keep_styles) {
bsw/jbe@1309 10258 for (style in newRules.tags[tag].keep_styles) {
bsw/jbe@1309 10259 if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) {
bsw/jbe@1309 10260 if (exceptStyles[style]) {
bsw/jbe@1309 10261 newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]);
bsw/jbe@1309 10262 }
bsw/jbe@1309 10263 }
bsw/jbe@1309 10264 }
bsw/jbe@1309 10265 }
bsw/jbe@1309 10266 }
bsw/jbe@1309 10267 }
bsw/jbe@1309 10268
bsw/jbe@1309 10269 return newRules;
bsw/jbe@1309 10270 };
bsw/jbe@1309 10271
bsw/jbe@1309 10272 var pickRuleset = function(ruleset, html) {
bsw/jbe@1309 10273 var pickedSet, defaultSet;
bsw/jbe@1309 10274
bsw/jbe@1309 10275 if (!ruleset) {
bsw/jbe@1309 10276 return null;
bsw/jbe@1309 10277 }
bsw/jbe@1309 10278
bsw/jbe@1309 10279 for (var i = 0, max = ruleset.length; i < max; i++) {
bsw/jbe@1309 10280 if (!ruleset[i].condition) {
bsw/jbe@1309 10281 defaultSet = ruleset[i].set;
bsw/jbe@1309 10282 }
bsw/jbe@1309 10283 if (ruleset[i].condition && ruleset[i].condition.test(html)) {
bsw/jbe@1309 10284 return ruleset[i].set;
bsw/jbe@1309 10285 }
bsw/jbe@1309 10286 }
bsw/jbe@1309 10287
bsw/jbe@1309 10288 return defaultSet;
bsw/jbe@1309 10289 };
bsw/jbe@1309 10290
bsw/jbe@1309 10291 return function(html, options) {
bsw/jbe@1309 10292 var exceptStyles = {
bsw/jbe@1309 10293 'color': wysihtml.dom.getStyle("color").from(options.referenceNode),
bsw/jbe@1309 10294 'fontSize': wysihtml.dom.getStyle("font-size").from(options.referenceNode)
bsw/jbe@1309 10295 },
bsw/jbe@1309 10296 rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
bsw/jbe@1309 10297 newHtml;
bsw/jbe@1309 10298
bsw/jbe@1309 10299 newHtml = wysihtml.dom.parse(html, {
bsw/jbe@1309 10300 "rules": rules,
bsw/jbe@1309 10301 "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content
bsw/jbe@1309 10302 "context": options.referenceNode.ownerDocument,
bsw/jbe@1309 10303 "uneditableClass": options.uneditableClass,
bsw/jbe@1309 10304 "clearInternals" : true, // don't paste temprorary selection and other markings
bsw/jbe@1309 10305 "unjoinNbsps" : true
bsw/jbe@1309 10306 });
bsw/jbe@1309 10307
bsw/jbe@1309 10308 return newHtml;
bsw/jbe@1309 10309 };
bsw/jbe@1309 10310
bsw/jbe@1309 10311 })();
bsw/jbe@1309 10312
bsw/jbe@1309 10313 /**
bsw/jbe@1309 10314 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
bsw/jbe@1309 10315 *
bsw/jbe@1309 10316 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
bsw/jbe@1309 10317 * @exaple
bsw/jbe@1309 10318 * wysihtml.quirks.ensureProperClearing(myContentEditableElement);
bsw/jbe@1309 10319 */
bsw/jbe@1309 10320 wysihtml.quirks.ensureProperClearing = (function() {
bsw/jbe@1309 10321 var clearIfNecessary = function() {
bsw/jbe@1309 10322 var element = this;
bsw/jbe@1309 10323 setTimeout(function() {
bsw/jbe@1309 10324 var innerHTML = element.innerHTML.toLowerCase();
bsw/jbe@1309 10325 if (innerHTML == "<p>&nbsp;</p>" ||
bsw/jbe@1309 10326 innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
bsw/jbe@1309 10327 element.innerHTML = "";
bsw/jbe@1309 10328 }
bsw/jbe@1309 10329 }, 0);
bsw/jbe@1309 10330 };
bsw/jbe@1309 10331
bsw/jbe@1309 10332 return function(composer) {
bsw/jbe@1309 10333 wysihtml.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
bsw/jbe@1309 10334 };
bsw/jbe@1309 10335 })();
bsw/jbe@1309 10336
bsw/jbe@1309 10337 // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
bsw/jbe@1309 10338 //
bsw/jbe@1309 10339 // In Firefox this:
bsw/jbe@1309 10340 // var d = document.createElement("div");
bsw/jbe@1309 10341 // d.innerHTML ='<a href="~"></a>';
bsw/jbe@1309 10342 // d.innerHTML;
bsw/jbe@1309 10343 // will result in:
bsw/jbe@1309 10344 // <a href="%7E"></a>
bsw/jbe@1309 10345 // which is wrong
bsw/jbe@1309 10346 (function(wysihtml) {
bsw/jbe@1309 10347 var TILDE_ESCAPED = "%7E";
bsw/jbe@1309 10348 wysihtml.quirks.getCorrectInnerHTML = function(element) {
bsw/jbe@1309 10349 var innerHTML = element.innerHTML;
bsw/jbe@1309 10350 if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
bsw/jbe@1309 10351 return innerHTML;
bsw/jbe@1309 10352 }
bsw/jbe@1309 10353
bsw/jbe@1309 10354 var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
bsw/jbe@1309 10355 url,
bsw/jbe@1309 10356 urlToSearch,
bsw/jbe@1309 10357 length,
bsw/jbe@1309 10358 i;
bsw/jbe@1309 10359 for (i=0, length=elementsWithTilde.length; i<length; i++) {
bsw/jbe@1309 10360 url = elementsWithTilde[i].href || elementsWithTilde[i].src;
bsw/jbe@1309 10361 urlToSearch = wysihtml.lang.string(url).replace("~").by(TILDE_ESCAPED);
bsw/jbe@1309 10362 innerHTML = wysihtml.lang.string(innerHTML).replace(urlToSearch).by(url);
bsw/jbe@1309 10363 }
bsw/jbe@1309 10364 return innerHTML;
bsw/jbe@1309 10365 };
bsw/jbe@1309 10366 })(wysihtml);
bsw/jbe@1309 10367
bsw/jbe@1309 10368 /**
bsw/jbe@1309 10369 * Force rerendering of a given element
bsw/jbe@1309 10370 * Needed to fix display misbehaviors of IE
bsw/jbe@1309 10371 *
bsw/jbe@1309 10372 * @param {Element} element The element object which needs to be rerendered
bsw/jbe@1309 10373 * @example
bsw/jbe@1309 10374 * wysihtml.quirks.redraw(document.body);
bsw/jbe@1309 10375 */
bsw/jbe@1309 10376 (function(wysihtml) {
bsw/jbe@1309 10377 var CLASS_NAME = "wysihtml-quirks-redraw";
bsw/jbe@1309 10378
bsw/jbe@1309 10379 wysihtml.quirks.redraw = function(element) {
bsw/jbe@1309 10380 wysihtml.dom.addClass(element, CLASS_NAME);
bsw/jbe@1309 10381 wysihtml.dom.removeClass(element, CLASS_NAME);
bsw/jbe@1309 10382
bsw/jbe@1309 10383 // Following hack is needed for firefox to make sure that image resize handles are properly removed
bsw/jbe@1309 10384 try {
bsw/jbe@1309 10385 var doc = element.ownerDocument;
bsw/jbe@1309 10386 doc.execCommand("italic", false, null);
bsw/jbe@1309 10387 doc.execCommand("italic", false, null);
bsw/jbe@1309 10388 } catch(e) {}
bsw/jbe@1309 10389 };
bsw/jbe@1309 10390 })(wysihtml);
bsw/jbe@1309 10391
bsw/jbe@1309 10392 (function(wysihtml) {
bsw/jbe@1309 10393
bsw/jbe@1309 10394 // List of supported color format parsing methods
bsw/jbe@1309 10395 // If radix is not defined 10 is expected as default
bsw/jbe@1309 10396 var colorParseMethods = {
bsw/jbe@1309 10397 rgba : {
bsw/jbe@1309 10398 regex: /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
bsw/jbe@1309 10399 name: "rgba"
bsw/jbe@1309 10400 },
bsw/jbe@1309 10401 rgb : {
bsw/jbe@1309 10402 regex: /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
bsw/jbe@1309 10403 name: "rgb"
bsw/jbe@1309 10404 },
bsw/jbe@1309 10405 hex6 : {
bsw/jbe@1309 10406 regex: /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
bsw/jbe@1309 10407 name: "hex",
bsw/jbe@1309 10408 radix: 16
bsw/jbe@1309 10409 },
bsw/jbe@1309 10410 hex3 : {
bsw/jbe@1309 10411 regex: /^#([0-9a-f])([0-9a-f])([0-9a-f])/i,
bsw/jbe@1309 10412 name: "hex",
bsw/jbe@1309 10413 radix: 16
bsw/jbe@1309 10414 }
bsw/jbe@1309 10415 },
bsw/jbe@1309 10416 // 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
bsw/jbe@1309 10417 makeParamRegExp = function (p) {
bsw/jbe@1309 10418 return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+", "gi");
bsw/jbe@1309 10419 };
bsw/jbe@1309 10420
bsw/jbe@1309 10421 // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns suitable parsing method for it
bsw/jbe@1309 10422 function getColorParseMethod (colorStr) {
bsw/jbe@1309 10423 var prop, colorTypeConf;
bsw/jbe@1309 10424
bsw/jbe@1309 10425 for (prop in colorParseMethods) {
bsw/jbe@1309 10426 if (!colorParseMethods.hasOwnProperty(prop)) { continue; }
bsw/jbe@1309 10427
bsw/jbe@1309 10428 colorTypeConf = colorParseMethods[prop];
bsw/jbe@1309 10429
bsw/jbe@1309 10430 if (colorTypeConf.regex.test(colorStr)) {
bsw/jbe@1309 10431 return colorTypeConf;
bsw/jbe@1309 10432 }
bsw/jbe@1309 10433 }
bsw/jbe@1309 10434 }
bsw/jbe@1309 10435
bsw/jbe@1309 10436 // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns the type of that color format "hex", "rgb", "rgba".
bsw/jbe@1309 10437 function getColorFormat (colorStr) {
bsw/jbe@1309 10438 var type = getColorParseMethod(colorStr);
bsw/jbe@1309 10439
bsw/jbe@1309 10440 return type ? type.name : undefined;
bsw/jbe@1309 10441 }
bsw/jbe@1309 10442
bsw/jbe@1309 10443 // Public API functions for styleParser
bsw/jbe@1309 10444 wysihtml.quirks.styleParser = {
bsw/jbe@1309 10445
bsw/jbe@1309 10446 // Takes color string value as an argument and returns suitable parsing method for it
bsw/jbe@1309 10447 getColorParseMethod : getColorParseMethod,
bsw/jbe@1309 10448
bsw/jbe@1309 10449 // Takes color string value as an argument and returns the type of that color format "hex", "rgb", "rgba".
bsw/jbe@1309 10450 getColorFormat : getColorFormat,
bsw/jbe@1309 10451
bsw/jbe@1309 10452 /* Parses a color string to and array of [red, green, blue, alpha].
bsw/jbe@1309 10453 * paramName: optional argument to parse color value directly from style string parameter
bsw/jbe@1309 10454 *
bsw/jbe@1309 10455 * Examples:
bsw/jbe@1309 10456 * var colorArray = wysihtml.quirks.styleParser.parseColor("#ABC"); // [170, 187, 204, 1]
bsw/jbe@1309 10457 * var colorArray = wysihtml.quirks.styleParser.parseColor("#AABBCC"); // [170, 187, 204, 1]
bsw/jbe@1309 10458 * var colorArray = wysihtml.quirks.styleParser.parseColor("rgb(1,2,3)"); // [1, 2, 3, 1]
bsw/jbe@1309 10459 * var colorArray = wysihtml.quirks.styleParser.parseColor("rgba(1,2,3,0.5)"); // [1, 2, 3, 0.5]
bsw/jbe@1309 10460 *
bsw/jbe@1309 10461 * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1]
bsw/jbe@1309 10462 * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color"); // [0, 0, 0, 1]
bsw/jbe@1309 10463 */
bsw/jbe@1309 10464 parseColor : function (stylesStr, paramName) {
bsw/jbe@1309 10465 var paramsRegex, params, colorType, colorMatch, radix,
bsw/jbe@1309 10466 colorStr = stylesStr;
bsw/jbe@1309 10467
bsw/jbe@1309 10468 if (paramName) {
bsw/jbe@1309 10469 paramsRegex = makeParamRegExp(paramName);
bsw/jbe@1309 10470
bsw/jbe@1309 10471 if (!(params = stylesStr.match(paramsRegex))) { return false; }
bsw/jbe@1309 10472
bsw/jbe@1309 10473 params = params.pop().split(":")[1];
bsw/jbe@1309 10474 colorStr = wysihtml.lang.string(params).trim();
bsw/jbe@1309 10475 }
bsw/jbe@1309 10476
bsw/jbe@1309 10477 if (!(colorType = getColorParseMethod(colorStr))) { return false; }
bsw/jbe@1309 10478 if (!(colorMatch = colorStr.match(colorType.regex))) { return false; }
bsw/jbe@1309 10479
bsw/jbe@1309 10480 radix = colorType.radix || 10;
bsw/jbe@1309 10481
bsw/jbe@1309 10482 if (colorType === colorParseMethods.hex3) {
bsw/jbe@1309 10483 colorMatch.shift();
bsw/jbe@1309 10484 colorMatch.push(1);
bsw/jbe@1309 10485 return wysihtml.lang.array(colorMatch).map(function(d, idx) {
bsw/jbe@1309 10486 return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d);
bsw/jbe@1309 10487 });
bsw/jbe@1309 10488 }
bsw/jbe@1309 10489
bsw/jbe@1309 10490 colorMatch.shift();
bsw/jbe@1309 10491
bsw/jbe@1309 10492 if (!colorMatch[3]) {
bsw/jbe@1309 10493 colorMatch.push(1);
bsw/jbe@1309 10494 }
bsw/jbe@1309 10495
bsw/jbe@1309 10496 return wysihtml.lang.array(colorMatch).map(function(d, idx) {
bsw/jbe@1309 10497 return (idx < 3) ? parseInt(d, radix): parseFloat(d);
bsw/jbe@1309 10498 });
bsw/jbe@1309 10499 },
bsw/jbe@1309 10500
bsw/jbe@1309 10501 /* Takes rgba color array [r,g,b,a] as a value and formats it to color string with given format type
bsw/jbe@1309 10502 * If no format is given, rgba/rgb is returned based on alpha value
bsw/jbe@1309 10503 *
bsw/jbe@1309 10504 * Example:
bsw/jbe@1309 10505 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash"); // "#AABBCC"
bsw/jbe@1309 10506 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex"); // "AABBCC"
bsw/jbe@1309 10507 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv"); // "170, 187, 204, 1"
bsw/jbe@1309 10508 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba"); // "rgba(170,187,204,1)"
bsw/jbe@1309 10509 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgb"); // "rgb(170,187,204)"
bsw/jbe@1309 10510 *
bsw/jbe@1309 10511 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 0.5]); // "rgba(170,187,204,0.5)"
bsw/jbe@1309 10512 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1]); // "rgb(170,187,204)"
bsw/jbe@1309 10513 */
bsw/jbe@1309 10514 unparseColor: function(val, colorFormat) {
bsw/jbe@1309 10515 var hexRadix = 16;
bsw/jbe@1309 10516
bsw/jbe@1309 10517 if (colorFormat === "hex") {
bsw/jbe@1309 10518 return (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
bsw/jbe@1309 10519 } else if (colorFormat === "hash") {
bsw/jbe@1309 10520 return "#" + (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
bsw/jbe@1309 10521 } else if (colorFormat === "rgb") {
bsw/jbe@1309 10522 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
bsw/jbe@1309 10523 } else if (colorFormat === "rgba") {
bsw/jbe@1309 10524 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
bsw/jbe@1309 10525 } else if (colorFormat === "csv") {
bsw/jbe@1309 10526 return val[0] + "," + val[1] + "," + val[2] + "," + val[3];
bsw/jbe@1309 10527 }
bsw/jbe@1309 10528
bsw/jbe@1309 10529 if (val[3] && val[3] !== 1) {
bsw/jbe@1309 10530 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
bsw/jbe@1309 10531 } else {
bsw/jbe@1309 10532 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
bsw/jbe@1309 10533 }
bsw/jbe@1309 10534 },
bsw/jbe@1309 10535
bsw/jbe@1309 10536 // Parses font size value from style string
bsw/jbe@1309 10537 parseFontSize: function(stylesStr) {
bsw/jbe@1309 10538 var params = stylesStr.match(makeParamRegExp("font-size"));
bsw/jbe@1309 10539 if (params) {
bsw/jbe@1309 10540 return wysihtml.lang.string(params[params.length - 1].split(":")[1]).trim();
bsw/jbe@1309 10541 }
bsw/jbe@1309 10542 return false;
bsw/jbe@1309 10543 }
bsw/jbe@1309 10544 };
bsw/jbe@1309 10545
bsw/jbe@1309 10546 })(wysihtml);
bsw/jbe@1309 10547
bsw/jbe@1309 10548 /**
bsw/jbe@1309 10549 * Selection API
bsw/jbe@1309 10550 *
bsw/jbe@1309 10551 * @example
bsw/jbe@1309 10552 * var selection = new wysihtml.Selection(editor);
bsw/jbe@1309 10553 */
bsw/jbe@1309 10554 (function(wysihtml) {
bsw/jbe@1309 10555 var dom = wysihtml.dom;
bsw/jbe@1309 10556
bsw/jbe@1309 10557 function _getCumulativeOffsetTop(element) {
bsw/jbe@1309 10558 var top = 0;
bsw/jbe@1309 10559 if (element.parentNode) {
bsw/jbe@1309 10560 do {
bsw/jbe@1309 10561 top += element.offsetTop || 0;
bsw/jbe@1309 10562 element = element.offsetParent;
bsw/jbe@1309 10563 } while (element);
bsw/jbe@1309 10564 }
bsw/jbe@1309 10565 return top;
bsw/jbe@1309 10566 }
bsw/jbe@1309 10567
bsw/jbe@1309 10568 // Provides the depth of ``descendant`` relative to ``ancestor``
bsw/jbe@1309 10569 function getDepth(ancestor, descendant) {
bsw/jbe@1309 10570 var ret = 0;
bsw/jbe@1309 10571 while (descendant !== ancestor) {
bsw/jbe@1309 10572 ret++;
bsw/jbe@1309 10573 descendant = descendant.parentNode;
bsw/jbe@1309 10574 if (!descendant)
bsw/jbe@1309 10575 throw new Error("not a descendant of ancestor!");
bsw/jbe@1309 10576 }
bsw/jbe@1309 10577 return ret;
bsw/jbe@1309 10578 }
bsw/jbe@1309 10579
bsw/jbe@1309 10580 function getRangeNode(node, offset) {
bsw/jbe@1309 10581 if (node.nodeType === 3) {
bsw/jbe@1309 10582 return node;
bsw/jbe@1309 10583 } else {
bsw/jbe@1309 10584 return node.childNodes[offset] || node;
bsw/jbe@1309 10585 }
bsw/jbe@1309 10586 }
bsw/jbe@1309 10587
bsw/jbe@1309 10588 function getWebkitSelectionFixNode(container) {
bsw/jbe@1309 10589 var blankNode = document.createElement('span');
bsw/jbe@1309 10590
bsw/jbe@1309 10591 var placeholderRemover = function(event) {
bsw/jbe@1309 10592 // Self-destructs the caret and keeps the text inserted into it by user
bsw/jbe@1309 10593 var lastChild;
bsw/jbe@1309 10594
bsw/jbe@1309 10595 container.removeEventListener('mouseup', placeholderRemover);
bsw/jbe@1309 10596 container.removeEventListener('keydown', placeholderRemover);
bsw/jbe@1309 10597 container.removeEventListener('touchstart', placeholderRemover);
bsw/jbe@1309 10598 container.removeEventListener('focus', placeholderRemover);
bsw/jbe@1309 10599 container.removeEventListener('blur', placeholderRemover);
bsw/jbe@1309 10600 container.removeEventListener('paste', delayedPlaceholderRemover);
bsw/jbe@1309 10601 container.removeEventListener('drop', delayedPlaceholderRemover);
bsw/jbe@1309 10602 container.removeEventListener('beforepaste', delayedPlaceholderRemover);
bsw/jbe@1309 10603
bsw/jbe@1309 10604 if (blankNode && blankNode.parentNode) {
bsw/jbe@1309 10605 blankNode.parentNode.removeChild(blankNode);
bsw/jbe@1309 10606 }
bsw/jbe@1309 10607 },
bsw/jbe@1309 10608 delayedPlaceholderRemover = function (event) {
bsw/jbe@1309 10609 if (blankNode && blankNode.parentNode) {
bsw/jbe@1309 10610 setTimeout(placeholderRemover, 0);
bsw/jbe@1309 10611 }
bsw/jbe@1309 10612 };
bsw/jbe@1309 10613
bsw/jbe@1309 10614 blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml.INVISIBLE_SPACE));
bsw/jbe@1309 10615 blankNode.className = '_wysihtml-temp-caret-fix';
bsw/jbe@1309 10616 blankNode.style.display = 'block';
bsw/jbe@1309 10617 blankNode.style.minWidth = '1px';
bsw/jbe@1309 10618 blankNode.style.height = '0px';
bsw/jbe@1309 10619
bsw/jbe@1309 10620 container.addEventListener('mouseup', placeholderRemover);
bsw/jbe@1309 10621 container.addEventListener('keydown', placeholderRemover);
bsw/jbe@1309 10622 container.addEventListener('touchstart', placeholderRemover);
bsw/jbe@1309 10623 container.addEventListener('focus', placeholderRemover);
bsw/jbe@1309 10624 container.addEventListener('blur', placeholderRemover);
bsw/jbe@1309 10625 container.addEventListener('paste', delayedPlaceholderRemover);
bsw/jbe@1309 10626 container.addEventListener('drop', delayedPlaceholderRemover);
bsw/jbe@1309 10627 container.addEventListener('beforepaste', delayedPlaceholderRemover);
bsw/jbe@1309 10628
bsw/jbe@1309 10629 return blankNode;
bsw/jbe@1309 10630 }
bsw/jbe@1309 10631
bsw/jbe@1309 10632 // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
bsw/jbe@1309 10633 // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
bsw/jbe@1309 10634 function expandRangeToSurround(range) {
bsw/jbe@1309 10635 if (range.canSurroundContents()) return;
bsw/jbe@1309 10636
bsw/jbe@1309 10637 var common = range.commonAncestorContainer,
bsw/jbe@1309 10638 start_depth = getDepth(common, range.startContainer),
bsw/jbe@1309 10639 end_depth = getDepth(common, range.endContainer);
bsw/jbe@1309 10640
bsw/jbe@1309 10641 while(!range.canSurroundContents()) {
bsw/jbe@1309 10642 // 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.
bsw/jbe@1309 10643 if (start_depth > end_depth) {
bsw/jbe@1309 10644 range.setStartBefore(range.startContainer);
bsw/jbe@1309 10645 start_depth = getDepth(common, range.startContainer);
bsw/jbe@1309 10646 }
bsw/jbe@1309 10647 else {
bsw/jbe@1309 10648 range.setEndAfter(range.endContainer);
bsw/jbe@1309 10649 end_depth = getDepth(common, range.endContainer);
bsw/jbe@1309 10650 }
bsw/jbe@1309 10651 }
bsw/jbe@1309 10652 }
bsw/jbe@1309 10653
bsw/jbe@1309 10654 wysihtml.Selection = Base.extend(
bsw/jbe@1309 10655 /** @scope wysihtml.Selection.prototype */ {
bsw/jbe@1309 10656 constructor: function(editor, contain, unselectableClass) {
bsw/jbe@1309 10657 // Make sure that our external range library is initialized
bsw/jbe@1309 10658 rangy.init();
bsw/jbe@1309 10659
bsw/jbe@1309 10660 this.editor = editor;
bsw/jbe@1309 10661 this.composer = editor.composer;
bsw/jbe@1309 10662 this.doc = this.composer.doc;
bsw/jbe@1309 10663 this.win = this.composer.win;
bsw/jbe@1309 10664 this.contain = contain;
bsw/jbe@1309 10665 this.unselectableClass = unselectableClass || false;
bsw/jbe@1309 10666 },
bsw/jbe@1309 10667
bsw/jbe@1309 10668 /**
bsw/jbe@1309 10669 * Get the current selection as a bookmark to be able to later restore it
bsw/jbe@1309 10670 *
bsw/jbe@1309 10671 * @return {Object} An object that represents the current selection
bsw/jbe@1309 10672 */
bsw/jbe@1309 10673 getBookmark: function() {
bsw/jbe@1309 10674 var range = this.getRange();
bsw/jbe@1309 10675 return range && range.cloneRange();
bsw/jbe@1309 10676 },
bsw/jbe@1309 10677
bsw/jbe@1309 10678 /**
bsw/jbe@1309 10679 * Restore a selection retrieved via wysihtml.Selection.prototype.getBookmark
bsw/jbe@1309 10680 *
bsw/jbe@1309 10681 * @param {Object} bookmark An object that represents the current selection
bsw/jbe@1309 10682 */
bsw/jbe@1309 10683 setBookmark: function(bookmark) {
bsw/jbe@1309 10684 if (!bookmark) {
bsw/jbe@1309 10685 return;
bsw/jbe@1309 10686 }
bsw/jbe@1309 10687
bsw/jbe@1309 10688 this.setSelection(bookmark);
bsw/jbe@1309 10689 },
bsw/jbe@1309 10690
bsw/jbe@1309 10691 /**
bsw/jbe@1309 10692 * Set the caret in front of the given node
bsw/jbe@1309 10693 *
bsw/jbe@1309 10694 * @param {Object} node The element or text node where to position the caret in front of
bsw/jbe@1309 10695 * @example
bsw/jbe@1309 10696 * selection.setBefore(myElement);
bsw/jbe@1309 10697 */
bsw/jbe@1309 10698 setBefore: function(node) {
bsw/jbe@1309 10699 var range = rangy.createRange(this.doc);
bsw/jbe@1309 10700 range.setStartBefore(node);
bsw/jbe@1309 10701 range.setEndBefore(node);
bsw/jbe@1309 10702 return this.setSelection(range);
bsw/jbe@1309 10703 },
bsw/jbe@1309 10704
bsw/jbe@1309 10705 // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail.
bsw/jbe@1309 10706 // Webkit has an issue with placing caret into places where there are no textnodes near by.
bsw/jbe@1309 10707 createTemporaryCaretSpaceAfter: function (node) {
bsw/jbe@1309 10708 var caretPlaceholder = this.doc.createElement('span'),
bsw/jbe@1309 10709 caretPlaceholderText = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE),
bsw/jbe@1309 10710 placeholderRemover = (function(event) {
bsw/jbe@1309 10711 // Self-destructs the caret and keeps the text inserted into it by user
bsw/jbe@1309 10712 var lastChild;
bsw/jbe@1309 10713
bsw/jbe@1309 10714 this.contain.removeEventListener('mouseup', placeholderRemover);
bsw/jbe@1309 10715 this.contain.removeEventListener('keydown', keyDownHandler);
bsw/jbe@1309 10716 this.contain.removeEventListener('touchstart', placeholderRemover);
bsw/jbe@1309 10717 this.contain.removeEventListener('focus', placeholderRemover);
bsw/jbe@1309 10718 this.contain.removeEventListener('blur', placeholderRemover);
bsw/jbe@1309 10719 this.contain.removeEventListener('paste', delayedPlaceholderRemover);
bsw/jbe@1309 10720 this.contain.removeEventListener('drop', delayedPlaceholderRemover);
bsw/jbe@1309 10721 this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover);
bsw/jbe@1309 10722
bsw/jbe@1309 10723 // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack
bsw/jbe@1309 10724 // Otherwise the wrapper can just be removed
bsw/jbe@1309 10725 if (caretPlaceholder && caretPlaceholder.parentNode) {
bsw/jbe@1309 10726 caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309 10727 if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) {
bsw/jbe@1309 10728 lastChild = caretPlaceholder.lastChild;
bsw/jbe@1309 10729 wysihtml.dom.unwrap(caretPlaceholder);
bsw/jbe@1309 10730 this.setAfter(lastChild);
bsw/jbe@1309 10731 } else {
bsw/jbe@1309 10732 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
bsw/jbe@1309 10733 }
bsw/jbe@1309 10734
bsw/jbe@1309 10735 }
bsw/jbe@1309 10736 }).bind(this),
bsw/jbe@1309 10737 delayedPlaceholderRemover = function (event) {
bsw/jbe@1309 10738 if (caretPlaceholder && caretPlaceholder.parentNode) {
bsw/jbe@1309 10739 setTimeout(placeholderRemover, 0);
bsw/jbe@1309 10740 }
bsw/jbe@1309 10741 },
bsw/jbe@1309 10742 keyDownHandler = function(event) {
bsw/jbe@1309 10743 if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) {
bsw/jbe@1309 10744 placeholderRemover();
bsw/jbe@1309 10745 }
bsw/jbe@1309 10746 };
bsw/jbe@1309 10747
bsw/jbe@1309 10748 caretPlaceholder.className = '_wysihtml-temp-caret-fix';
bsw/jbe@1309 10749 caretPlaceholder.style.position = 'absolute';
bsw/jbe@1309 10750 caretPlaceholder.style.display = 'block';
bsw/jbe@1309 10751 caretPlaceholder.style.minWidth = '1px';
bsw/jbe@1309 10752 caretPlaceholder.style.zIndex = '99999';
bsw/jbe@1309 10753 caretPlaceholder.appendChild(caretPlaceholderText);
bsw/jbe@1309 10754
bsw/jbe@1309 10755 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
bsw/jbe@1309 10756 this.setBefore(caretPlaceholderText);
bsw/jbe@1309 10757
bsw/jbe@1309 10758 // Remove the caret fix on any of the following events (some are delayed as content change happens after event)
bsw/jbe@1309 10759 this.contain.addEventListener('mouseup', placeholderRemover);
bsw/jbe@1309 10760 this.contain.addEventListener('keydown', keyDownHandler);
bsw/jbe@1309 10761 this.contain.addEventListener('touchstart', placeholderRemover);
bsw/jbe@1309 10762 this.contain.addEventListener('focus', placeholderRemover);
bsw/jbe@1309 10763 this.contain.addEventListener('blur', placeholderRemover);
bsw/jbe@1309 10764 this.contain.addEventListener('paste', delayedPlaceholderRemover);
bsw/jbe@1309 10765 this.contain.addEventListener('drop', delayedPlaceholderRemover);
bsw/jbe@1309 10766 this.contain.addEventListener('beforepaste', delayedPlaceholderRemover);
bsw/jbe@1309 10767
bsw/jbe@1309 10768 return caretPlaceholder;
bsw/jbe@1309 10769 },
bsw/jbe@1309 10770
bsw/jbe@1309 10771 /**
bsw/jbe@1309 10772 * Set the caret after the given node
bsw/jbe@1309 10773 *
bsw/jbe@1309 10774 * @param {Object} node The element or text node where to position the caret in front of
bsw/jbe@1309 10775 * @example
bsw/jbe@1309 10776 * selection.setBefore(myElement);
bsw/jbe@1309 10777 * callback is an optional parameter accepting a function to execute when selection ahs been set
bsw/jbe@1309 10778 */
bsw/jbe@1309 10779 setAfter: function(node, notVisual, callback) {
bsw/jbe@1309 10780 var win = this.win,
bsw/jbe@1309 10781 range = rangy.createRange(this.doc),
bsw/jbe@1309 10782 fixWebkitSelection = function() {
bsw/jbe@1309 10783 // Webkit fails to add selection if there are no textnodes in that region
bsw/jbe@1309 10784 // (like an uneditable container at the end of content).
bsw/jbe@1309 10785 var parent = node.parentNode,
bsw/jbe@1309 10786 lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null;
bsw/jbe@1309 10787
bsw/jbe@1309 10788 if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) {
bsw/jbe@1309 10789 if (notVisual) {
bsw/jbe@1309 10790 // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation
bsw/jbe@1309 10791 // and remove itself in call stack end instead on user interaction
bsw/jbe@1309 10792 var caretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309 10793 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
bsw/jbe@1309 10794 this.selectNode(caretPlaceholder);
bsw/jbe@1309 10795 setTimeout(function() {
bsw/jbe@1309 10796 if (caretPlaceholder && caretPlaceholder.parentNode) {
bsw/jbe@1309 10797 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
bsw/jbe@1309 10798 }
bsw/jbe@1309 10799 }, 0);
bsw/jbe@1309 10800 } else {
bsw/jbe@1309 10801 this.createTemporaryCaretSpaceAfter(node);
bsw/jbe@1309 10802 }
bsw/jbe@1309 10803 }
bsw/jbe@1309 10804 }.bind(this),
bsw/jbe@1309 10805 sel;
bsw/jbe@1309 10806
bsw/jbe@1309 10807 range.setStartAfter(node);
bsw/jbe@1309 10808 range.setEndAfter(node);
bsw/jbe@1309 10809
bsw/jbe@1309 10810 // In IE contenteditable must be focused before we can set selection
bsw/jbe@1309 10811 // thus setting the focus if activeElement is not this composer
bsw/jbe@1309 10812 if (!document.activeElement || document.activeElement !== this.composer.element) {
bsw/jbe@1309 10813 var scrollPos = this.composer.getScrollPos();
bsw/jbe@1309 10814 this.composer.element.focus();
bsw/jbe@1309 10815 this.composer.setScrollPos(scrollPos);
bsw/jbe@1309 10816 setTimeout(function() {
bsw/jbe@1309 10817 sel = this.setSelection(range);
bsw/jbe@1309 10818 fixWebkitSelection();
bsw/jbe@1309 10819 if (callback) {
bsw/jbe@1309 10820 callback(sel);
bsw/jbe@1309 10821 }
bsw/jbe@1309 10822 }.bind(this), 0);
bsw/jbe@1309 10823 } else {
bsw/jbe@1309 10824 sel = this.setSelection(range);
bsw/jbe@1309 10825 fixWebkitSelection();
bsw/jbe@1309 10826 if (callback) {
bsw/jbe@1309 10827 callback(sel);
bsw/jbe@1309 10828 }
bsw/jbe@1309 10829 }
bsw/jbe@1309 10830 },
bsw/jbe@1309 10831
bsw/jbe@1309 10832 /**
bsw/jbe@1309 10833 * Ability to select/mark nodes
bsw/jbe@1309 10834 *
bsw/jbe@1309 10835 * @param {Element} node The node/element to select
bsw/jbe@1309 10836 * @example
bsw/jbe@1309 10837 * selection.selectNode(document.getElementById("my-image"));
bsw/jbe@1309 10838 */
bsw/jbe@1309 10839 selectNode: function(node, avoidInvisibleSpace) {
bsw/jbe@1309 10840 var range = rangy.createRange(this.doc),
bsw/jbe@1309 10841 isElement = node.nodeType === wysihtml.ELEMENT_NODE,
bsw/jbe@1309 10842 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
bsw/jbe@1309 10843 content = isElement ? node.innerHTML : node.data,
bsw/jbe@1309 10844 isEmpty = (content === "" || content === wysihtml.INVISIBLE_SPACE),
bsw/jbe@1309 10845 displayStyle = dom.getStyle("display").from(node),
bsw/jbe@1309 10846 isBlockElement = (displayStyle === "block" || displayStyle === "list-item");
bsw/jbe@1309 10847
bsw/jbe@1309 10848 if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
bsw/jbe@1309 10849 // Make sure that caret is visible in node by inserting a zero width no breaking space
bsw/jbe@1309 10850 try { node.innerHTML = wysihtml.INVISIBLE_SPACE; } catch(e) {}
bsw/jbe@1309 10851 }
bsw/jbe@1309 10852 if (canHaveHTML) {
bsw/jbe@1309 10853 range.selectNodeContents(node);
bsw/jbe@1309 10854 } else {
bsw/jbe@1309 10855 range.selectNode(node);
bsw/jbe@1309 10856 }
bsw/jbe@1309 10857
bsw/jbe@1309 10858 if (canHaveHTML && isEmpty && isElement) {
bsw/jbe@1309 10859 range.collapse(isBlockElement);
bsw/jbe@1309 10860 } else if (canHaveHTML && isEmpty) {
bsw/jbe@1309 10861 range.setStartAfter(node);
bsw/jbe@1309 10862 range.setEndAfter(node);
bsw/jbe@1309 10863 }
bsw/jbe@1309 10864
bsw/jbe@1309 10865 this.setSelection(range);
bsw/jbe@1309 10866 },
bsw/jbe@1309 10867
bsw/jbe@1309 10868 /**
bsw/jbe@1309 10869 * Get the node which contains the selection
bsw/jbe@1309 10870 *
bsw/jbe@1309 10871 * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
bsw/jbe@1309 10872 * @return {Object} The node that contains the caret
bsw/jbe@1309 10873 * @example
bsw/jbe@1309 10874 * var nodeThatContainsCaret = selection.getSelectedNode();
bsw/jbe@1309 10875 */
bsw/jbe@1309 10876 getSelectedNode: function(controlRange) {
bsw/jbe@1309 10877 var selection,
bsw/jbe@1309 10878 range;
bsw/jbe@1309 10879
bsw/jbe@1309 10880 if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
bsw/jbe@1309 10881 range = this.doc.selection.createRange();
bsw/jbe@1309 10882 if (range && range.length) {
bsw/jbe@1309 10883 return range.item(0);
bsw/jbe@1309 10884 }
bsw/jbe@1309 10885 }
bsw/jbe@1309 10886
bsw/jbe@1309 10887 selection = this.getSelection(this.doc);
bsw/jbe@1309 10888 if (selection.focusNode === selection.anchorNode) {
bsw/jbe@1309 10889 return selection.focusNode;
bsw/jbe@1309 10890 } else {
bsw/jbe@1309 10891 range = this.getRange(this.doc);
bsw/jbe@1309 10892 return range ? range.commonAncestorContainer : this.doc.body;
bsw/jbe@1309 10893 }
bsw/jbe@1309 10894 },
bsw/jbe@1309 10895
bsw/jbe@1309 10896 fixSelBorders: function() {
bsw/jbe@1309 10897 var range = this.getRange();
bsw/jbe@1309 10898 expandRangeToSurround(range);
bsw/jbe@1309 10899 this.setSelection(range);
bsw/jbe@1309 10900 },
bsw/jbe@1309 10901
bsw/jbe@1309 10902 getSelectedOwnNodes: function(controlRange) {
bsw/jbe@1309 10903 var selection,
bsw/jbe@1309 10904 ranges = this.getOwnRanges(),
bsw/jbe@1309 10905 ownNodes = [];
bsw/jbe@1309 10906
bsw/jbe@1309 10907 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309 10908 ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
bsw/jbe@1309 10909 }
bsw/jbe@1309 10910 return ownNodes;
bsw/jbe@1309 10911 },
bsw/jbe@1309 10912
bsw/jbe@1309 10913 findNodesInSelection: function(nodeTypes) {
bsw/jbe@1309 10914 var ranges = this.getOwnRanges(),
bsw/jbe@1309 10915 nodes = [], curNodes;
bsw/jbe@1309 10916 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309 10917 curNodes = ranges[i].getNodes([1], function(node) {
bsw/jbe@1309 10918 return wysihtml.lang.array(nodeTypes).contains(node.nodeName);
bsw/jbe@1309 10919 });
bsw/jbe@1309 10920 nodes = nodes.concat(curNodes);
bsw/jbe@1309 10921 }
bsw/jbe@1309 10922 return nodes;
bsw/jbe@1309 10923 },
bsw/jbe@1309 10924
bsw/jbe@1309 10925 filterElements: function(filter) {
bsw/jbe@1309 10926 var ranges = this.getOwnRanges(),
bsw/jbe@1309 10927 nodes = [], curNodes;
bsw/jbe@1309 10928
bsw/jbe@1309 10929 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309 10930 curNodes = ranges[i].getNodes([1], function(element){
bsw/jbe@1309 10931 return filter(element, ranges[i]);
bsw/jbe@1309 10932 });
bsw/jbe@1309 10933 nodes = nodes.concat(curNodes);
bsw/jbe@1309 10934 }
bsw/jbe@1309 10935 return nodes;
bsw/jbe@1309 10936 },
bsw/jbe@1309 10937
bsw/jbe@1309 10938 containsUneditable: function() {
bsw/jbe@1309 10939 var uneditables = this.getOwnUneditables(),
bsw/jbe@1309 10940 selection = this.getSelection();
bsw/jbe@1309 10941
bsw/jbe@1309 10942 for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
bsw/jbe@1309 10943 if (selection.containsNode(uneditables[i])) {
bsw/jbe@1309 10944 return true;
bsw/jbe@1309 10945 }
bsw/jbe@1309 10946 }
bsw/jbe@1309 10947
bsw/jbe@1309 10948 return false;
bsw/jbe@1309 10949 },
bsw/jbe@1309 10950
bsw/jbe@1309 10951 // Deletes selection contents making sure uneditables/unselectables are not partially deleted
bsw/jbe@1309 10952 // Triggers wysihtml:uneditable:delete custom event on all deleted uneditables if customevents suppoorted
bsw/jbe@1309 10953 deleteContents: function() {
bsw/jbe@1309 10954 var range = this.getRange();
bsw/jbe@1309 10955 this.deleteRangeContents(range);
bsw/jbe@1309 10956 this.setSelection(range);
bsw/jbe@1309 10957 },
bsw/jbe@1309 10958
bsw/jbe@1309 10959 // Makes sure all uneditable sare notified before deleting contents
bsw/jbe@1309 10960 deleteRangeContents: function (range) {
bsw/jbe@1309 10961 var startParent, endParent, uneditables, ev;
bsw/jbe@1309 10962
bsw/jbe@1309 10963 if (this.unselectableClass) {
bsw/jbe@1309 10964 if ((startParent = wysihtml.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
bsw/jbe@1309 10965 range.setStartBefore(startParent);
bsw/jbe@1309 10966 }
bsw/jbe@1309 10967 if ((endParent = wysihtml.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
bsw/jbe@1309 10968 range.setEndAfter(endParent);
bsw/jbe@1309 10969 }
bsw/jbe@1309 10970
bsw/jbe@1309 10971 // If customevents present notify uneditable elements of being deleted
bsw/jbe@1309 10972 uneditables = range.getNodes([1], (function (node) {
bsw/jbe@1309 10973 return wysihtml.dom.hasClass(node, this.unselectableClass);
bsw/jbe@1309 10974 }).bind(this));
bsw/jbe@1309 10975 for (var i = uneditables.length; i--;) {
bsw/jbe@1309 10976 try {
bsw/jbe@1309 10977 ev = new CustomEvent("wysihtml:uneditable:delete");
bsw/jbe@1309 10978 uneditables[i].dispatchEvent(ev);
bsw/jbe@1309 10979 } catch (err) {}
bsw/jbe@1309 10980 }
bsw/jbe@1309 10981 }
bsw/jbe@1309 10982 range.deleteContents();
bsw/jbe@1309 10983 },
bsw/jbe@1309 10984
bsw/jbe@1309 10985 getCaretNode: function () {
bsw/jbe@1309 10986 var selection = this.getSelection();
bsw/jbe@1309 10987 return (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
bsw/jbe@1309 10988 },
bsw/jbe@1309 10989
bsw/jbe@1309 10990 getPreviousNode: function(node, ignoreEmpty) {
bsw/jbe@1309 10991 var displayStyle;
bsw/jbe@1309 10992 if (!node) {
bsw/jbe@1309 10993 var selection = this.getSelection();
bsw/jbe@1309 10994 node = (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
bsw/jbe@1309 10995 }
bsw/jbe@1309 10996
bsw/jbe@1309 10997 if (node === this.contain) {
bsw/jbe@1309 10998 return false;
bsw/jbe@1309 10999 }
bsw/jbe@1309 11000
bsw/jbe@1309 11001 var ret = node.previousSibling,
bsw/jbe@1309 11002 parent;
bsw/jbe@1309 11003
bsw/jbe@1309 11004 if (ret === this.contain) {
bsw/jbe@1309 11005 return false;
bsw/jbe@1309 11006 }
bsw/jbe@1309 11007
bsw/jbe@1309 11008 if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
bsw/jbe@1309 11009 // do not count comments and other node types
bsw/jbe@1309 11010 ret = this.getPreviousNode(ret, ignoreEmpty);
bsw/jbe@1309 11011 } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
bsw/jbe@1309 11012 // do not count empty textnodes as previous nodes
bsw/jbe@1309 11013 ret = this.getPreviousNode(ret, ignoreEmpty);
bsw/jbe@1309 11014 } else if (ignoreEmpty && ret && ret.nodeType === 1) {
bsw/jbe@1309 11015 // Do not count empty nodes if param set.
bsw/jbe@1309 11016 // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like
bsw/jbe@1309 11017 displayStyle = wysihtml.dom.getStyle("display").from(ret);
bsw/jbe@1309 11018 if (
bsw/jbe@1309 11019 !wysihtml.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) &&
bsw/jbe@1309 11020 !wysihtml.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) &&
bsw/jbe@1309 11021 (/^[\s]*$/).test(ret.innerHTML)
bsw/jbe@1309 11022 ) {
bsw/jbe@1309 11023 ret = this.getPreviousNode(ret, ignoreEmpty);
bsw/jbe@1309 11024 }
bsw/jbe@1309 11025 } else if (!ret && node !== this.contain) {
bsw/jbe@1309 11026 parent = node.parentNode;
bsw/jbe@1309 11027 if (parent !== this.contain) {
bsw/jbe@1309 11028 ret = this.getPreviousNode(parent, ignoreEmpty);
bsw/jbe@1309 11029 }
bsw/jbe@1309 11030 }
bsw/jbe@1309 11031
bsw/jbe@1309 11032 return (ret !== this.contain) ? ret : false;
bsw/jbe@1309 11033 },
bsw/jbe@1309 11034
bsw/jbe@1309 11035 // Gather info about caret location (caret node, previous and next node)
bsw/jbe@1309 11036 getNodesNearCaret: function() {
bsw/jbe@1309 11037 if (!this.isCollapsed()) {
bsw/jbe@1309 11038 throw "Selection must be caret when using selection.getNodesNearCaret()";
bsw/jbe@1309 11039 }
bsw/jbe@1309 11040
bsw/jbe@1309 11041 var r = this.getOwnRanges(),
bsw/jbe@1309 11042 caretNode, prevNode, nextNode, offset;
bsw/jbe@1309 11043
bsw/jbe@1309 11044 if (r && r.length > 0) {
bsw/jbe@1309 11045 if (r[0].startContainer.nodeType === 1) {
bsw/jbe@1309 11046 caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1];
bsw/jbe@1309 11047 if (!caretNode && r[0].startOffset === 0) {
bsw/jbe@1309 11048 // Is first position before all nodes
bsw/jbe@1309 11049 nextNode = r[0].startContainer.childNodes[0];
bsw/jbe@1309 11050 } else if (caretNode) {
bsw/jbe@1309 11051 prevNode = caretNode.previousSibling;
bsw/jbe@1309 11052 nextNode = caretNode.nextSibling;
bsw/jbe@1309 11053 }
bsw/jbe@1309 11054 } else {
bsw/jbe@1309 11055 if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) {
bsw/jbe@1309 11056 caretNode = r[0].startContainer.previousSibling;
bsw/jbe@1309 11057 if (caretNode.nodeType === 3) {
bsw/jbe@1309 11058 offset = caretNode.data.length;
bsw/jbe@1309 11059 }
bsw/jbe@1309 11060 } else {
bsw/jbe@1309 11061 caretNode = r[0].startContainer;
bsw/jbe@1309 11062 offset = r[0].startOffset;
bsw/jbe@1309 11063 }
bsw/jbe@1309 11064 prevNode = caretNode.previousSibling;
bsw/jbe@1309 11065 nextNode = caretNode.nextSibling;
bsw/jbe@1309 11066 }
bsw/jbe@1309 11067
bsw/jbe@1309 11068 return {
bsw/jbe@1309 11069 "caretNode": caretNode,
bsw/jbe@1309 11070 "prevNode": prevNode,
bsw/jbe@1309 11071 "nextNode": nextNode,
bsw/jbe@1309 11072 "textOffset": offset
bsw/jbe@1309 11073 };
bsw/jbe@1309 11074 }
bsw/jbe@1309 11075
bsw/jbe@1309 11076 return null;
bsw/jbe@1309 11077 },
bsw/jbe@1309 11078
bsw/jbe@1309 11079 getSelectionParentsByTag: function(tagName) {
bsw/jbe@1309 11080 var nodes = this.getSelectedOwnNodes(),
bsw/jbe@1309 11081 curEl, parents = [];
bsw/jbe@1309 11082
bsw/jbe@1309 11083 for (var i = 0, maxi = nodes.length; i < maxi; i++) {
bsw/jbe@1309 11084 curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain);
bsw/jbe@1309 11085 if (curEl) {
bsw/jbe@1309 11086 parents.push(curEl);
bsw/jbe@1309 11087 }
bsw/jbe@1309 11088 }
bsw/jbe@1309 11089 return (parents.length) ? parents : null;
bsw/jbe@1309 11090 },
bsw/jbe@1309 11091
bsw/jbe@1309 11092 getRangeToNodeEnd: function() {
bsw/jbe@1309 11093 if (this.isCollapsed()) {
bsw/jbe@1309 11094 var range = this.getRange(),
bsw/jbe@1309 11095 sNode, pos, lastR;
bsw/jbe@1309 11096 if (range) {
bsw/jbe@1309 11097 sNode = range.startContainer;
bsw/jbe@1309 11098 pos = range.startOffset;
bsw/jbe@1309 11099 lastR = rangy.createRange(this.doc);
bsw/jbe@1309 11100
bsw/jbe@1309 11101 lastR.selectNodeContents(sNode);
bsw/jbe@1309 11102 lastR.setStart(sNode, pos);
bsw/jbe@1309 11103 return lastR;
bsw/jbe@1309 11104 }
bsw/jbe@1309 11105 }
bsw/jbe@1309 11106 },
bsw/jbe@1309 11107
bsw/jbe@1309 11108 getRangeToNodeBeginning: function() {
bsw/jbe@1309 11109 if (this.isCollapsed()) {
bsw/jbe@1309 11110 var range = this.getRange(),
bsw/jbe@1309 11111 sNode = range.startContainer,
bsw/jbe@1309 11112 pos = range.startOffset,
bsw/jbe@1309 11113 lastR = rangy.createRange(this.doc);
bsw/jbe@1309 11114
bsw/jbe@1309 11115 lastR.selectNodeContents(sNode);
bsw/jbe@1309 11116 lastR.setEnd(sNode, pos);
bsw/jbe@1309 11117 return lastR;
bsw/jbe@1309 11118 }
bsw/jbe@1309 11119 },
bsw/jbe@1309 11120
bsw/jbe@1309 11121 // This function returns if caret is last in a node (no textual visible content follows)
bsw/jbe@1309 11122 caretIsInTheEndOfNode: function(ignoreIfSpaceIsBeforeCaret) {
bsw/jbe@1309 11123 var r = rangy.createRange(this.doc),
bsw/jbe@1309 11124 s = this.getSelection(),
bsw/jbe@1309 11125 rangeToNodeEnd = this.getRangeToNodeEnd(),
bsw/jbe@1309 11126 endc, endtxt, beginc, begintxt;
bsw/jbe@1309 11127
bsw/jbe@1309 11128 if (rangeToNodeEnd) {
bsw/jbe@1309 11129 endc = rangeToNodeEnd.cloneContents();
bsw/jbe@1309 11130 endtxt = endc.textContent;
bsw/jbe@1309 11131
bsw/jbe@1309 11132 if ((/^\s*$/).test(endtxt)) {
bsw/jbe@1309 11133 if (ignoreIfSpaceIsBeforeCaret) {
bsw/jbe@1309 11134 beginc = this.getRangeToNodeBeginning().cloneContents();
bsw/jbe@1309 11135 begintxt = beginc.textContent;
bsw/jbe@1309 11136 return !(/[\u00A0 ][\s\uFEFF]*$/).test(begintxt);
bsw/jbe@1309 11137 } else {
bsw/jbe@1309 11138 return true;
bsw/jbe@1309 11139 }
bsw/jbe@1309 11140 } else {
bsw/jbe@1309 11141 return false;
bsw/jbe@1309 11142 }
bsw/jbe@1309 11143 } else {
bsw/jbe@1309 11144 return false;
bsw/jbe@1309 11145 }
bsw/jbe@1309 11146 },
bsw/jbe@1309 11147
bsw/jbe@1309 11148 caretIsFirstInSelection: function(includeLineBreaks) {
bsw/jbe@1309 11149 var r = rangy.createRange(this.doc),
bsw/jbe@1309 11150 s = this.getSelection(),
bsw/jbe@1309 11151 range = this.getRange(),
bsw/jbe@1309 11152 startNode = getRangeNode(range.startContainer, range.startOffset);
bsw/jbe@1309 11153
bsw/jbe@1309 11154 if (startNode) {
bsw/jbe@1309 11155 if (startNode.nodeType === wysihtml.TEXT_NODE) {
bsw/jbe@1309 11156 if (!startNode.parentNode) {
bsw/jbe@1309 11157 return false;
bsw/jbe@1309 11158 }
bsw/jbe@1309 11159 if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml.dom.domNode(startNode.previousSibling).is.block())) {
bsw/jbe@1309 11160 return false;
bsw/jbe@1309 11161 }
bsw/jbe@1309 11162 var ws = this.win.getComputedStyle(startNode.parentNode).whiteSpace;
bsw/jbe@1309 11163 return (ws === "pre" || ws === "pre-wrap") ? range.startOffset === 0 : (/^\s*$/).test(startNode.data.substr(0,range.startOffset));
bsw/jbe@1309 11164 } else if (includeLineBreaks && wysihtml.dom.domNode(startNode).is.lineBreak()) {
bsw/jbe@1309 11165 return true;
bsw/jbe@1309 11166 } else {
bsw/jbe@1309 11167 r.selectNodeContents(this.getRange().commonAncestorContainer);
bsw/jbe@1309 11168 r.collapse(true);
bsw/jbe@1309 11169 return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
bsw/jbe@1309 11170 }
bsw/jbe@1309 11171 }
bsw/jbe@1309 11172 },
bsw/jbe@1309 11173
bsw/jbe@1309 11174 caretIsInTheBeginnig: function(ofNode) {
bsw/jbe@1309 11175 var selection = this.getSelection(),
bsw/jbe@1309 11176 node = selection.anchorNode,
bsw/jbe@1309 11177 offset = selection.anchorOffset;
bsw/jbe@1309 11178 if (ofNode && node) {
bsw/jbe@1309 11179 return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml.dom.getParentElement(node.parentNode, { query: ofNode }, 1)));
bsw/jbe@1309 11180 } else if (node) {
bsw/jbe@1309 11181 return (offset === 0 && !this.getPreviousNode(node, true));
bsw/jbe@1309 11182 }
bsw/jbe@1309 11183 },
bsw/jbe@1309 11184
bsw/jbe@1309 11185 // Returns object describing node/text before selection
bsw/jbe@1309 11186 // If includePrevLeaves is true returns also previous last leaf child if selection is in the beginning of current node
bsw/jbe@1309 11187 getBeforeSelection: function(includePrevLeaves) {
bsw/jbe@1309 11188 var sel = this.getSelection(),
bsw/jbe@1309 11189 startNode = (sel.isBackwards()) ? sel.focusNode : sel.anchorNode,
bsw/jbe@1309 11190 startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset,
bsw/jbe@1309 11191 rng = this.createRange(), endNode, inTmpCaret;
bsw/jbe@1309 11192
bsw/jbe@1309 11193 // If start is textnode and all is whitespace before caret. Set start offset to 0
bsw/jbe@1309 11194 if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) {
bsw/jbe@1309 11195 startOffset = 0;
bsw/jbe@1309 11196 }
bsw/jbe@1309 11197
bsw/jbe@1309 11198 // Escape temproray helper nodes if selection in them
bsw/jbe@1309 11199 inTmpCaret = wysihtml.dom.getParentElement(startNode, { query: '._wysihtml-temp-caret-fix' }, 1);
bsw/jbe@1309 11200 if (inTmpCaret) {
bsw/jbe@1309 11201 startNode = inTmpCaret.parentNode;
bsw/jbe@1309 11202 startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret);
bsw/jbe@1309 11203 }
bsw/jbe@1309 11204
bsw/jbe@1309 11205 if (startNode) {
bsw/jbe@1309 11206 if (startOffset > 0) {
bsw/jbe@1309 11207 if (startNode.nodeType === 3) {
bsw/jbe@1309 11208 rng.setStart(startNode, 0);
bsw/jbe@1309 11209 rng.setEnd(startNode, startOffset);
bsw/jbe@1309 11210 return {
bsw/jbe@1309 11211 type: "text",
bsw/jbe@1309 11212 range: rng,
bsw/jbe@1309 11213 offset : startOffset,
bsw/jbe@1309 11214 node: startNode
bsw/jbe@1309 11215 };
bsw/jbe@1309 11216 } else {
bsw/jbe@1309 11217 rng.setStartBefore(startNode.childNodes[0]);
bsw/jbe@1309 11218 endNode = startNode.childNodes[startOffset - 1];
bsw/jbe@1309 11219 rng.setEndAfter(endNode);
bsw/jbe@1309 11220 return {
bsw/jbe@1309 11221 type: "element",
bsw/jbe@1309 11222 range: rng,
bsw/jbe@1309 11223 offset : startOffset,
bsw/jbe@1309 11224 node: endNode
bsw/jbe@1309 11225 };
bsw/jbe@1309 11226 }
bsw/jbe@1309 11227 } else {
bsw/jbe@1309 11228 rng.setStartAndEnd(startNode, 0);
bsw/jbe@1309 11229
bsw/jbe@1309 11230 if (includePrevLeaves) {
bsw/jbe@1309 11231 var prevNode = this.getPreviousNode(startNode, true),
bsw/jbe@1309 11232 prevLeaf = null;
bsw/jbe@1309 11233
bsw/jbe@1309 11234 if(prevNode) {
bsw/jbe@1309 11235 if (prevNode.nodeType === 1 && wysihtml.dom.hasClass(prevNode, this.unselectableClass)) {
bsw/jbe@1309 11236 prevLeaf = prevNode;
bsw/jbe@1309 11237 } else {
bsw/jbe@1309 11238 prevLeaf = wysihtml.dom.domNode(prevNode).lastLeafNode();
bsw/jbe@1309 11239 }
bsw/jbe@1309 11240 }
bsw/jbe@1309 11241
bsw/jbe@1309 11242 if (prevLeaf) {
bsw/jbe@1309 11243 return {
bsw/jbe@1309 11244 type: "leafnode",
bsw/jbe@1309 11245 range: rng,
bsw/jbe@1309 11246 offset : startOffset,
bsw/jbe@1309 11247 node: prevLeaf
bsw/jbe@1309 11248 };
bsw/jbe@1309 11249 }
bsw/jbe@1309 11250 }
bsw/jbe@1309 11251
bsw/jbe@1309 11252 return {
bsw/jbe@1309 11253 type: "none",
bsw/jbe@1309 11254 range: rng,
bsw/jbe@1309 11255 offset : startOffset,
bsw/jbe@1309 11256 node: startNode
bsw/jbe@1309 11257 };
bsw/jbe@1309 11258 }
bsw/jbe@1309 11259 }
bsw/jbe@1309 11260 return null;
bsw/jbe@1309 11261 },
bsw/jbe@1309 11262
bsw/jbe@1309 11263 // TODO: Figure out a method from following 2 that would work universally
bsw/jbe@1309 11264 executeAndRestoreRangy: function(method, restoreScrollPosition) {
bsw/jbe@1309 11265 var sel = rangy.saveSelection(this.win);
bsw/jbe@1309 11266 if (!sel) {
bsw/jbe@1309 11267 method();
bsw/jbe@1309 11268 } else {
bsw/jbe@1309 11269 try {
bsw/jbe@1309 11270 method();
bsw/jbe@1309 11271 } catch(e) {
bsw/jbe@1309 11272 setTimeout(function() { throw e; }, 0);
bsw/jbe@1309 11273 }
bsw/jbe@1309 11274 }
bsw/jbe@1309 11275 rangy.restoreSelection(sel);
bsw/jbe@1309 11276 },
bsw/jbe@1309 11277
bsw/jbe@1309 11278 // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
bsw/jbe@1309 11279 executeAndRestore: function(method, restoreScrollPosition) {
bsw/jbe@1309 11280 var body = this.doc.body,
bsw/jbe@1309 11281 oldScrollTop = restoreScrollPosition && body.scrollTop,
bsw/jbe@1309 11282 oldScrollLeft = restoreScrollPosition && body.scrollLeft,
bsw/jbe@1309 11283 className = "_wysihtml-temp-placeholder",
bsw/jbe@1309 11284 placeholderHtml = '<span class="' + className + '">' + wysihtml.INVISIBLE_SPACE + '</span>',
bsw/jbe@1309 11285 range = this.getRange(true),
bsw/jbe@1309 11286 caretPlaceholder,
bsw/jbe@1309 11287 newCaretPlaceholder,
bsw/jbe@1309 11288 nextSibling, prevSibling,
bsw/jbe@1309 11289 node, node2, range2,
bsw/jbe@1309 11290 newRange;
bsw/jbe@1309 11291
bsw/jbe@1309 11292 // Nothing selected, execute and say goodbye
bsw/jbe@1309 11293 if (!range) {
bsw/jbe@1309 11294 method(body, body);
bsw/jbe@1309 11295 return;
bsw/jbe@1309 11296 }
bsw/jbe@1309 11297
bsw/jbe@1309 11298 if (!range.collapsed) {
bsw/jbe@1309 11299 range2 = range.cloneRange();
bsw/jbe@1309 11300 node2 = range2.createContextualFragment(placeholderHtml);
bsw/jbe@1309 11301 range2.collapse(false);
bsw/jbe@1309 11302 range2.insertNode(node2);
bsw/jbe@1309 11303 range2.detach();
bsw/jbe@1309 11304 }
bsw/jbe@1309 11305
bsw/jbe@1309 11306 node = range.createContextualFragment(placeholderHtml);
bsw/jbe@1309 11307 range.insertNode(node);
bsw/jbe@1309 11308
bsw/jbe@1309 11309 if (node2) {
bsw/jbe@1309 11310 caretPlaceholder = this.contain.querySelectorAll("." + className);
bsw/jbe@1309 11311 range.setStartBefore(caretPlaceholder[0]);
bsw/jbe@1309 11312 range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
bsw/jbe@1309 11313 }
bsw/jbe@1309 11314 this.setSelection(range);
bsw/jbe@1309 11315
bsw/jbe@1309 11316 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
bsw/jbe@1309 11317 try {
bsw/jbe@1309 11318 method(range.startContainer, range.endContainer);
bsw/jbe@1309 11319 } catch(e) {
bsw/jbe@1309 11320 setTimeout(function() { throw e; }, 0);
bsw/jbe@1309 11321 }
bsw/jbe@1309 11322 caretPlaceholder = this.contain.querySelectorAll("." + className);
bsw/jbe@1309 11323 if (caretPlaceholder && caretPlaceholder.length) {
bsw/jbe@1309 11324 newRange = rangy.createRange(this.doc);
bsw/jbe@1309 11325 nextSibling = caretPlaceholder[0].nextSibling;
bsw/jbe@1309 11326 if (caretPlaceholder.length > 1) {
bsw/jbe@1309 11327 prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
bsw/jbe@1309 11328 }
bsw/jbe@1309 11329 if (prevSibling && nextSibling) {
bsw/jbe@1309 11330 newRange.setStartBefore(nextSibling);
bsw/jbe@1309 11331 newRange.setEndAfter(prevSibling);
bsw/jbe@1309 11332 } else {
bsw/jbe@1309 11333 newCaretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309 11334 dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
bsw/jbe@1309 11335 newRange.setStartBefore(newCaretPlaceholder);
bsw/jbe@1309 11336 newRange.setEndAfter(newCaretPlaceholder);
bsw/jbe@1309 11337 }
bsw/jbe@1309 11338 this.setSelection(newRange);
bsw/jbe@1309 11339 for (var i = caretPlaceholder.length; i--;) {
bsw/jbe@1309 11340 caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
bsw/jbe@1309 11341 }
bsw/jbe@1309 11342
bsw/jbe@1309 11343 } else {
bsw/jbe@1309 11344 // fallback for when all hell breaks loose
bsw/jbe@1309 11345 this.contain.focus();
bsw/jbe@1309 11346 }
bsw/jbe@1309 11347
bsw/jbe@1309 11348 if (restoreScrollPosition) {
bsw/jbe@1309 11349 body.scrollTop = oldScrollTop;
bsw/jbe@1309 11350 body.scrollLeft = oldScrollLeft;
bsw/jbe@1309 11351 }
bsw/jbe@1309 11352
bsw/jbe@1309 11353 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
bsw/jbe@1309 11354 try {
bsw/jbe@1309 11355 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
bsw/jbe@1309 11356 } catch(e2) {}
bsw/jbe@1309 11357 },
bsw/jbe@1309 11358
bsw/jbe@1309 11359 set: function(node, offset) {
bsw/jbe@1309 11360 var newRange = rangy.createRange(this.doc);
bsw/jbe@1309 11361 newRange.setStart(node, offset || 0);
bsw/jbe@1309 11362 this.setSelection(newRange);
bsw/jbe@1309 11363 },
bsw/jbe@1309 11364
bsw/jbe@1309 11365 /**
bsw/jbe@1309 11366 * Insert html at the caret or selection position and move the cursor after the inserted html
bsw/jbe@1309 11367 * Replaces selection content if present
bsw/jbe@1309 11368 *
bsw/jbe@1309 11369 * @param {String} html HTML string to insert
bsw/jbe@1309 11370 * @example
bsw/jbe@1309 11371 * selection.insertHTML("<p>foobar</p>");
bsw/jbe@1309 11372 */
bsw/jbe@1309 11373 insertHTML: function(html) {
bsw/jbe@1309 11374 var range = this.getRange(),
bsw/jbe@1309 11375 node = this.doc.createElement('DIV'),
bsw/jbe@1309 11376 fragment = this.doc.createDocumentFragment(),
bsw/jbe@1309 11377 lastChild, lastEditorElement;
bsw/jbe@1309 11378
bsw/jbe@1309 11379 if (range) {
bsw/jbe@1309 11380 range.deleteContents();
bsw/jbe@1309 11381 node.innerHTML = html;
bsw/jbe@1309 11382 lastChild = node.lastChild;
bsw/jbe@1309 11383
bsw/jbe@1309 11384 while (node.firstChild) {
bsw/jbe@1309 11385 fragment.appendChild(node.firstChild);
bsw/jbe@1309 11386 }
bsw/jbe@1309 11387 range.insertNode(fragment);
bsw/jbe@1309 11388
bsw/jbe@1309 11389 lastEditorElement = this.contain.lastChild;
bsw/jbe@1309 11390 while (lastEditorElement && lastEditorElement.nodeType === 3 && lastEditorElement.previousSibling && (/^\s*$/).test(lastEditorElement.data)) {
bsw/jbe@1309 11391 lastEditorElement = lastEditorElement.previousSibling;
bsw/jbe@1309 11392 }
bsw/jbe@1309 11393
bsw/jbe@1309 11394 if (lastChild) {
bsw/jbe@1309 11395 // fixes some pad cases mostly on webkit where last nr is needed
bsw/jbe@1309 11396 if (lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === 1) {
bsw/jbe@1309 11397 this.contain.appendChild(this.doc.createElement('br'));
bsw/jbe@1309 11398 }
bsw/jbe@1309 11399 this.setAfter(lastChild);
bsw/jbe@1309 11400 }
bsw/jbe@1309 11401 }
bsw/jbe@1309 11402 },
bsw/jbe@1309 11403
bsw/jbe@1309 11404 /**
bsw/jbe@1309 11405 * Insert a node at the caret position and move the cursor behind it
bsw/jbe@1309 11406 *
bsw/jbe@1309 11407 * @param {Object} node HTML string to insert
bsw/jbe@1309 11408 * @example
bsw/jbe@1309 11409 * selection.insertNode(document.createTextNode("foobar"));
bsw/jbe@1309 11410 */
bsw/jbe@1309 11411 insertNode: function(node) {
bsw/jbe@1309 11412 var range = this.getRange();
bsw/jbe@1309 11413 if (range) {
bsw/jbe@1309 11414 range.deleteContents();
bsw/jbe@1309 11415 range.insertNode(node);
bsw/jbe@1309 11416 }
bsw/jbe@1309 11417 },
bsw/jbe@1309 11418
bsw/jbe@1309 11419 canAppendChild: function (node) {
bsw/jbe@1309 11420 var anchorNode, anchorNodeTagNameLower,
bsw/jbe@1309 11421 voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"],
bsw/jbe@1309 11422 range = this.getRange();
bsw/jbe@1309 11423
bsw/jbe@1309 11424 anchorNode = node || range.startContainer;
bsw/jbe@1309 11425
bsw/jbe@1309 11426 if (anchorNode) {
bsw/jbe@1309 11427 anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase();
bsw/jbe@1309 11428 }
bsw/jbe@1309 11429
bsw/jbe@1309 11430 return voidElements.indexOf(anchorNodeTagNameLower) === -1;
bsw/jbe@1309 11431 },
bsw/jbe@1309 11432
bsw/jbe@1309 11433 splitElementAtCaret: function (element, insertNode) {
bsw/jbe@1309 11434 var sel = this.getSelection(),
bsw/jbe@1309 11435 range, contentAfterRangeStart,
bsw/jbe@1309 11436 firstChild, lastChild, childNodes;
bsw/jbe@1309 11437
bsw/jbe@1309 11438 if (sel.rangeCount > 0) {
bsw/jbe@1309 11439 range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with
bsw/jbe@1309 11440
bsw/jbe@1309 11441 range.setEndAfter(element); // Place the end of the range after the element
bsw/jbe@1309 11442 contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment
bsw/jbe@1309 11443
bsw/jbe@1309 11444 childNodes = contentAfterRangeStart.childNodes;
bsw/jbe@1309 11445
bsw/jbe@1309 11446 // Empty elements are cleaned up from extracted content
bsw/jbe@1309 11447 for (var i = childNodes.length; i --;) {
bsw/jbe@1309 11448 if (!wysihtml.dom.domNode(childNodes[i]).is.visible()) {
bsw/jbe@1309 11449 contentAfterRangeStart.removeChild(childNodes[i]);
bsw/jbe@1309 11450 }
bsw/jbe@1309 11451 }
bsw/jbe@1309 11452
bsw/jbe@1309 11453 element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling);
bsw/jbe@1309 11454
bsw/jbe@1309 11455 if (insertNode) {
bsw/jbe@1309 11456 firstChild = insertNode.firstChild || insertNode;
bsw/jbe@1309 11457 lastChild = insertNode.lastChild || insertNode;
bsw/jbe@1309 11458
bsw/jbe@1309 11459 element.parentNode.insertBefore(insertNode, element.nextSibling);
bsw/jbe@1309 11460
bsw/jbe@1309 11461 // Select inserted node contents
bsw/jbe@1309 11462 if (firstChild && lastChild) {
bsw/jbe@1309 11463 range.setStartBefore(firstChild);
bsw/jbe@1309 11464 range.setEndAfter(lastChild);
bsw/jbe@1309 11465 this.setSelection(range);
bsw/jbe@1309 11466 }
bsw/jbe@1309 11467 } else {
bsw/jbe@1309 11468 range.setStartAfter(element);
bsw/jbe@1309 11469 range.setEndAfter(element);
bsw/jbe@1309 11470 }
bsw/jbe@1309 11471
bsw/jbe@1309 11472 if (!wysihtml.dom.domNode(element).is.visible()) {
bsw/jbe@1309 11473 if (wysihtml.dom.getTextContent(element) === '') {
bsw/jbe@1309 11474 element.parentNode.removeChild(element);
bsw/jbe@1309 11475 } else {
bsw/jbe@1309 11476 element.parentNode.replaceChild(this.doc.createTextNode(" "), element);
bsw/jbe@1309 11477 }
bsw/jbe@1309 11478 }
bsw/jbe@1309 11479
bsw/jbe@1309 11480
bsw/jbe@1309 11481 }
bsw/jbe@1309 11482 },
bsw/jbe@1309 11483
bsw/jbe@1309 11484 /**
bsw/jbe@1309 11485 * Wraps current selection with the given node
bsw/jbe@1309 11486 *
bsw/jbe@1309 11487 * @param {Object} node The node to surround the selected elements with
bsw/jbe@1309 11488 */
bsw/jbe@1309 11489 surround: function(nodeOptions) {
bsw/jbe@1309 11490 var ranges = this.getOwnRanges(),
bsw/jbe@1309 11491 node, nodes = [];
bsw/jbe@1309 11492 if (ranges.length == 0) {
bsw/jbe@1309 11493 return nodes;
bsw/jbe@1309 11494 }
bsw/jbe@1309 11495
bsw/jbe@1309 11496 for (var i = ranges.length; i--;) {
bsw/jbe@1309 11497 node = this.doc.createElement(nodeOptions.nodeName);
bsw/jbe@1309 11498 nodes.push(node);
bsw/jbe@1309 11499 if (nodeOptions.className) {
bsw/jbe@1309 11500 node.className = nodeOptions.className;
bsw/jbe@1309 11501 }
bsw/jbe@1309 11502 if (nodeOptions.cssStyle) {
bsw/jbe@1309 11503 node.setAttribute('style', nodeOptions.cssStyle);
bsw/jbe@1309 11504 }
bsw/jbe@1309 11505 try {
bsw/jbe@1309 11506 // This only works when the range boundaries are not overlapping other elements
bsw/jbe@1309 11507 ranges[i].surroundContents(node);
bsw/jbe@1309 11508 this.selectNode(node);
bsw/jbe@1309 11509 } catch(e) {
bsw/jbe@1309 11510 // fallback
bsw/jbe@1309 11511 node.appendChild(ranges[i].extractContents());
bsw/jbe@1309 11512 ranges[i].insertNode(node);
bsw/jbe@1309 11513 }
bsw/jbe@1309 11514 }
bsw/jbe@1309 11515 return nodes;
bsw/jbe@1309 11516 },
bsw/jbe@1309 11517
bsw/jbe@1309 11518 /**
bsw/jbe@1309 11519 * Scroll the current caret position into the view
bsw/jbe@1309 11520 * FIXME: This is a bit hacky, there might be a smarter way of doing this
bsw/jbe@1309 11521 *
bsw/jbe@1309 11522 * @example
bsw/jbe@1309 11523 * selection.scrollIntoView();
bsw/jbe@1309 11524 */
bsw/jbe@1309 11525 scrollIntoView: function() {
bsw/jbe@1309 11526 var doc = this.doc,
bsw/jbe@1309 11527 tolerance = 5, // px
bsw/jbe@1309 11528 hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
bsw/jbe@1309 11529 tempElement = doc._wysihtmlScrollIntoViewElement = doc._wysihtmlScrollIntoViewElement || (function() {
bsw/jbe@1309 11530 var element = doc.createElement("span");
bsw/jbe@1309 11531 // The element needs content in order to be able to calculate it's position properly
bsw/jbe@1309 11532 element.innerHTML = wysihtml.INVISIBLE_SPACE;
bsw/jbe@1309 11533 return element;
bsw/jbe@1309 11534 })(),
bsw/jbe@1309 11535 offsetTop;
bsw/jbe@1309 11536
bsw/jbe@1309 11537 if (hasScrollBars) {
bsw/jbe@1309 11538 this.insertNode(tempElement);
bsw/jbe@1309 11539 offsetTop = _getCumulativeOffsetTop(tempElement);
bsw/jbe@1309 11540 tempElement.parentNode.removeChild(tempElement);
bsw/jbe@1309 11541 if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
bsw/jbe@1309 11542 doc.body.scrollTop = offsetTop;
bsw/jbe@1309 11543 }
bsw/jbe@1309 11544 }
bsw/jbe@1309 11545 },
bsw/jbe@1309 11546
bsw/jbe@1309 11547 /**
bsw/jbe@1309 11548 * Select line where the caret is in
bsw/jbe@1309 11549 */
bsw/jbe@1309 11550 selectLine: function() {
bsw/jbe@1309 11551 var r = rangy.createRange();
bsw/jbe@1309 11552 if (wysihtml.browser.supportsSelectionModify()) {
bsw/jbe@1309 11553 this._selectLine_W3C();
bsw/jbe@1309 11554 } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
bsw/jbe@1309 11555 // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
bsw/jbe@1309 11556 this._selectLineUniversal();
bsw/jbe@1309 11557 }
bsw/jbe@1309 11558 },
bsw/jbe@1309 11559
bsw/jbe@1309 11560 includeRangyRangeHelpers: function() {
bsw/jbe@1309 11561 var s = this.getSelection(),
bsw/jbe@1309 11562 r = s.getRangeAt(0),
bsw/jbe@1309 11563 isHelperNode = function(node) {
bsw/jbe@1309 11564 return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
bsw/jbe@1309 11565 },
bsw/jbe@1309 11566 getNodeLength = function (node) {
bsw/jbe@1309 11567 if (node.nodeType === 1) {
bsw/jbe@1309 11568 return node.childNodes && node.childNodes.length || 0;
bsw/jbe@1309 11569 } else {
bsw/jbe@1309 11570 return node.data && node.data.length || 0;
bsw/jbe@1309 11571 }
bsw/jbe@1309 11572 },
bsw/jbe@1309 11573 anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
bsw/jbe@1309 11574 fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
bsw/jbe@1309 11575
bsw/jbe@1309 11576 if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
bsw/jbe@1309 11577 r.setEndAfter(fnode.nextSibling);
bsw/jbe@1309 11578 }
bsw/jbe@1309 11579 if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
bsw/jbe@1309 11580 r.setStartBefore(anode.previousSibling);
bsw/jbe@1309 11581 }
bsw/jbe@1309 11582 r.select();
bsw/jbe@1309 11583 },
bsw/jbe@1309 11584
bsw/jbe@1309 11585 /**
bsw/jbe@1309 11586 * See https://developer.mozilla.org/en/DOM/Selection/modify
bsw/jbe@1309 11587 */
bsw/jbe@1309 11588 _selectLine_W3C: function() {
bsw/jbe@1309 11589 var selection = this.win.getSelection(),
bsw/jbe@1309 11590 initialBoundry = [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset];
bsw/jbe@1309 11591
bsw/jbe@1309 11592 selection.modify("move", "left", "lineboundary");
bsw/jbe@1309 11593 selection.modify("extend", "right", "lineboundary");
bsw/jbe@1309 11594
bsw/jbe@1309 11595 // IF lineboundary extending did not change selection try universal fallback (FF fails sometimes without a reason)
bsw/jbe@1309 11596 if (selection.anchorNode === initialBoundry[0] &&
bsw/jbe@1309 11597 selection.anchorOffset === initialBoundry[1] &&
bsw/jbe@1309 11598 selection.focusNode === initialBoundry[2] &&
bsw/jbe@1309 11599 selection.focusOffset === initialBoundry[3]
bsw/jbe@1309 11600 ) {
bsw/jbe@1309 11601 this._selectLineUniversal();
bsw/jbe@1309 11602 } else {
bsw/jbe@1309 11603 this.includeRangyRangeHelpers();
bsw/jbe@1309 11604 }
bsw/jbe@1309 11605 },
bsw/jbe@1309 11606
bsw/jbe@1309 11607 // collapses selection to current line beginning or end
bsw/jbe@1309 11608 toLineBoundary: function (location, collapse) {
bsw/jbe@1309 11609 collapse = (typeof collapse === 'undefined') ? false : collapse;
bsw/jbe@1309 11610 if (wysihtml.browser.supportsSelectionModify()) {
bsw/jbe@1309 11611 var selection = this.win.getSelection();
bsw/jbe@1309 11612
bsw/jbe@1309 11613 selection.modify("extend", location, "lineboundary");
bsw/jbe@1309 11614 if (collapse) {
bsw/jbe@1309 11615 if (location === "left") {
bsw/jbe@1309 11616 selection.collapseToStart();
bsw/jbe@1309 11617 } else if (location === "right") {
bsw/jbe@1309 11618 selection.collapseToEnd();
bsw/jbe@1309 11619 }
bsw/jbe@1309 11620 }
bsw/jbe@1309 11621 }
bsw/jbe@1309 11622 },
bsw/jbe@1309 11623
bsw/jbe@1309 11624 getRangeRect: function(r) {
bsw/jbe@1309 11625 var textNode = this.doc.createTextNode("i"),
bsw/jbe@1309 11626 testNode = this.doc.createTextNode("i"),
bsw/jbe@1309 11627 rect, cr;
bsw/jbe@1309 11628
bsw/jbe@1309 11629 /*testNode.style.visibility = "hidden";
bsw/jbe@1309 11630 testNode.style.width = "0px";
bsw/jbe@1309 11631 testNode.style.display = "inline-block";
bsw/jbe@1309 11632 testNode.style.overflow = "hidden";
bsw/jbe@1309 11633 testNode.appendChild(textNode);*/
bsw/jbe@1309 11634
bsw/jbe@1309 11635 if (r.collapsed) {
bsw/jbe@1309 11636 r.insertNode(testNode);
bsw/jbe@1309 11637 r.selectNode(testNode);
bsw/jbe@1309 11638 rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309 11639 r.deleteContents();
bsw/jbe@1309 11640
bsw/jbe@1309 11641 } else {
bsw/jbe@1309 11642 rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309 11643 }
bsw/jbe@1309 11644
bsw/jbe@1309 11645 return rect;
bsw/jbe@1309 11646
bsw/jbe@1309 11647 },
bsw/jbe@1309 11648
bsw/jbe@1309 11649 _selectLineUniversal: function() {
bsw/jbe@1309 11650 var s = this.getSelection(),
bsw/jbe@1309 11651 r = s.getRangeAt(0),
bsw/jbe@1309 11652 rect,
bsw/jbe@1309 11653 startRange, endRange, testRange,
bsw/jbe@1309 11654 count = 0,
bsw/jbe@1309 11655 amount, testRect, found,
bsw/jbe@1309 11656 that = this,
bsw/jbe@1309 11657 isLineBreakingElement = function(el) {
bsw/jbe@1309 11658 return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml.lang.array(['BR', 'HR']).contains(el.nodeName));
bsw/jbe@1309 11659 },
bsw/jbe@1309 11660 prevNode = function(node) {
bsw/jbe@1309 11661 var pnode = node;
bsw/jbe@1309 11662 if (pnode) {
bsw/jbe@1309 11663 while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
bsw/jbe@1309 11664 pnode = pnode.previousSibling;
bsw/jbe@1309 11665 }
bsw/jbe@1309 11666 }
bsw/jbe@1309 11667 return pnode;
bsw/jbe@1309 11668 };
bsw/jbe@1309 11669
bsw/jbe@1309 11670 startRange = r.cloneRange();
bsw/jbe@1309 11671 endRange = r.cloneRange();
bsw/jbe@1309 11672
bsw/jbe@1309 11673 if (r.collapsed) {
bsw/jbe@1309 11674 // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
bsw/jbe@1309 11675 // TODO: figure out a shorter and more readable way
bsw/jbe@1309 11676 if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
bsw/jbe@1309 11677 r.moveEnd('character', 1);
bsw/jbe@1309 11678 } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) {
bsw/jbe@1309 11679 r.moveEnd('character', 1);
bsw/jbe@1309 11680 } else if (
bsw/jbe@1309 11681 r.startOffset > 0 &&
bsw/jbe@1309 11682 (
bsw/jbe@1309 11683 r.startContainer.nodeType === 3 ||
bsw/jbe@1309 11684 (
bsw/jbe@1309 11685 r.startContainer.nodeType === 1 &&
bsw/jbe@1309 11686 !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))
bsw/jbe@1309 11687 )
bsw/jbe@1309 11688 )
bsw/jbe@1309 11689 ) {
bsw/jbe@1309 11690 r.moveStart('character', -1);
bsw/jbe@1309 11691 }
bsw/jbe@1309 11692 }
bsw/jbe@1309 11693 if (!r.collapsed) {
bsw/jbe@1309 11694 r.insertNode(this.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
bsw/jbe@1309 11695 }
bsw/jbe@1309 11696
bsw/jbe@1309 11697 // Is probably just empty line as can not be expanded
bsw/jbe@1309 11698 rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309 11699 // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes;
bsw/jbe@1309 11700 do {
bsw/jbe@1309 11701 amount = r.moveStart('character', -1);
bsw/jbe@1309 11702 testRect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309 11703
bsw/jbe@1309 11704 if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
bsw/jbe@1309 11705 r.moveStart('character', 1);
bsw/jbe@1309 11706 found = true;
bsw/jbe@1309 11707 }
bsw/jbe@1309 11708 count++;
bsw/jbe@1309 11709 } while (amount !== 0 && !found && count < 2000);
bsw/jbe@1309 11710 count = 0;
bsw/jbe@1309 11711 found = false;
bsw/jbe@1309 11712 rect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309 11713
bsw/jbe@1309 11714 if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) {
bsw/jbe@1309 11715 do {
bsw/jbe@1309 11716 amount = r.moveEnd('character', 1);
bsw/jbe@1309 11717 testRect = r.nativeRange.getBoundingClientRect();
bsw/jbe@1309 11718 if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
bsw/jbe@1309 11719 r.moveEnd('character', -1);
bsw/jbe@1309 11720
bsw/jbe@1309 11721 // Fix a IE line end marked by linebreak element although caret is before it
bsw/jbe@1309 11722 // If causes problems should be changed to be applied only to IE
bsw/jbe@1309 11723 if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) {
bsw/jbe@1309 11724 if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
bsw/jbe@1309 11725 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
bsw/jbe@1309 11726 } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
bsw/jbe@1309 11727 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
bsw/jbe@1309 11728 }
bsw/jbe@1309 11729 }
bsw/jbe@1309 11730 found = true;
bsw/jbe@1309 11731 }
bsw/jbe@1309 11732 count++;
bsw/jbe@1309 11733 } while (amount !== 0 && !found && count < 2000);
bsw/jbe@1309 11734 }
bsw/jbe@1309 11735 r.select();
bsw/jbe@1309 11736 this.includeRangyRangeHelpers();
bsw/jbe@1309 11737 },
bsw/jbe@1309 11738
bsw/jbe@1309 11739 getText: function() {
bsw/jbe@1309 11740 var selection = this.getSelection();
bsw/jbe@1309 11741 return selection ? selection.toString() : "";
bsw/jbe@1309 11742 },
bsw/jbe@1309 11743
bsw/jbe@1309 11744 getNodes: function(nodeType, filter) {
bsw/jbe@1309 11745 var range = this.getRange();
bsw/jbe@1309 11746 if (range) {
bsw/jbe@1309 11747 return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter);
bsw/jbe@1309 11748 } else {
bsw/jbe@1309 11749 return [];
bsw/jbe@1309 11750 }
bsw/jbe@1309 11751 },
bsw/jbe@1309 11752
bsw/jbe@1309 11753 // Gets all the elements in selection with nodeType
bsw/jbe@1309 11754 // Ignores the elements not belonging to current editable area
bsw/jbe@1309 11755 // If filter is defined nodes must pass the filter function with true to be included in list
bsw/jbe@1309 11756 getOwnNodes: function(nodeType, filter, splitBounds) {
bsw/jbe@1309 11757 var ranges = this.getOwnRanges(),
bsw/jbe@1309 11758 nodes = [];
bsw/jbe@1309 11759 for (var r = 0, rmax = ranges.length; r < rmax; r++) {
bsw/jbe@1309 11760 if (ranges[r]) {
bsw/jbe@1309 11761 if (splitBounds) {
bsw/jbe@1309 11762 ranges[r].splitBoundaries();
bsw/jbe@1309 11763 }
bsw/jbe@1309 11764 nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter));
bsw/jbe@1309 11765 }
bsw/jbe@1309 11766 }
bsw/jbe@1309 11767
bsw/jbe@1309 11768 return nodes;
bsw/jbe@1309 11769 },
bsw/jbe@1309 11770
bsw/jbe@1309 11771 fixRangeOverflow: function(range) {
bsw/jbe@1309 11772 if (this.contain && this.contain.firstChild && range) {
bsw/jbe@1309 11773 var containment = range.compareNode(this.contain);
bsw/jbe@1309 11774 if (containment !== 2) {
bsw/jbe@1309 11775 if (containment === 1) {
bsw/jbe@1309 11776 range.setStartBefore(this.contain.firstChild);
bsw/jbe@1309 11777 }
bsw/jbe@1309 11778 if (containment === 0) {
bsw/jbe@1309 11779 range.setEndAfter(this.contain.lastChild);
bsw/jbe@1309 11780 }
bsw/jbe@1309 11781 if (containment === 3) {
bsw/jbe@1309 11782 range.setStartBefore(this.contain.firstChild);
bsw/jbe@1309 11783 range.setEndAfter(this.contain.lastChild);
bsw/jbe@1309 11784 }
bsw/jbe@1309 11785 } else if (this._detectInlineRangeProblems(range)) {
bsw/jbe@1309 11786 var previousElementSibling = range.endContainer.previousElementSibling;
bsw/jbe@1309 11787 if (previousElementSibling) {
bsw/jbe@1309 11788 range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
bsw/jbe@1309 11789 }
bsw/jbe@1309 11790 }
bsw/jbe@1309 11791 }
bsw/jbe@1309 11792 },
bsw/jbe@1309 11793
bsw/jbe@1309 11794 _endOffsetForNode: function(node) {
bsw/jbe@1309 11795 var range = document.createRange();
bsw/jbe@1309 11796 range.selectNodeContents(node);
bsw/jbe@1309 11797 return range.endOffset;
bsw/jbe@1309 11798 },
bsw/jbe@1309 11799
bsw/jbe@1309 11800 _detectInlineRangeProblems: function(range) {
bsw/jbe@1309 11801 var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
bsw/jbe@1309 11802 return (
bsw/jbe@1309 11803 range.endOffset == 0 &&
bsw/jbe@1309 11804 position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
bsw/jbe@1309 11805 );
bsw/jbe@1309 11806 },
bsw/jbe@1309 11807
bsw/jbe@1309 11808 getRange: function(dontFix) {
bsw/jbe@1309 11809 var selection = this.getSelection(),
bsw/jbe@1309 11810 range = selection && selection.rangeCount && selection.getRangeAt(0);
bsw/jbe@1309 11811
bsw/jbe@1309 11812 if (dontFix !== true) {
bsw/jbe@1309 11813 this.fixRangeOverflow(range);
bsw/jbe@1309 11814 }
bsw/jbe@1309 11815
bsw/jbe@1309 11816 return range;
bsw/jbe@1309 11817 },
bsw/jbe@1309 11818
bsw/jbe@1309 11819 getOwnUneditables: function() {
bsw/jbe@1309 11820 var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
bsw/jbe@1309 11821 deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
bsw/jbe@1309 11822
bsw/jbe@1309 11823 return wysihtml.lang.array(allUneditables).without(deepUneditables);
bsw/jbe@1309 11824 },
bsw/jbe@1309 11825
bsw/jbe@1309 11826 // Returns an array of ranges that belong only to this editable
bsw/jbe@1309 11827 // Needed as uneditable block in contenteditabel can split range into pieces
bsw/jbe@1309 11828 // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
bsw/jbe@1309 11829 getOwnRanges: function() {
bsw/jbe@1309 11830 var ranges = [],
bsw/jbe@1309 11831 r = this.getRange(),
bsw/jbe@1309 11832 tmpRanges;
bsw/jbe@1309 11833
bsw/jbe@1309 11834 if (r) { ranges.push(r); }
bsw/jbe@1309 11835
bsw/jbe@1309 11836 if (this.unselectableClass && this.contain && r) {
bsw/jbe@1309 11837 var uneditables = this.getOwnUneditables(),
bsw/jbe@1309 11838 tmpRange;
bsw/jbe@1309 11839 if (uneditables.length > 0) {
bsw/jbe@1309 11840 for (var i = 0, imax = uneditables.length; i < imax; i++) {
bsw/jbe@1309 11841 tmpRanges = [];
bsw/jbe@1309 11842 for (var j = 0, jmax = ranges.length; j < jmax; j++) {
bsw/jbe@1309 11843 if (ranges[j]) {
bsw/jbe@1309 11844 switch (ranges[j].compareNode(uneditables[i])) {
bsw/jbe@1309 11845 case 2:
bsw/jbe@1309 11846 // all selection inside uneditable. remove
bsw/jbe@1309 11847 break;
bsw/jbe@1309 11848 case 3:
bsw/jbe@1309 11849 //section begins before and ends after uneditable. spilt
bsw/jbe@1309 11850 tmpRange = ranges[j].cloneRange();
bsw/jbe@1309 11851 tmpRange.setEndBefore(uneditables[i]);
bsw/jbe@1309 11852 tmpRanges.push(tmpRange);
bsw/jbe@1309 11853
bsw/jbe@1309 11854 tmpRange = ranges[j].cloneRange();
bsw/jbe@1309 11855 tmpRange.setStartAfter(uneditables[i]);
bsw/jbe@1309 11856 tmpRanges.push(tmpRange);
bsw/jbe@1309 11857 break;
bsw/jbe@1309 11858 default:
bsw/jbe@1309 11859 // in all other cases uneditable does not touch selection. dont modify
bsw/jbe@1309 11860 tmpRanges.push(ranges[j]);
bsw/jbe@1309 11861 }
bsw/jbe@1309 11862 }
bsw/jbe@1309 11863 ranges = tmpRanges;
bsw/jbe@1309 11864 }
bsw/jbe@1309 11865 }
bsw/jbe@1309 11866 }
bsw/jbe@1309 11867 }
bsw/jbe@1309 11868 return ranges;
bsw/jbe@1309 11869 },
bsw/jbe@1309 11870
bsw/jbe@1309 11871 getSelection: function() {
bsw/jbe@1309 11872 return rangy.getSelection(this.win);
bsw/jbe@1309 11873 },
bsw/jbe@1309 11874
bsw/jbe@1309 11875 // Sets selection in document to a given range
bsw/jbe@1309 11876 // Set selection method detects if it fails to set any selection in document and returns null on fail
bsw/jbe@1309 11877 // (especially needed in webkit where some ranges just can not create selection for no reason)
bsw/jbe@1309 11878 setSelection: function(range) {
bsw/jbe@1309 11879 var selection = rangy.getSelection(this.win);
bsw/jbe@1309 11880 selection.setSingleRange(range);
bsw/jbe@1309 11881 return (selection && selection.anchorNode && selection.focusNode) ? selection : null;
bsw/jbe@1309 11882 },
bsw/jbe@1309 11883
bsw/jbe@1309 11884
bsw/jbe@1309 11885
bsw/jbe@1309 11886 // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area
bsw/jbe@1309 11887 selectAll: function() {
bsw/jbe@1309 11888 var range = this.createRange(),
bsw/jbe@1309 11889 composer = this.composer,
bsw/jbe@1309 11890 that = this,
bsw/jbe@1309 11891 blankEndNode = getWebkitSelectionFixNode(this.composer.element),
bsw/jbe@1309 11892 blankStartNode = getWebkitSelectionFixNode(this.composer.element),
bsw/jbe@1309 11893 s;
bsw/jbe@1309 11894
bsw/jbe@1309 11895 var doSelect = function() {
bsw/jbe@1309 11896 range.setStart(composer.element, 0);
bsw/jbe@1309 11897 range.setEnd(composer.element, composer.element.childNodes.length);
bsw/jbe@1309 11898 s = that.setSelection(range);
bsw/jbe@1309 11899 };
bsw/jbe@1309 11900
bsw/jbe@1309 11901 var notSelected = function() {
bsw/jbe@1309 11902 return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None"));
bsw/jbe@1309 11903 }
bsw/jbe@1309 11904
bsw/jbe@1309 11905 wysihtml.dom.removeInvisibleSpaces(this.composer.element);
bsw/jbe@1309 11906 doSelect();
bsw/jbe@1309 11907
bsw/jbe@1309 11908 if (this.composer.element.firstChild && notSelected()) {
bsw/jbe@1309 11909 // Try fixing end
bsw/jbe@1309 11910 this.composer.element.appendChild(blankEndNode);
bsw/jbe@1309 11911 doSelect();
bsw/jbe@1309 11912
bsw/jbe@1309 11913 if (notSelected()) {
bsw/jbe@1309 11914 // Remove end fix
bsw/jbe@1309 11915 blankEndNode.parentNode.removeChild(blankEndNode);
bsw/jbe@1309 11916
bsw/jbe@1309 11917 // Try fixing beginning
bsw/jbe@1309 11918 this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild);
bsw/jbe@1309 11919 doSelect();
bsw/jbe@1309 11920
bsw/jbe@1309 11921 if (notSelected()) {
bsw/jbe@1309 11922 // Try fixing both
bsw/jbe@1309 11923 this.composer.element.appendChild(blankEndNode);
bsw/jbe@1309 11924 doSelect();
bsw/jbe@1309 11925 }
bsw/jbe@1309 11926 }
bsw/jbe@1309 11927 }
bsw/jbe@1309 11928 },
bsw/jbe@1309 11929
bsw/jbe@1309 11930 createRange: function() {
bsw/jbe@1309 11931 return rangy.createRange(this.doc);
bsw/jbe@1309 11932 },
bsw/jbe@1309 11933
bsw/jbe@1309 11934 isCollapsed: function() {
bsw/jbe@1309 11935 return this.getSelection().isCollapsed;
bsw/jbe@1309 11936 },
bsw/jbe@1309 11937
bsw/jbe@1309 11938 getHtml: function() {
bsw/jbe@1309 11939 return this.getSelection().toHtml();
bsw/jbe@1309 11940 },
bsw/jbe@1309 11941
bsw/jbe@1309 11942 getPlainText: function () {
bsw/jbe@1309 11943 return this.getSelection().toString();
bsw/jbe@1309 11944 },
bsw/jbe@1309 11945
bsw/jbe@1309 11946 isEndToEndInNode: function(nodeNames) {
bsw/jbe@1309 11947 var range = this.getRange(),
bsw/jbe@1309 11948 parentElement = range.commonAncestorContainer,
bsw/jbe@1309 11949 startNode = range.startContainer,
bsw/jbe@1309 11950 endNode = range.endContainer;
bsw/jbe@1309 11951
bsw/jbe@1309 11952
bsw/jbe@1309 11953 if (parentElement.nodeType === wysihtml.TEXT_NODE) {
bsw/jbe@1309 11954 parentElement = parentElement.parentNode;
bsw/jbe@1309 11955 }
bsw/jbe@1309 11956
bsw/jbe@1309 11957 if (startNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
bsw/jbe@1309 11958 return false;
bsw/jbe@1309 11959 }
bsw/jbe@1309 11960
bsw/jbe@1309 11961 if (endNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
bsw/jbe@1309 11962 return false;
bsw/jbe@1309 11963 }
bsw/jbe@1309 11964
bsw/jbe@1309 11965 while (startNode && startNode !== parentElement) {
bsw/jbe@1309 11966 if (startNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, startNode)) {
bsw/jbe@1309 11967 return false;
bsw/jbe@1309 11968 }
bsw/jbe@1309 11969 if (wysihtml.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
bsw/jbe@1309 11970 return false;
bsw/jbe@1309 11971 }
bsw/jbe@1309 11972 startNode = startNode.parentNode;
bsw/jbe@1309 11973 }
bsw/jbe@1309 11974
bsw/jbe@1309 11975 while (endNode && endNode !== parentElement) {
bsw/jbe@1309 11976 if (endNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, endNode)) {
bsw/jbe@1309 11977 return false;
bsw/jbe@1309 11978 }
bsw/jbe@1309 11979 if (wysihtml.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
bsw/jbe@1309 11980 return false;
bsw/jbe@1309 11981 }
bsw/jbe@1309 11982 endNode = endNode.parentNode;
bsw/jbe@1309 11983 }
bsw/jbe@1309 11984
bsw/jbe@1309 11985 return (wysihtml.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
bsw/jbe@1309 11986 },
bsw/jbe@1309 11987
bsw/jbe@1309 11988 isInThisEditable: function() {
bsw/jbe@1309 11989 var sel = this.getSelection(),
bsw/jbe@1309 11990 fnode = sel.focusNode,
bsw/jbe@1309 11991 anode = sel.anchorNode;
bsw/jbe@1309 11992
bsw/jbe@1309 11993 // In IE node contains will not work for textnodes, thus taking parentNode
bsw/jbe@1309 11994 if (fnode && fnode.nodeType !== 1) {
bsw/jbe@1309 11995 fnode = fnode.parentNode;
bsw/jbe@1309 11996 }
bsw/jbe@1309 11997
bsw/jbe@1309 11998 if (anode && anode.nodeType !== 1) {
bsw/jbe@1309 11999 anode = anode.parentNode;
bsw/jbe@1309 12000 }
bsw/jbe@1309 12001
bsw/jbe@1309 12002 return anode && fnode &&
bsw/jbe@1309 12003 (wysihtml.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) &&
bsw/jbe@1309 12004 (wysihtml.dom.contains(this.composer.element, anode) || this.composer.element === anode);
bsw/jbe@1309 12005 },
bsw/jbe@1309 12006
bsw/jbe@1309 12007 deselect: function() {
bsw/jbe@1309 12008 var sel = this.getSelection();
bsw/jbe@1309 12009 sel && sel.removeAllRanges();
bsw/jbe@1309 12010 }
bsw/jbe@1309 12011 });
bsw/jbe@1309 12012
bsw/jbe@1309 12013 })(wysihtml);
bsw/jbe@1309 12014
bsw/jbe@1309 12015 /**
bsw/jbe@1309 12016 * Rich Text Query/Formatting Commands
bsw/jbe@1309 12017 *
bsw/jbe@1309 12018 * @example
bsw/jbe@1309 12019 * var commands = new wysihtml.Commands(editor);
bsw/jbe@1309 12020 */
bsw/jbe@1309 12021 wysihtml.Commands = Base.extend(
bsw/jbe@1309 12022 /** @scope wysihtml.Commands.prototype */ {
bsw/jbe@1309 12023 constructor: function(editor) {
bsw/jbe@1309 12024 this.editor = editor;
bsw/jbe@1309 12025 this.composer = editor.composer;
bsw/jbe@1309 12026 this.doc = this.composer.doc;
bsw/jbe@1309 12027 },
bsw/jbe@1309 12028
bsw/jbe@1309 12029 /**
bsw/jbe@1309 12030 * Check whether the browser supports the given command
bsw/jbe@1309 12031 *
bsw/jbe@1309 12032 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
bsw/jbe@1309 12033 * @example
bsw/jbe@1309 12034 * commands.supports("createLink");
bsw/jbe@1309 12035 */
bsw/jbe@1309 12036 support: function(command) {
bsw/jbe@1309 12037 return wysihtml.browser.supportsCommand(this.doc, command);
bsw/jbe@1309 12038 },
bsw/jbe@1309 12039
bsw/jbe@1309 12040 /**
bsw/jbe@1309 12041 * Check whether the browser supports the given command
bsw/jbe@1309 12042 *
bsw/jbe@1309 12043 * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
bsw/jbe@1309 12044 * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
bsw/jbe@1309 12045 * @example
bsw/jbe@1309 12046 * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
bsw/jbe@1309 12047 */
bsw/jbe@1309 12048 exec: function(command, value) {
bsw/jbe@1309 12049 var obj = wysihtml.commands[command],
bsw/jbe@1309 12050 args = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309 12051 method = obj && obj.exec,
bsw/jbe@1309 12052 result = null;
bsw/jbe@1309 12053
bsw/jbe@1309 12054 // If composer ahs placeholder unset it before command
bsw/jbe@1309 12055 // Do not apply on commands that are behavioral
bsw/jbe@1309 12056 if (this.composer.hasPlaceholderSet() && !wysihtml.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) {
bsw/jbe@1309 12057 this.composer.element.innerHTML = "";
bsw/jbe@1309 12058 this.composer.selection.selectNode(this.composer.element);
bsw/jbe@1309 12059 }
bsw/jbe@1309 12060
bsw/jbe@1309 12061 this.editor.fire("beforecommand:composer");
bsw/jbe@1309 12062
bsw/jbe@1309 12063 if (method) {
bsw/jbe@1309 12064 args.unshift(this.composer);
bsw/jbe@1309 12065 result = method.apply(obj, args);
bsw/jbe@1309 12066 } else {
bsw/jbe@1309 12067 try {
bsw/jbe@1309 12068 // try/catch for buggy firefox
bsw/jbe@1309 12069 result = this.doc.execCommand(command, false, value);
bsw/jbe@1309 12070 } catch(e) {}
bsw/jbe@1309 12071 }
bsw/jbe@1309 12072
bsw/jbe@1309 12073 this.editor.fire("aftercommand:composer");
bsw/jbe@1309 12074 return result;
bsw/jbe@1309 12075 },
bsw/jbe@1309 12076
bsw/jbe@1309 12077 remove: function(command, commandValue) {
bsw/jbe@1309 12078 var obj = wysihtml.commands[command],
bsw/jbe@1309 12079 args = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309 12080 method = obj && obj.remove;
bsw/jbe@1309 12081 if (method) {
bsw/jbe@1309 12082 args.unshift(this.composer);
bsw/jbe@1309 12083 return method.apply(obj, args);
bsw/jbe@1309 12084 }
bsw/jbe@1309 12085 },
bsw/jbe@1309 12086
bsw/jbe@1309 12087 /**
bsw/jbe@1309 12088 * Check whether the current command is active
bsw/jbe@1309 12089 * If the caret is within a bold text, then calling this with command "bold" should return true
bsw/jbe@1309 12090 *
bsw/jbe@1309 12091 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
bsw/jbe@1309 12092 * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
bsw/jbe@1309 12093 * @return {Boolean} Whether the command is active
bsw/jbe@1309 12094 * @example
bsw/jbe@1309 12095 * var isCurrentSelectionBold = commands.state("bold");
bsw/jbe@1309 12096 */
bsw/jbe@1309 12097 state: function(command, commandValue) {
bsw/jbe@1309 12098 var obj = wysihtml.commands[command],
bsw/jbe@1309 12099 args = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309 12100 method = obj && obj.state;
bsw/jbe@1309 12101 if (method) {
bsw/jbe@1309 12102 args.unshift(this.composer);
bsw/jbe@1309 12103 return method.apply(obj, args);
bsw/jbe@1309 12104 } else {
bsw/jbe@1309 12105 try {
bsw/jbe@1309 12106 // try/catch for buggy firefox
bsw/jbe@1309 12107 return this.doc.queryCommandState(command);
bsw/jbe@1309 12108 } catch(e) {
bsw/jbe@1309 12109 return false;
bsw/jbe@1309 12110 }
bsw/jbe@1309 12111 }
bsw/jbe@1309 12112 },
bsw/jbe@1309 12113
bsw/jbe@1309 12114 /* Get command state parsed value if command has stateValue parsing function */
bsw/jbe@1309 12115 stateValue: function(command) {
bsw/jbe@1309 12116 var obj = wysihtml.commands[command],
bsw/jbe@1309 12117 args = wysihtml.lang.array(arguments).get(),
bsw/jbe@1309 12118 method = obj && obj.stateValue;
bsw/jbe@1309 12119 if (method) {
bsw/jbe@1309 12120 args.unshift(this.composer);
bsw/jbe@1309 12121 return method.apply(obj, args);
bsw/jbe@1309 12122 } else {
bsw/jbe@1309 12123 return false;
bsw/jbe@1309 12124 }
bsw/jbe@1309 12125 }
bsw/jbe@1309 12126 });
bsw/jbe@1309 12127
bsw/jbe@1309 12128 (function(wysihtml) {
bsw/jbe@1309 12129
bsw/jbe@1309 12130 var nodeOptions = {
bsw/jbe@1309 12131 nodeName: "A",
bsw/jbe@1309 12132 toggle: false
bsw/jbe@1309 12133 };
bsw/jbe@1309 12134
bsw/jbe@1309 12135 function getOptions(value) {
bsw/jbe@1309 12136 var options = typeof value === 'object' ? value : {'href': value};
bsw/jbe@1309 12137 return wysihtml.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get();
bsw/jbe@1309 12138 }
bsw/jbe@1309 12139
bsw/jbe@1309 12140 wysihtml.commands.createLink = {
bsw/jbe@1309 12141 exec: function(composer, command, value) {
bsw/jbe@1309 12142 var opts = getOptions(value);
bsw/jbe@1309 12143
bsw/jbe@1309 12144 if (composer.selection.isCollapsed() && !this.state(composer, command)) {
bsw/jbe@1309 12145 var textNode = composer.doc.createTextNode(opts.attribute.href);
bsw/jbe@1309 12146 composer.selection.insertNode(textNode);
bsw/jbe@1309 12147 composer.selection.selectNode(textNode);
bsw/jbe@1309 12148 }
bsw/jbe@1309 12149 wysihtml.commands.formatInline.exec(composer, command, opts);
bsw/jbe@1309 12150 },
bsw/jbe@1309 12151
bsw/jbe@1309 12152 state: function(composer, command) {
bsw/jbe@1309 12153 return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
bsw/jbe@1309 12154 }
bsw/jbe@1309 12155 };
bsw/jbe@1309 12156
bsw/jbe@1309 12157 })(wysihtml);
bsw/jbe@1309 12158
bsw/jbe@1309 12159 /* Formatblock
bsw/jbe@1309 12160 * Is used to insert block level elements
bsw/jbe@1309 12161 * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...)
bsw/jbe@1309 12162 *
bsw/jbe@1309 12163 */
bsw/jbe@1309 12164 (function(wysihtml) {
bsw/jbe@1309 12165
bsw/jbe@1309 12166 var dom = wysihtml.dom,
bsw/jbe@1309 12167 // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
bsw/jbe@1309 12168 // instead of creating a H4 within a H1 which would result in semantically invalid html
bsw/jbe@1309 12169 UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre",
bsw/jbe@1309 12170 BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote",
bsw/jbe@1309 12171 INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u";
bsw/jbe@1309 12172
bsw/jbe@1309 12173 function correctOptionsForSimilarityCheck(options) {
bsw/jbe@1309 12174 return {
bsw/jbe@1309 12175 nodeName: options.nodeName || null,
bsw/jbe@1309 12176 className: (!options.classRegExp) ? options.className || null : null,
bsw/jbe@1309 12177 classRegExp: options.classRegExp || null,
bsw/jbe@1309 12178 styleProperty: options.styleProperty || null
bsw/jbe@1309 12179 };
bsw/jbe@1309 12180 }
bsw/jbe@1309 12181
bsw/jbe@1309 12182 function getRangeNode(node, offset) {
bsw/jbe@1309 12183 if (node.nodeType === 3) {
bsw/jbe@1309 12184 return node;
bsw/jbe@1309 12185 } else {
bsw/jbe@1309 12186 return node.childNodes[offset] || node;
bsw/jbe@1309 12187 }
bsw/jbe@1309 12188 }
bsw/jbe@1309 12189
bsw/jbe@1309 12190 // Returns if node is a line break
bsw/jbe@1309 12191 function isBr(n) {
bsw/jbe@1309 12192 return n && n.nodeType === 1 && n.nodeName === "BR";
bsw/jbe@1309 12193 }
bsw/jbe@1309 12194
bsw/jbe@1309 12195 // Is block level element
bsw/jbe@1309 12196 function isBlock(n, composer) {
bsw/jbe@1309 12197 return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
bsw/jbe@1309 12198 }
bsw/jbe@1309 12199
bsw/jbe@1309 12200 // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
bsw/jbe@1309 12201 function isBookmark(n) {
bsw/jbe@1309 12202 return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
bsw/jbe@1309 12203 }
bsw/jbe@1309 12204
bsw/jbe@1309 12205 // Is line breaking node
bsw/jbe@1309 12206 function isLineBreaking(n, composer) {
bsw/jbe@1309 12207 return isBr(n) || isBlock(n, composer);
bsw/jbe@1309 12208 }
bsw/jbe@1309 12209
bsw/jbe@1309 12210 // Removes empty block level elements
bsw/jbe@1309 12211 function cleanup(composer, newBlockElements) {
bsw/jbe@1309 12212 wysihtml.dom.removeInvisibleSpaces(composer.element);
bsw/jbe@1309 12213 var container = composer.element,
bsw/jbe@1309 12214 allElements = container.querySelectorAll(BLOCK_ELEMENTS),
bsw/jbe@1309 12215 noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
bsw/jbe@1309 12216 uneditables = container.querySelectorAll(noEditQuery),
bsw/jbe@1309 12217 elements = wysihtml.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
bsw/jbe@1309 12218 nbIdx;
bsw/jbe@1309 12219
bsw/jbe@1309 12220 for (var i = elements.length; i--;) {
bsw/jbe@1309 12221 if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) {
bsw/jbe@1309 12222 // If cleanup removes some new block elements. remove them from newblocks array too
bsw/jbe@1309 12223 nbIdx = wysihtml.lang.array(newBlockElements).indexOf(elements[i]);
bsw/jbe@1309 12224 if (nbIdx > -1) {
bsw/jbe@1309 12225 newBlockElements.splice(nbIdx, 1);
bsw/jbe@1309 12226 }
bsw/jbe@1309 12227 elements[i].parentNode.removeChild(elements[i]);
bsw/jbe@1309 12228 }
bsw/jbe@1309 12229 }
bsw/jbe@1309 12230
bsw/jbe@1309 12231 return newBlockElements;
bsw/jbe@1309 12232 }
bsw/jbe@1309 12233
bsw/jbe@1309 12234 function defaultNodeName(composer) {
bsw/jbe@1309 12235 return composer.config.useLineBreaks ? "DIV" : "P";
bsw/jbe@1309 12236 }
bsw/jbe@1309 12237
bsw/jbe@1309 12238 // The outermost un-nestable block element parent of from node
bsw/jbe@1309 12239 function findOuterBlock(node, container, allBlocks) {
bsw/jbe@1309 12240 var n = node,
bsw/jbe@1309 12241 block = null;
bsw/jbe@1309 12242
bsw/jbe@1309 12243 while (n && container && n !== container) {
bsw/jbe@1309 12244 if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) {
bsw/jbe@1309 12245 block = n;
bsw/jbe@1309 12246 }
bsw/jbe@1309 12247 n = n.parentNode;
bsw/jbe@1309 12248 }
bsw/jbe@1309 12249
bsw/jbe@1309 12250 return block;
bsw/jbe@1309 12251 }
bsw/jbe@1309 12252
bsw/jbe@1309 12253 // Clone for splitting the inner inline element out of its parent inline elements context
bsw/jbe@1309 12254 // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
bsw/jbe@1309 12255 function cloneOuterInlines(node, container) {
bsw/jbe@1309 12256 var n = node,
bsw/jbe@1309 12257 innerNode,
bsw/jbe@1309 12258 parentNode,
bsw/jbe@1309 12259 el = null,
bsw/jbe@1309 12260 el2;
bsw/jbe@1309 12261
bsw/jbe@1309 12262 while (n && container && n !== container) {
bsw/jbe@1309 12263 if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
bsw/jbe@1309 12264 parentNode = n;
bsw/jbe@1309 12265 if (el === null) {
bsw/jbe@1309 12266 el = n.cloneNode(false);
bsw/jbe@1309 12267 innerNode = el;
bsw/jbe@1309 12268 } else {
bsw/jbe@1309 12269 el2 = n.cloneNode(false);
bsw/jbe@1309 12270 el2.appendChild(el);
bsw/jbe@1309 12271 el = el2;
bsw/jbe@1309 12272 }
bsw/jbe@1309 12273 }
bsw/jbe@1309 12274 n = n.parentNode;
bsw/jbe@1309 12275 }
bsw/jbe@1309 12276
bsw/jbe@1309 12277 return {
bsw/jbe@1309 12278 parent: parentNode,
bsw/jbe@1309 12279 outerNode: el,
bsw/jbe@1309 12280 innerNode: innerNode
bsw/jbe@1309 12281 };
bsw/jbe@1309 12282 }
bsw/jbe@1309 12283
bsw/jbe@1309 12284 // Formats an element according to options nodeName, className, styleProperty, styleValue
bsw/jbe@1309 12285 // If element is not defined, creates new element
bsw/jbe@1309 12286 // if opotions is null, remove format instead
bsw/jbe@1309 12287 function applyOptionsToElement(element, options, composer) {
bsw/jbe@1309 12288
bsw/jbe@1309 12289 if (!element) {
bsw/jbe@1309 12290 element = composer.doc.createElement(options.nodeName || defaultNodeName(composer));
bsw/jbe@1309 12291 // Add invisible space as otherwise webkit cannot set selection or range to it correctly
bsw/jbe@1309 12292 element.appendChild(composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
bsw/jbe@1309 12293 }
bsw/jbe@1309 12294
bsw/jbe@1309 12295 if (options.nodeName && element.nodeName !== options.nodeName) {
bsw/jbe@1309 12296 element = dom.renameElement(element, options.nodeName);
bsw/jbe@1309 12297 }
bsw/jbe@1309 12298
bsw/jbe@1309 12299 // Remove similar classes before applying className
bsw/jbe@1309 12300 if (options.classRegExp) {
bsw/jbe@1309 12301 element.className = element.className.replace(options.classRegExp, "");
bsw/jbe@1309 12302 }
bsw/jbe@1309 12303 if (options.className) {
bsw/jbe@1309 12304 element.classList.add(options.className);
bsw/jbe@1309 12305 }
bsw/jbe@1309 12306
bsw/jbe@1309 12307 if (options.styleProperty && typeof options.styleValue !== "undefined") {
bsw/jbe@1309 12308 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
bsw/jbe@1309 12309 }
bsw/jbe@1309 12310
bsw/jbe@1309 12311 return element;
bsw/jbe@1309 12312 }
bsw/jbe@1309 12313
bsw/jbe@1309 12314 // Unsets element properties by options
bsw/jbe@1309 12315 // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
bsw/jbe@1309 12316 function removeOptionsFromElement(element, options, composer) {
bsw/jbe@1309 12317 var style, classes,
bsw/jbe@1309 12318 prevNode = element.previousSibling,
bsw/jbe@1309 12319 nextNode = element.nextSibling,
bsw/jbe@1309 12320 unwrapped = false;
bsw/jbe@1309 12321
bsw/jbe@1309 12322 if (options.styleProperty) {
bsw/jbe@1309 12323 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
bsw/jbe@1309 12324 }
bsw/jbe@1309 12325 if (options.className) {
bsw/jbe@1309 12326 element.classList.remove(options.className);
bsw/jbe@1309 12327 }
bsw/jbe@1309 12328
bsw/jbe@1309 12329 if (options.classRegExp) {
bsw/jbe@1309 12330 element.className = element.className.replace(options.classRegExp, "");
bsw/jbe@1309 12331 }
bsw/jbe@1309 12332
bsw/jbe@1309 12333 // Clean up blank class attribute
bsw/jbe@1309 12334 if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") {
bsw/jbe@1309 12335 element.removeAttribute('class');
bsw/jbe@1309 12336 }
bsw/jbe@1309 12337
bsw/jbe@1309 12338 if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
bsw/jbe@1309 12339 style = element.getAttribute('style');
bsw/jbe@1309 12340 if (!style || style.trim() === '') {
bsw/jbe@1309 12341 dom.unwrap(element);
bsw/jbe@1309 12342 unwrapped = true;
bsw/jbe@1309 12343 } else {
bsw/jbe@1309 12344 element = dom.renameElement(element, defaultNodeName(composer));
bsw/jbe@1309 12345 }
bsw/jbe@1309 12346 }
bsw/jbe@1309 12347
bsw/jbe@1309 12348 // Clean up blank style attribute
bsw/jbe@1309 12349 if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
bsw/jbe@1309 12350 element.removeAttribute('style');
bsw/jbe@1309 12351 }
bsw/jbe@1309 12352
bsw/jbe@1309 12353 if (unwrapped) {
bsw/jbe@1309 12354 applySurroundingLineBreaks(prevNode, nextNode, composer);
bsw/jbe@1309 12355 }
bsw/jbe@1309 12356 }
bsw/jbe@1309 12357
bsw/jbe@1309 12358 // Unwraps block level elements from inside content
bsw/jbe@1309 12359 // Useful as not all block level elements can contain other block-levels
bsw/jbe@1309 12360 function unwrapBlocksFromContent(element) {
bsw/jbe@1309 12361 var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
bsw/jbe@1309 12362 nextEl, prevEl;
bsw/jbe@1309 12363
bsw/jbe@1309 12364 for (var i = blocks.length; i--;) {
bsw/jbe@1309 12365 nextEl = wysihtml.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
bsw/jbe@1309 12366 prevEl = wysihtml.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 12367
bsw/jbe@1309 12368 if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
bsw/jbe@1309 12369 if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
bsw/jbe@1309 12370 blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
bsw/jbe@1309 12371 }
bsw/jbe@1309 12372 }
bsw/jbe@1309 12373 if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
bsw/jbe@1309 12374 if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
bsw/jbe@1309 12375 blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
bsw/jbe@1309 12376 }
bsw/jbe@1309 12377 }
bsw/jbe@1309 12378 wysihtml.dom.unwrap(blocks[i]);
bsw/jbe@1309 12379 }
bsw/jbe@1309 12380 }
bsw/jbe@1309 12381
bsw/jbe@1309 12382 // Fix ranges that visually cover whole block element to actually cover the block
bsw/jbe@1309 12383 function fixRangeCoverage(range, composer) {
bsw/jbe@1309 12384 var node,
bsw/jbe@1309 12385 start = range.startContainer,
bsw/jbe@1309 12386 end = range.endContainer;
bsw/jbe@1309 12387
bsw/jbe@1309 12388 // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
bsw/jbe@1309 12389 // This ensures the wrapper node is modified and optios added to it
bsw/jbe@1309 12390 if (start && start.nodeType === 1 && start === end) {
bsw/jbe@1309 12391 if (start.firstChild === start.lastChild && range.endOffset === 1) {
bsw/jbe@1309 12392 if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
bsw/jbe@1309 12393 range.setStartBefore(start);
bsw/jbe@1309 12394 range.setEndAfter(end);
bsw/jbe@1309 12395 }
bsw/jbe@1309 12396 }
bsw/jbe@1309 12397 return;
bsw/jbe@1309 12398 }
bsw/jbe@1309 12399
bsw/jbe@1309 12400 // If range starts outside of node and ends inside at textrange and covers the whole node visually, extend end to cover the node end too
bsw/jbe@1309 12401 if (start && start.nodeType === 1 && end.nodeType === 3) {
bsw/jbe@1309 12402 if (start.firstChild === end && range.endOffset === end.data.length) {
bsw/jbe@1309 12403 if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
bsw/jbe@1309 12404 range.setEndAfter(start);
bsw/jbe@1309 12405 }
bsw/jbe@1309 12406 }
bsw/jbe@1309 12407 return;
bsw/jbe@1309 12408 }
bsw/jbe@1309 12409
bsw/jbe@1309 12410 // If range ends outside of node and starts inside at textrange and covers the whole node visually, extend start to cover the node start too
bsw/jbe@1309 12411 if (end && end.nodeType === 1 && start.nodeType === 3) {
bsw/jbe@1309 12412 if (end.firstChild === start && range.startOffset === 0) {
bsw/jbe@1309 12413 if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
bsw/jbe@1309 12414 range.setStartBefore(end);
bsw/jbe@1309 12415 }
bsw/jbe@1309 12416 }
bsw/jbe@1309 12417 return;
bsw/jbe@1309 12418 }
bsw/jbe@1309 12419
bsw/jbe@1309 12420 // If range covers a whole textnode and the textnode is the only child of node, extend range to node
bsw/jbe@1309 12421 if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
bsw/jbe@1309 12422 if (range.endOffset == end.data.length && range.startOffset === 0) {
bsw/jbe@1309 12423 node = start.parentNode;
bsw/jbe@1309 12424 if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
bsw/jbe@1309 12425 range.setStartBefore(node);
bsw/jbe@1309 12426 range.setEndAfter(node);
bsw/jbe@1309 12427 }
bsw/jbe@1309 12428 }
bsw/jbe@1309 12429 return;
bsw/jbe@1309 12430 }
bsw/jbe@1309 12431 }
bsw/jbe@1309 12432
bsw/jbe@1309 12433 // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
bsw/jbe@1309 12434 // Some places do not allow block level elements inbetween (inside ul and outside li)
bsw/jbe@1309 12435 // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
bsw/jbe@1309 12436 function fixNotPermittedInsertionPoints(ranges) {
bsw/jbe@1309 12437 var newRanges = [],
bsw/jbe@1309 12438 lis, j, maxj, tmpRange, rangePos, closestLI;
bsw/jbe@1309 12439
bsw/jbe@1309 12440 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
bsw/jbe@1309 12441
bsw/jbe@1309 12442 // Fixes range start and end positions if inside UL or OL element (outside of LI)
bsw/jbe@1309 12443 if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
bsw/jbe@1309 12444 ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
bsw/jbe@1309 12445 }
bsw/jbe@1309 12446 if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
bsw/jbe@1309 12447 closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
bsw/jbe@1309 12448 if (closestLI.childNodes) {
bsw/jbe@1309 12449 ranges[i].setEnd(closestLI, closestLI.childNodes.length);
bsw/jbe@1309 12450 }
bsw/jbe@1309 12451 }
bsw/jbe@1309 12452
bsw/jbe@1309 12453 // Get all LI eleemnts in selection (fully or partially covered)
bsw/jbe@1309 12454 // And make sure ranges are either inside LI or outside UL/OL
bsw/jbe@1309 12455 // Split and add new ranges as needed to cover same range content
bsw/jbe@1309 12456 // TODO: Needs improvement to accept DL, DD, DT
bsw/jbe@1309 12457 lis = ranges[i].getNodes([1], function(node) {
bsw/jbe@1309 12458 return node.nodeName === "LI";
bsw/jbe@1309 12459 });
bsw/jbe@1309 12460 if (lis.length > 0) {
bsw/jbe@1309 12461
bsw/jbe@1309 12462 for (j = 0, maxj = lis.length; j < maxj; j++) {
bsw/jbe@1309 12463 rangePos = ranges[i].compareNode(lis[j]);
bsw/jbe@1309 12464
bsw/jbe@1309 12465 // Fixes start of range that crosses LI border
bsw/jbe@1309 12466 if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
bsw/jbe@1309 12467 // Range starts before and ends inside the node
bsw/jbe@1309 12468
bsw/jbe@1309 12469 tmpRange = ranges[i].cloneRange();
bsw/jbe@1309 12470 closestLI = wysihtml.dom.domNode(lis[j]).prev({nodeTypes: [1]});
bsw/jbe@1309 12471
bsw/jbe@1309 12472 if (closestLI) {
bsw/jbe@1309 12473 tmpRange.setEnd(closestLI, closestLI.childNodes.length);
bsw/jbe@1309 12474 } else if (lis[j].closest('ul, ol')) {
bsw/jbe@1309 12475 tmpRange.setEndBefore(lis[j].closest('ul, ol'));
bsw/jbe@1309 12476 } else {
bsw/jbe@1309 12477 tmpRange.setEndBefore(lis[j]);
bsw/jbe@1309 12478 }
bsw/jbe@1309 12479 newRanges.push(tmpRange);
bsw/jbe@1309 12480 ranges[i].setStart(lis[j], 0);
bsw/jbe@1309 12481 }
bsw/jbe@1309 12482
bsw/jbe@1309 12483 // Fixes end of range that crosses li border
bsw/jbe@1309 12484 if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
bsw/jbe@1309 12485 // Range starts inside the node and ends after node
bsw/jbe@1309 12486
bsw/jbe@1309 12487 tmpRange = ranges[i].cloneRange();
bsw/jbe@1309 12488 tmpRange.setEnd(lis[j], lis[j].childNodes.length);
bsw/jbe@1309 12489 newRanges.push(tmpRange);
bsw/jbe@1309 12490
bsw/jbe@1309 12491 // Find next LI in list and if present set range to it, else
bsw/jbe@1309 12492 closestLI = wysihtml.dom.domNode(lis[j]).next({nodeTypes: [1]});
bsw/jbe@1309 12493 if (closestLI) {
bsw/jbe@1309 12494 ranges[i].setStart(closestLI, 0);
bsw/jbe@1309 12495 } else if (lis[j].closest('ul, ol')) {
bsw/jbe@1309 12496 ranges[i].setStartAfter(lis[j].closest('ul, ol'));
bsw/jbe@1309 12497 } else {
bsw/jbe@1309 12498 ranges[i].setStartAfter(lis[j]);
bsw/jbe@1309 12499 }
bsw/jbe@1309 12500 }
bsw/jbe@1309 12501 }
bsw/jbe@1309 12502 newRanges.push(ranges[i]);
bsw/jbe@1309 12503 } else {
bsw/jbe@1309 12504 newRanges.push(ranges[i]);
bsw/jbe@1309 12505 }
bsw/jbe@1309 12506 }
bsw/jbe@1309 12507 return newRanges;
bsw/jbe@1309 12508 }
bsw/jbe@1309 12509
bsw/jbe@1309 12510 // Return options object with nodeName set if original did not have any
bsw/jbe@1309 12511 // Node name is set to local or global default
bsw/jbe@1309 12512 function getOptionsWithNodename(options, defaultName, composer) {
bsw/jbe@1309 12513 var correctedOptions = (options) ? wysihtml.lang.object(options).clone(true) : null;
bsw/jbe@1309 12514 if (correctedOptions) {
bsw/jbe@1309 12515 correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
bsw/jbe@1309 12516 }
bsw/jbe@1309 12517 return correctedOptions;
bsw/jbe@1309 12518 }
bsw/jbe@1309 12519
bsw/jbe@1309 12520 // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
bsw/jbe@1309 12521 // Also wraps empty clones of split parent tags around fragment to keep formatting
bsw/jbe@1309 12522 // If firstOuterBlock is given assume that instead of finding outer (useful for solving cases of some blocks are allowed into others while others are not)
bsw/jbe@1309 12523 function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
bsw/jbe@1309 12524 var rangeStartContainer = range.startContainer,
bsw/jbe@1309 12525 firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
bsw/jbe@1309 12526 outerInlines, first, last, prev, next;
bsw/jbe@1309 12527
bsw/jbe@1309 12528 if (firstOuterBlock) {
bsw/jbe@1309 12529 // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
bsw/jbe@1309 12530 first = fragment.firstChild;
bsw/jbe@1309 12531 last = fragment.lastChild;
bsw/jbe@1309 12532
bsw/jbe@1309 12533 composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
bsw/jbe@1309 12534
bsw/jbe@1309 12535 next = wysihtml.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 12536 prev = wysihtml.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 12537
bsw/jbe@1309 12538 if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
bsw/jbe@1309 12539 first.parentNode.insertBefore(composer.doc.createElement('br'), first);
bsw/jbe@1309 12540 }
bsw/jbe@1309 12541
bsw/jbe@1309 12542 if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
bsw/jbe@1309 12543 next.parentNode.insertBefore(composer.doc.createElement('br'), next);
bsw/jbe@1309 12544 }
bsw/jbe@1309 12545
bsw/jbe@1309 12546 } else {
bsw/jbe@1309 12547 // Ensure node does not get inserted into an inline where it is not allowed
bsw/jbe@1309 12548 outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
bsw/jbe@1309 12549 if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
bsw/jbe@1309 12550 if (fragment.childNodes.length === 1) {
bsw/jbe@1309 12551 while(fragment.firstChild.firstChild) {
bsw/jbe@1309 12552 outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
bsw/jbe@1309 12553 }
bsw/jbe@1309 12554 fragment.firstChild.appendChild(outerInlines.outerNode);
bsw/jbe@1309 12555 }
bsw/jbe@1309 12556 composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
bsw/jbe@1309 12557 } else {
bsw/jbe@1309 12558 var fc = fragment.firstChild,
bsw/jbe@1309 12559 lc = fragment.lastChild;
bsw/jbe@1309 12560
bsw/jbe@1309 12561 range.insertNode(fragment);
bsw/jbe@1309 12562 // restore range position as it might get lost in webkit sometimes
bsw/jbe@1309 12563 range.setStartBefore(fc);
bsw/jbe@1309 12564 range.setEndAfter(lc);
bsw/jbe@1309 12565 }
bsw/jbe@1309 12566 }
bsw/jbe@1309 12567 }
bsw/jbe@1309 12568
bsw/jbe@1309 12569 // Removes all block formatting from range
bsw/jbe@1309 12570 function clearRangeBlockFromating(range, closestBlockName, composer) {
bsw/jbe@1309 12571 var r = range.cloneRange(),
bsw/jbe@1309 12572 prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
bsw/jbe@1309 12573 nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
bsw/jbe@1309 12574 content = r.extractContents(),
bsw/jbe@1309 12575 fragment = composer.doc.createDocumentFragment(),
bsw/jbe@1309 12576 children, blocks,
bsw/jbe@1309 12577 first = true;
bsw/jbe@1309 12578
bsw/jbe@1309 12579 while(content.firstChild) {
bsw/jbe@1309 12580 // Iterate over all selection content first level childNodes
bsw/jbe@1309 12581 if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
bsw/jbe@1309 12582 // If node is a block element
bsw/jbe@1309 12583 // Split block formating and add new block to wrap caret
bsw/jbe@1309 12584
bsw/jbe@1309 12585 unwrapBlocksFromContent(content.firstChild);
bsw/jbe@1309 12586 children = wysihtml.dom.unwrap(content.firstChild);
bsw/jbe@1309 12587
bsw/jbe@1309 12588 // Add line break before if needed
bsw/jbe@1309 12589 if (children.length > 0) {
bsw/jbe@1309 12590 if (
bsw/jbe@1309 12591 (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
bsw/jbe@1309 12592 (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
bsw/jbe@1309 12593 ){
bsw/jbe@1309 12594 fragment.appendChild(composer.doc.createElement('BR'));
bsw/jbe@1309 12595 }
bsw/jbe@1309 12596 }
bsw/jbe@1309 12597
bsw/jbe@1309 12598 for (var c = 0, cmax = children.length; c < cmax; c++) {
bsw/jbe@1309 12599 fragment.appendChild(children[c]);
bsw/jbe@1309 12600 }
bsw/jbe@1309 12601
bsw/jbe@1309 12602 // Add line break after if needed
bsw/jbe@1309 12603 if (children.length > 0) {
bsw/jbe@1309 12604 if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
bsw/jbe@1309 12605 if (nextNode || fragment.lastChild !== content.lastChild) {
bsw/jbe@1309 12606 fragment.appendChild(composer.doc.createElement('BR'));
bsw/jbe@1309 12607 }
bsw/jbe@1309 12608 }
bsw/jbe@1309 12609 }
bsw/jbe@1309 12610
bsw/jbe@1309 12611 } else {
bsw/jbe@1309 12612 fragment.appendChild(content.firstChild);
bsw/jbe@1309 12613 }
bsw/jbe@1309 12614
bsw/jbe@1309 12615 first = false;
bsw/jbe@1309 12616 }
bsw/jbe@1309 12617 blocks = wysihtml.lang.array(fragment.childNodes).get();
bsw/jbe@1309 12618 injectFragmentToRange(fragment, r, composer);
bsw/jbe@1309 12619 return blocks;
bsw/jbe@1309 12620 }
bsw/jbe@1309 12621
bsw/jbe@1309 12622 // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
bsw/jbe@1309 12623 function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
bsw/jbe@1309 12624 var prevPrev = prevNode && wysihtml.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 12625 if (isBr(nextNode)) {
bsw/jbe@1309 12626 nextNode.parentNode.removeChild(nextNode);
bsw/jbe@1309 12627 }
bsw/jbe@1309 12628 if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
bsw/jbe@1309 12629 prevNode.parentNode.removeChild(prevNode);
bsw/jbe@1309 12630 }
bsw/jbe@1309 12631 }
bsw/jbe@1309 12632
bsw/jbe@1309 12633 function applySurroundingLineBreaks(prevNode, nextNode, composer) {
bsw/jbe@1309 12634 var prevPrev;
bsw/jbe@1309 12635
bsw/jbe@1309 12636 if (prevNode && isBookmark(prevNode)) {
bsw/jbe@1309 12637 prevNode = prevNode.previousSibling;
bsw/jbe@1309 12638 }
bsw/jbe@1309 12639 if (nextNode && isBookmark(nextNode)) {
bsw/jbe@1309 12640 nextNode = nextNode.nextSibling;
bsw/jbe@1309 12641 }
bsw/jbe@1309 12642
bsw/jbe@1309 12643 prevPrev = prevNode && prevNode.previousSibling;
bsw/jbe@1309 12644
bsw/jbe@1309 12645 if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
bsw/jbe@1309 12646 prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
bsw/jbe@1309 12647 }
bsw/jbe@1309 12648
bsw/jbe@1309 12649 if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
bsw/jbe@1309 12650 nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
bsw/jbe@1309 12651 }
bsw/jbe@1309 12652 }
bsw/jbe@1309 12653
bsw/jbe@1309 12654 var isWhitespaceBefore = function (textNode, offset) {
bsw/jbe@1309 12655 var str = textNode.data ? textNode.data.slice(0, offset) : "";
bsw/jbe@1309 12656 return (/^\s*$/).test(str);
bsw/jbe@1309 12657 }
bsw/jbe@1309 12658
bsw/jbe@1309 12659 var isWhitespaceAfter = function (textNode, offset) {
bsw/jbe@1309 12660 var str = textNode.data ? textNode.data.slice(offset) : "";
bsw/jbe@1309 12661 return (/^\s*$/).test(str);
bsw/jbe@1309 12662 }
bsw/jbe@1309 12663
bsw/jbe@1309 12664 var trimBlankTextsAndBreaks = function(fragment) {
bsw/jbe@1309 12665 if (fragment) {
bsw/jbe@1309 12666 while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309 12667 fragment.removeChild(fragment.firstChild);
bsw/jbe@1309 12668 }
bsw/jbe@1309 12669
bsw/jbe@1309 12670 while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309 12671 fragment.removeChild(fragment.lastChild);
bsw/jbe@1309 12672 }
bsw/jbe@1309 12673
bsw/jbe@1309 12674 if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309 12675 fragment.removeChild(fragment.firstChild);
bsw/jbe@1309 12676 }
bsw/jbe@1309 12677
bsw/jbe@1309 12678 if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
bsw/jbe@1309 12679 fragment.removeChild(fragment.lastChild);
bsw/jbe@1309 12680 }
bsw/jbe@1309 12681 }
bsw/jbe@1309 12682 }
bsw/jbe@1309 12683
bsw/jbe@1309 12684 // Wrap the range with a block level element
bsw/jbe@1309 12685 // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
bsw/jbe@1309 12686 function wrapRangeWithElement(range, options, closestBlockName, composer) {
bsw/jbe@1309 12687 var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
bsw/jbe@1309 12688 r = range.cloneRange(),
bsw/jbe@1309 12689 rangeStartContainer = r.startContainer,
bsw/jbe@1309 12690 startNode = getRangeNode(r.startContainer, r.startOffset),
bsw/jbe@1309 12691 endNode = getRangeNode(r.endContainer, r.endOffset),
bsw/jbe@1309 12692 prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
bsw/jbe@1309 12693 nextNode = (
bsw/jbe@1309 12694 (
bsw/jbe@1309 12695 r.endContainer.nodeType === 1 &&
bsw/jbe@1309 12696 r.endContainer.childNodes[r.endOffset] === endNode &&
bsw/jbe@1309 12697 (
bsw/jbe@1309 12698 endNode.nodeType === 1 ||
bsw/jbe@1309 12699 !isWhitespaceAfter(endNode, r.endOffset) &&
bsw/jbe@1309 12700 !wysihtml.dom.domNode(endNode).is.rangyBookmark()
bsw/jbe@1309 12701 )
bsw/jbe@1309 12702 ) || (
bsw/jbe@1309 12703 r.endContainer === endNode &&
bsw/jbe@1309 12704 endNode.nodeType === 3 &&
bsw/jbe@1309 12705 !isWhitespaceAfter(endNode, r.endOffset)
bsw/jbe@1309 12706 )
bsw/jbe@1309 12707 ) ? endNode : wysihtml.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
bsw/jbe@1309 12708 content = r.extractContents(),
bsw/jbe@1309 12709 fragment = composer.doc.createDocumentFragment(),
bsw/jbe@1309 12710 similarOuterBlock = similarOptions ? wysihtml.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
bsw/jbe@1309 12711 splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
bsw/jbe@1309 12712 firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
bsw/jbe@1309 12713 wrapper, blocks, children,
bsw/jbe@1309 12714 firstc, lastC;
bsw/jbe@1309 12715
bsw/jbe@1309 12716 if (wysihtml.dom.domNode(nextNode).is.rangyBookmark()) {
bsw/jbe@1309 12717 endNode = nextNode;
bsw/jbe@1309 12718 nextNode = endNode.nextSibling;
bsw/jbe@1309 12719 }
bsw/jbe@1309 12720
bsw/jbe@1309 12721 trimBlankTextsAndBreaks(content);
bsw/jbe@1309 12722
bsw/jbe@1309 12723 if (options && options.nodeName === "BLOCKQUOTE") {
bsw/jbe@1309 12724
bsw/jbe@1309 12725 // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
bsw/jbe@1309 12726 var tmpEl = applyOptionsToElement(null, options, composer);
bsw/jbe@1309 12727 tmpEl.appendChild(content);
bsw/jbe@1309 12728 fragment.appendChild(tmpEl);
bsw/jbe@1309 12729 blocks = [tmpEl];
bsw/jbe@1309 12730
bsw/jbe@1309 12731 } else {
bsw/jbe@1309 12732
bsw/jbe@1309 12733 if (!content.firstChild) {
bsw/jbe@1309 12734 // IF selection is caret (can happen if line is empty) add format around tag
bsw/jbe@1309 12735 fragment.appendChild(applyOptionsToElement(null, options, composer));
bsw/jbe@1309 12736 } else {
bsw/jbe@1309 12737
bsw/jbe@1309 12738 while(content.firstChild) {
bsw/jbe@1309 12739 // Iterate over all selection content first level childNodes
bsw/jbe@1309 12740
bsw/jbe@1309 12741 if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
bsw/jbe@1309 12742
bsw/jbe@1309 12743 // If node is a block element
bsw/jbe@1309 12744 // Escape(split) block formatting at caret
bsw/jbe@1309 12745 applyOptionsToElement(content.firstChild, options, composer);
bsw/jbe@1309 12746 if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
bsw/jbe@1309 12747 unwrapBlocksFromContent(content.firstChild);
bsw/jbe@1309 12748 }
bsw/jbe@1309 12749 fragment.appendChild(content.firstChild);
bsw/jbe@1309 12750
bsw/jbe@1309 12751 } else {
bsw/jbe@1309 12752
bsw/jbe@1309 12753 // Wrap subsequent non-block nodes inside new block element
bsw/jbe@1309 12754 wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
bsw/jbe@1309 12755 while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
bsw/jbe@1309 12756 if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
bsw/jbe@1309 12757 unwrapBlocksFromContent(content.firstChild);
bsw/jbe@1309 12758 }
bsw/jbe@1309 12759 wrapper.appendChild(content.firstChild);
bsw/jbe@1309 12760 }
bsw/jbe@1309 12761 fragment.appendChild(wrapper);
bsw/jbe@1309 12762 }
bsw/jbe@1309 12763 }
bsw/jbe@1309 12764 }
bsw/jbe@1309 12765
bsw/jbe@1309 12766 blocks = wysihtml.lang.array(fragment.childNodes).get();
bsw/jbe@1309 12767 }
bsw/jbe@1309 12768 injectFragmentToRange(fragment, r, composer, firstOuterBlock);
bsw/jbe@1309 12769 removeSurroundingLineBreaks(prevNode, nextNode, composer);
bsw/jbe@1309 12770
bsw/jbe@1309 12771 // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block
bsw/jbe@1309 12772 // (if it contains rangy bookmark, so selection can be restored later correctly)
bsw/jbe@1309 12773 if (blocks.length > 0 &&
bsw/jbe@1309 12774 (
bsw/jbe@1309 12775 typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark()
bsw/jbe@1309 12776 )
bsw/jbe@1309 12777 ) {
bsw/jbe@1309 12778 blocks[blocks.length - 1].appendChild(composer.doc.createElement('br'));
bsw/jbe@1309 12779 }
bsw/jbe@1309 12780 return blocks;
bsw/jbe@1309 12781 }
bsw/jbe@1309 12782
bsw/jbe@1309 12783 // Find closest block level element
bsw/jbe@1309 12784 function getParentBlockNodeName(element, composer) {
bsw/jbe@1309 12785 var parentNode = wysihtml.dom.getParentElement(element, {
bsw/jbe@1309 12786 query: BLOCK_ELEMENTS
bsw/jbe@1309 12787 }, null, composer.element);
bsw/jbe@1309 12788
bsw/jbe@1309 12789 return (parentNode) ? parentNode.nodeName : null;
bsw/jbe@1309 12790 }
bsw/jbe@1309 12791
bsw/jbe@1309 12792 // Expands caret to cover the closest block that:
bsw/jbe@1309 12793 // * cannot contain other block level elements (h1-6,p, etc)
bsw/jbe@1309 12794 // * Has the same nodeName that is to be inserted
bsw/jbe@1309 12795 // * has insertingNodeName
bsw/jbe@1309 12796 // * is DIV if insertingNodeName is not present
bsw/jbe@1309 12797 //
bsw/jbe@1309 12798 // If nothing found selects the current line
bsw/jbe@1309 12799 function expandCaretToBlock(composer, insertingNodeName) {
bsw/jbe@1309 12800 var parent = wysihtml.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
bsw/jbe@1309 12801 query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
bsw/jbe@1309 12802 }, null, composer.element),
bsw/jbe@1309 12803 range;
bsw/jbe@1309 12804
bsw/jbe@1309 12805 if (parent) {
bsw/jbe@1309 12806 range = composer.selection.createRange();
bsw/jbe@1309 12807 range.selectNode(parent);
bsw/jbe@1309 12808 composer.selection.setSelection(range);
bsw/jbe@1309 12809 } else if (!composer.isEmpty()) {
bsw/jbe@1309 12810 composer.selection.selectLine();
bsw/jbe@1309 12811 }
bsw/jbe@1309 12812 }
bsw/jbe@1309 12813
bsw/jbe@1309 12814 // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
bsw/jbe@1309 12815 // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
bsw/jbe@1309 12816 function selectElements(newBlockElements, composer) {
bsw/jbe@1309 12817 var range = composer.selection.createRange(),
bsw/jbe@1309 12818 lastEl = newBlockElements[newBlockElements.length - 1],
bsw/jbe@1309 12819 lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
bsw/jbe@1309 12820
bsw/jbe@1309 12821 range.setStart(newBlockElements[0], 0);
bsw/jbe@1309 12822 range.setEnd(lastEl, lastOffset);
bsw/jbe@1309 12823 range.select();
bsw/jbe@1309 12824 }
bsw/jbe@1309 12825
bsw/jbe@1309 12826 // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
bsw/jbe@1309 12827 // Return created/modified block level elements
bsw/jbe@1309 12828 // Method can be either "apply" or "remove"
bsw/jbe@1309 12829 function formatSelection(method, composer, options) {
bsw/jbe@1309 12830 var ranges = composer.selection.getOwnRanges(),
bsw/jbe@1309 12831 newBlockElements = [],
bsw/jbe@1309 12832 closestBlockName;
bsw/jbe@1309 12833
bsw/jbe@1309 12834 // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
bsw/jbe@1309 12835 ranges = fixNotPermittedInsertionPoints(ranges);
bsw/jbe@1309 12836
bsw/jbe@1309 12837 for (var i = ranges.length; i--;) {
bsw/jbe@1309 12838 fixRangeCoverage(ranges[i], composer);
bsw/jbe@1309 12839 closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
bsw/jbe@1309 12840 if (method === "remove") {
bsw/jbe@1309 12841 newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
bsw/jbe@1309 12842 } else {
bsw/jbe@1309 12843 newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
bsw/jbe@1309 12844 }
bsw/jbe@1309 12845 }
bsw/jbe@1309 12846 return newBlockElements;
bsw/jbe@1309 12847 }
bsw/jbe@1309 12848
bsw/jbe@1309 12849 // If properties is passed as a string, look for tag with that tagName/query
bsw/jbe@1309 12850 function parseOptions(options) {
bsw/jbe@1309 12851 if (typeof options === "string") {
bsw/jbe@1309 12852 options = {
bsw/jbe@1309 12853 nodeName: options.toUpperCase()
bsw/jbe@1309 12854 };
bsw/jbe@1309 12855 }
bsw/jbe@1309 12856 return options;
bsw/jbe@1309 12857 }
bsw/jbe@1309 12858
bsw/jbe@1309 12859 function caretIsOnEmptyLine(composer) {
bsw/jbe@1309 12860 var caretInfo;
bsw/jbe@1309 12861 if (composer.selection.isCollapsed()) {
bsw/jbe@1309 12862 caretInfo = composer.selection.getNodesNearCaret();
bsw/jbe@1309 12863 if (caretInfo && caretInfo.caretNode) {
bsw/jbe@1309 12864 if (
bsw/jbe@1309 12865 // caret is allready breaknode
bsw/jbe@1309 12866 wysihtml.dom.domNode(caretInfo.caretNode).is.lineBreak() ||
bsw/jbe@1309 12867 // caret is textnode
bsw/jbe@1309 12868 (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak())) ||
bsw/jbe@1309 12869 // Caret is temprorary rangy selection marker
bsw/jbe@1309 12870 (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') &&
bsw/jbe@1309 12871 (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.prevNode).is.block()) &&
bsw/jbe@1309 12872 (!caretInfo.nextNode || wysihtml.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.nextNode).is.block())
bsw/jbe@1309 12873 )
bsw/jbe@1309 12874 ) {
bsw/jbe@1309 12875 return true;
bsw/jbe@1309 12876 }
bsw/jbe@1309 12877 }
bsw/jbe@1309 12878 }
bsw/jbe@1309 12879 return false;
bsw/jbe@1309 12880 }
bsw/jbe@1309 12881
bsw/jbe@1309 12882 wysihtml.commands.formatBlock = {
bsw/jbe@1309 12883 exec: function(composer, command, options) {
bsw/jbe@1309 12884 options = parseOptions(options);
bsw/jbe@1309 12885 var newBlockElements = [],
bsw/jbe@1309 12886 ranges, range, bookmark, state, closestBlockName;
bsw/jbe@1309 12887
bsw/jbe@1309 12888 // Find if current format state is active if options.toggle is set as true
bsw/jbe@1309 12889 // In toggle case active state elemets are formatted instead of working directly on selection
bsw/jbe@1309 12890 if (options && options.toggle) {
bsw/jbe@1309 12891 state = this.state(composer, command, options);
bsw/jbe@1309 12892 }
bsw/jbe@1309 12893 if (state) {
bsw/jbe@1309 12894 // Remove format from state nodes if toggle set and state on and selection is collapsed
bsw/jbe@1309 12895 bookmark = rangy.saveSelection(composer.win);
bsw/jbe@1309 12896 for (var j = 0, jmax = state.length; j < jmax; j++) {
bsw/jbe@1309 12897 removeOptionsFromElement(state[j], options, composer);
bsw/jbe@1309 12898 }
bsw/jbe@1309 12899
bsw/jbe@1309 12900 } else {
bsw/jbe@1309 12901 // If selection is caret expand it to cover nearest suitable block element or row if none found
bsw/jbe@1309 12902 if (composer.selection.isCollapsed()) {
bsw/jbe@1309 12903 bookmark = rangy.saveSelection(composer.win);
bsw/jbe@1309 12904 if (caretIsOnEmptyLine(composer)) {
bsw/jbe@1309 12905 composer.selection.selectLine();
bsw/jbe@1309 12906 } else {
bsw/jbe@1309 12907 expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
bsw/jbe@1309 12908 }
bsw/jbe@1309 12909 }
bsw/jbe@1309 12910 if (options) {
bsw/jbe@1309 12911 newBlockElements = formatSelection("apply", composer, options);
bsw/jbe@1309 12912 } else {
bsw/jbe@1309 12913 // Options == null means block formatting should be removed from selection
bsw/jbe@1309 12914 newBlockElements = formatSelection("remove", composer);
bsw/jbe@1309 12915 }
bsw/jbe@1309 12916
bsw/jbe@1309 12917 }
bsw/jbe@1309 12918
bsw/jbe@1309 12919 // Remove empty block elements that may be left behind
bsw/jbe@1309 12920 // Also remove them from new blocks list
bsw/jbe@1309 12921 newBlockElements = cleanup(composer, newBlockElements);
bsw/jbe@1309 12922
bsw/jbe@1309 12923 // Restore selection
bsw/jbe@1309 12924 if (bookmark) {
bsw/jbe@1309 12925 rangy.restoreSelection(bookmark);
bsw/jbe@1309 12926 } else {
bsw/jbe@1309 12927 selectElements(newBlockElements, composer);
bsw/jbe@1309 12928 }
bsw/jbe@1309 12929 },
bsw/jbe@1309 12930
bsw/jbe@1309 12931 // Removes all block formatting from selection
bsw/jbe@1309 12932 remove: function(composer, command, options) {
bsw/jbe@1309 12933 options = parseOptions(options);
bsw/jbe@1309 12934 var newBlockElements, bookmark;
bsw/jbe@1309 12935
bsw/jbe@1309 12936 // If selection is caret expand it to cover nearest suitable block element or row if none found
bsw/jbe@1309 12937 if (composer.selection.isCollapsed()) {
bsw/jbe@1309 12938 bookmark = rangy.saveSelection(composer.win);
bsw/jbe@1309 12939 expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
bsw/jbe@1309 12940 }
bsw/jbe@1309 12941
bsw/jbe@1309 12942 newBlockElements = formatSelection("remove", composer);
bsw/jbe@1309 12943 newBlockElements = cleanup(composer, newBlockElements);
bsw/jbe@1309 12944
bsw/jbe@1309 12945 // Restore selection
bsw/jbe@1309 12946 if (bookmark) {
bsw/jbe@1309 12947 rangy.restoreSelection(bookmark);
bsw/jbe@1309 12948 } else {
bsw/jbe@1309 12949 selectElements(newBlockElements, composer);
bsw/jbe@1309 12950 }
bsw/jbe@1309 12951 },
bsw/jbe@1309 12952
bsw/jbe@1309 12953 // If options as null is passed returns status describing all block level elements
bsw/jbe@1309 12954 state: function(composer, command, options) {
bsw/jbe@1309 12955 options = parseOptions(options);
bsw/jbe@1309 12956
bsw/jbe@1309 12957 var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
bsw/jbe@1309 12958 return wysihtml.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
bsw/jbe@1309 12959 }).bind(this)),
bsw/jbe@1309 12960 parentNodes = composer.selection.getSelectedOwnNodes(),
bsw/jbe@1309 12961 parent;
bsw/jbe@1309 12962
bsw/jbe@1309 12963 // Finds matching elements that are parents of selection and adds to nodes list
bsw/jbe@1309 12964 for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
bsw/jbe@1309 12965 parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
bsw/jbe@1309 12966 if (parent && nodes.indexOf(parent) === -1) {
bsw/jbe@1309 12967 nodes.push(parent);
bsw/jbe@1309 12968 }
bsw/jbe@1309 12969 }
bsw/jbe@1309 12970
bsw/jbe@1309 12971 return (nodes.length === 0) ? false : nodes;
bsw/jbe@1309 12972 }
bsw/jbe@1309 12973
bsw/jbe@1309 12974 };
bsw/jbe@1309 12975 })(wysihtml);
bsw/jbe@1309 12976
bsw/jbe@1309 12977 /**
bsw/jbe@1309 12978 * Unifies all inline tags additions and removals
bsw/jbe@1309 12979 * See https://github.com/Voog/wysihtml/pull/169 for specification of action
bsw/jbe@1309 12980 */
bsw/jbe@1309 12981
bsw/jbe@1309 12982 (function(wysihtml) {
bsw/jbe@1309 12983
bsw/jbe@1309 12984 var defaultTag = "SPAN",
bsw/jbe@1309 12985 INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u",
bsw/jbe@1309 12986 queryAliasMap = {
bsw/jbe@1309 12987 "b": "b, strong",
bsw/jbe@1309 12988 "strong": "b, strong",
bsw/jbe@1309 12989 "em": "em, i",
bsw/jbe@1309 12990 "i": "em, i"
bsw/jbe@1309 12991 };
bsw/jbe@1309 12992
bsw/jbe@1309 12993 function hasNoClass(element) {
bsw/jbe@1309 12994 return (/^\s*$/).test(element.className);
bsw/jbe@1309 12995 }
bsw/jbe@1309 12996
bsw/jbe@1309 12997 function hasNoStyle(element) {
bsw/jbe@1309 12998 return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style'));
bsw/jbe@1309 12999 }
bsw/jbe@1309 13000
bsw/jbe@1309 13001 // Associative arrays in javascript are really objects and do not have length defined
bsw/jbe@1309 13002 // Thus have to check emptyness in a different way
bsw/jbe@1309 13003 function hasNoAttributes(element) {
bsw/jbe@1309 13004 var attr = wysihtml.dom.getAttributes(element);
bsw/jbe@1309 13005 return wysihtml.lang.object(attr).isEmpty();
bsw/jbe@1309 13006 }
bsw/jbe@1309 13007
bsw/jbe@1309 13008 // compares two nodes if they are semantically the same
bsw/jbe@1309 13009 // Used in cleanup to find consequent semantically similar elements for merge
bsw/jbe@1309 13010 function isSameNode(element1, element2) {
bsw/jbe@1309 13011 var classes1, classes2,
bsw/jbe@1309 13012 attr1, attr2;
bsw/jbe@1309 13013
bsw/jbe@1309 13014 if (element1.nodeType !== 1 || element2.nodeType !== 1) {
bsw/jbe@1309 13015 return false;
bsw/jbe@1309 13016 }
bsw/jbe@1309 13017
bsw/jbe@1309 13018 if (element1.nodeName !== element2.nodeName) {
bsw/jbe@1309 13019 return false;
bsw/jbe@1309 13020 }
bsw/jbe@1309 13021
bsw/jbe@1309 13022 classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' ');
bsw/jbe@1309 13023 classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' ');
bsw/jbe@1309 13024 if (wysihtml.lang.array(classes1).without(classes2).length > 0) {
bsw/jbe@1309 13025 return false;
bsw/jbe@1309 13026 }
bsw/jbe@1309 13027
bsw/jbe@1309 13028 attr1 = wysihtml.dom.getAttributes(element1);
bsw/jbe@1309 13029 attr2 = wysihtml.dom.getAttributes(element2);
bsw/jbe@1309 13030
bsw/jbe@1309 13031 if (attr1.length !== attr2.length || !wysihtml.lang.object(wysihtml.lang.object(attr1).difference(attr2)).isEmpty()) {
bsw/jbe@1309 13032 return false;
bsw/jbe@1309 13033 }
bsw/jbe@1309 13034
bsw/jbe@1309 13035 return true;
bsw/jbe@1309 13036 }
bsw/jbe@1309 13037
bsw/jbe@1309 13038 function createWrapNode(textNode, options) {
bsw/jbe@1309 13039 var nodeName = options && options.nodeName || defaultTag,
bsw/jbe@1309 13040 element = textNode.ownerDocument.createElement(nodeName);
bsw/jbe@1309 13041
bsw/jbe@1309 13042 // Remove similar classes before applying className
bsw/jbe@1309 13043 if (options.classRegExp) {
bsw/jbe@1309 13044 element.className = element.className.replace(options.classRegExp, "");
bsw/jbe@1309 13045 }
bsw/jbe@1309 13046
bsw/jbe@1309 13047 if (options.className) {
bsw/jbe@1309 13048 element.classList.add(options.className);
bsw/jbe@1309 13049 }
bsw/jbe@1309 13050
bsw/jbe@1309 13051 if (options.styleProperty && typeof options.styleValue !== "undefined") {
bsw/jbe@1309 13052 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
bsw/jbe@1309 13053 }
bsw/jbe@1309 13054
bsw/jbe@1309 13055 if (options.attribute) {
bsw/jbe@1309 13056 if (typeof options.attribute === "object") {
bsw/jbe@1309 13057 for (var a in options.attribute) {
bsw/jbe@1309 13058 if (options.attribute.hasOwnProperty(a)) {
bsw/jbe@1309 13059 element.setAttribute(a, options.attribute[a]);
bsw/jbe@1309 13060 }
bsw/jbe@1309 13061 }
bsw/jbe@1309 13062 } else if (typeof options.attributeValue !== "undefined") {
bsw/jbe@1309 13063 element.setAttribute(options.attribute, options.attributeValue);
bsw/jbe@1309 13064 }
bsw/jbe@1309 13065 }
bsw/jbe@1309 13066
bsw/jbe@1309 13067 return element;
bsw/jbe@1309 13068 }
bsw/jbe@1309 13069
bsw/jbe@1309 13070 // Tests if attr2 list contains all attributes present in attr1
bsw/jbe@1309 13071 // Note: attr 1 can have more attributes than attr2
bsw/jbe@1309 13072 function containsSameAttributes(attr1, attr2) {
bsw/jbe@1309 13073 for (var a in attr1) {
bsw/jbe@1309 13074 if (attr1.hasOwnProperty(a)) {
bsw/jbe@1309 13075 if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) {
bsw/jbe@1309 13076 return false;
bsw/jbe@1309 13077 }
bsw/jbe@1309 13078 }
bsw/jbe@1309 13079 }
bsw/jbe@1309 13080 return true;
bsw/jbe@1309 13081 }
bsw/jbe@1309 13082
bsw/jbe@1309 13083 // If attrbutes and values are the same > remove
bsw/jbe@1309 13084 // if attributes or values
bsw/jbe@1309 13085 function updateElementAttributes(element, newAttributes, toggle) {
bsw/jbe@1309 13086 var attr = wysihtml.dom.getAttributes(element),
bsw/jbe@1309 13087 fullContain = containsSameAttributes(newAttributes, attr),
bsw/jbe@1309 13088 attrDifference = wysihtml.lang.object(attr).difference(newAttributes),
bsw/jbe@1309 13089 a, b;
bsw/jbe@1309 13090
bsw/jbe@1309 13091 if (fullContain && toggle !== false) {
bsw/jbe@1309 13092 for (a in newAttributes) {
bsw/jbe@1309 13093 if (newAttributes.hasOwnProperty(a)) {
bsw/jbe@1309 13094 element.removeAttribute(a);
bsw/jbe@1309 13095 }
bsw/jbe@1309 13096 }
bsw/jbe@1309 13097 } else {
bsw/jbe@1309 13098
bsw/jbe@1309 13099 /*if (!wysihtml.lang.object(attrDifference).isEmpty()) {
bsw/jbe@1309 13100 for (b in attrDifference) {
bsw/jbe@1309 13101 if (attrDifference.hasOwnProperty(b)) {
bsw/jbe@1309 13102 element.removeAttribute(b);
bsw/jbe@1309 13103 }
bsw/jbe@1309 13104 }
bsw/jbe@1309 13105 }*/
bsw/jbe@1309 13106
bsw/jbe@1309 13107 for (a in newAttributes) {
bsw/jbe@1309 13108 if (newAttributes.hasOwnProperty(a)) {
bsw/jbe@1309 13109 element.setAttribute(a, newAttributes[a]);
bsw/jbe@1309 13110 }
bsw/jbe@1309 13111 }
bsw/jbe@1309 13112 }
bsw/jbe@1309 13113 }
bsw/jbe@1309 13114
bsw/jbe@1309 13115 function updateFormatOfElement(element, options) {
bsw/jbe@1309 13116 var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch;
bsw/jbe@1309 13117
bsw/jbe@1309 13118 if (options.className) {
bsw/jbe@1309 13119 if (options.toggle !== false && element.classList.contains(options.className)) {
bsw/jbe@1309 13120 element.classList.remove(options.className);
bsw/jbe@1309 13121 } else {
bsw/jbe@1309 13122 if (options.classRegExp) {
bsw/jbe@1309 13123 element.className = element.className.replace(options.classRegExp, '');
bsw/jbe@1309 13124 }
bsw/jbe@1309 13125 element.classList.add(options.className);
bsw/jbe@1309 13126 }
bsw/jbe@1309 13127 if (hasNoClass(element)) {
bsw/jbe@1309 13128 element.removeAttribute('class');
bsw/jbe@1309 13129 }
bsw/jbe@1309 13130 }
bsw/jbe@1309 13131
bsw/jbe@1309 13132 // change/remove style
bsw/jbe@1309 13133 if (options.styleProperty) {
bsw/jbe@1309 13134 if (options.toggle !== false && element.style[wysihtml.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) {
bsw/jbe@1309 13135 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
bsw/jbe@1309 13136 } else {
bsw/jbe@1309 13137 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
bsw/jbe@1309 13138 }
bsw/jbe@1309 13139 }
bsw/jbe@1309 13140 if (hasNoStyle(element)) {
bsw/jbe@1309 13141 element.removeAttribute('style');
bsw/jbe@1309 13142 }
bsw/jbe@1309 13143
bsw/jbe@1309 13144 if (options.attribute) {
bsw/jbe@1309 13145 if (typeof options.attribute === "object") {
bsw/jbe@1309 13146 newAttributes = options.attribute;
bsw/jbe@1309 13147 } else {
bsw/jbe@1309 13148 newAttributes = {};
bsw/jbe@1309 13149 newAttributes[options.attribute] = options.attributeValue || '';
bsw/jbe@1309 13150 }
bsw/jbe@1309 13151 updateElementAttributes(element, newAttributes, options.toggle);
bsw/jbe@1309 13152 }
bsw/jbe@1309 13153
bsw/jbe@1309 13154
bsw/jbe@1309 13155 // Handle similar semantically same elements (queryAliasMap)
bsw/jbe@1309 13156 nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null;
bsw/jbe@1309 13157 nodeQueryMatch = nodeNameQuery ? wysihtml.dom.domNode(element).test({ query: nodeNameQuery }) : false;
bsw/jbe@1309 13158
bsw/jbe@1309 13159 // Unwrap element if no attributes present and node name given
bsw/jbe@1309 13160 // or no attributes and if no nodename set but node is the default
bsw/jbe@1309 13161 if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) {
bsw/jbe@1309 13162 if (
bsw/jbe@1309 13163 ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) &&
bsw/jbe@1309 13164 hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)
bsw/jbe@1309 13165 ) {
bsw/jbe@1309 13166 wysihtml.dom.unwrap(element);
bsw/jbe@1309 13167 }
bsw/jbe@1309 13168
bsw/jbe@1309 13169 }
bsw/jbe@1309 13170 }
bsw/jbe@1309 13171
bsw/jbe@1309 13172 // Fetch all textnodes in selection
bsw/jbe@1309 13173 // Empty textnodes are ignored except the one containing text caret
bsw/jbe@1309 13174 function getSelectedTextNodes(selection, splitBounds) {
bsw/jbe@1309 13175 var textNodes = [];
bsw/jbe@1309 13176
bsw/jbe@1309 13177 if (!selection.isCollapsed()) {
bsw/jbe@1309 13178 textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) {
bsw/jbe@1309 13179 // Exclude empty nodes except caret node
bsw/jbe@1309 13180 return (!wysihtml.dom.domNode(node).is.emptyTextNode());
bsw/jbe@1309 13181 }, splitBounds));
bsw/jbe@1309 13182 }
bsw/jbe@1309 13183
bsw/jbe@1309 13184 return textNodes;
bsw/jbe@1309 13185 }
bsw/jbe@1309 13186
bsw/jbe@1309 13187 function findSimilarTextNodeWrapper(textNode, options, container, exact) {
bsw/jbe@1309 13188 var node = textNode,
bsw/jbe@1309 13189 similarOptions = exact ? options : correctOptionsForSimilarityCheck(options);
bsw/jbe@1309 13190
bsw/jbe@1309 13191 do {
bsw/jbe@1309 13192 if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) {
bsw/jbe@1309 13193 return node;
bsw/jbe@1309 13194 }
bsw/jbe@1309 13195 node = node.parentNode;
bsw/jbe@1309 13196 } while (node && node !== container);
bsw/jbe@1309 13197
bsw/jbe@1309 13198 return null;
bsw/jbe@1309 13199 }
bsw/jbe@1309 13200
bsw/jbe@1309 13201 function correctOptionsForSimilarityCheck(options) {
bsw/jbe@1309 13202 return {
bsw/jbe@1309 13203 nodeName: options.nodeName || null,
bsw/jbe@1309 13204 className: (!options.classRegExp) ? options.className || null : null,
bsw/jbe@1309 13205 classRegExp: options.classRegExp || null,
bsw/jbe@1309 13206 styleProperty: options.styleProperty || null
bsw/jbe@1309 13207 };
bsw/jbe@1309 13208 }
bsw/jbe@1309 13209
bsw/jbe@1309 13210 // Finds inline node with similar nodeName/style/className
bsw/jbe@1309 13211 // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes
bsw/jbe@1309 13212 function isSimilarNode(node, options) {
bsw/jbe@1309 13213 var o;
bsw/jbe@1309 13214 if (options.nodeName) {
bsw/jbe@1309 13215 var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase();
bsw/jbe@1309 13216 return wysihtml.dom.domNode(node).test({ query: query });
bsw/jbe@1309 13217 } else {
bsw/jbe@1309 13218 o = wysihtml.lang.object(options).clone();
bsw/jbe@1309 13219 o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted
bsw/jbe@1309 13220 return wysihtml.dom.domNode(node).test(o);
bsw/jbe@1309 13221 }
bsw/jbe@1309 13222 }
bsw/jbe@1309 13223
bsw/jbe@1309 13224 function selectRange(composer, range) {
bsw/jbe@1309 13225 var d = document.documentElement || document.body,
bsw/jbe@1309 13226 oldScrollTop = d.scrollTop,
bsw/jbe@1309 13227 oldScrollLeft = d.scrollLeft,
bsw/jbe@1309 13228 selection = rangy.getSelection(composer.win);
bsw/jbe@1309 13229
bsw/jbe@1309 13230 rangy.getSelection(composer.win).removeAllRanges();
bsw/jbe@1309 13231
bsw/jbe@1309 13232 // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again
bsw/jbe@1309 13233 try {
bsw/jbe@1309 13234 rangy.getSelection(composer.win).addRange(range);
bsw/jbe@1309 13235 } catch (e) {}
bsw/jbe@1309 13236 if (!composer.doc.activeElement || !wysihtml.dom.contains(composer.element, composer.doc.activeElement)) {
bsw/jbe@1309 13237 composer.element.focus();
bsw/jbe@1309 13238 d.scrollTop = oldScrollTop;
bsw/jbe@1309 13239 d.scrollLeft = oldScrollLeft;
bsw/jbe@1309 13240 rangy.getSelection(composer.win).addRange(range);
bsw/jbe@1309 13241 }
bsw/jbe@1309 13242 }
bsw/jbe@1309 13243
bsw/jbe@1309 13244 function selectTextNodes(textNodes, composer) {
bsw/jbe@1309 13245 var range = rangy.createRange(composer.doc),
bsw/jbe@1309 13246 lastText = textNodes[textNodes.length - 1];
bsw/jbe@1309 13247
bsw/jbe@1309 13248 if (textNodes[0] && lastText) {
bsw/jbe@1309 13249 range.setStart(textNodes[0], 0);
bsw/jbe@1309 13250 range.setEnd(lastText, lastText.length);
bsw/jbe@1309 13251 selectRange(composer, range);
bsw/jbe@1309 13252 }
bsw/jbe@1309 13253
bsw/jbe@1309 13254 }
bsw/jbe@1309 13255
bsw/jbe@1309 13256 function selectTextNode(composer, node, start, end) {
bsw/jbe@1309 13257 var range = rangy.createRange(composer.doc);
bsw/jbe@1309 13258 if (node) {
bsw/jbe@1309 13259 range.setStart(node, start);
bsw/jbe@1309 13260 range.setEnd(node, typeof end !== 'undefined' ? end : start);
bsw/jbe@1309 13261 selectRange(composer, range);
bsw/jbe@1309 13262 }
bsw/jbe@1309 13263 }
bsw/jbe@1309 13264
bsw/jbe@1309 13265 function getState(composer, options, exact) {
bsw/jbe@1309 13266 var searchNodes = getSelectedTextNodes(composer.selection),
bsw/jbe@1309 13267 nodes = [],
bsw/jbe@1309 13268 partial = false,
bsw/jbe@1309 13269 node, range, caretNode;
bsw/jbe@1309 13270
bsw/jbe@1309 13271 if (composer.selection.isInThisEditable()) {
bsw/jbe@1309 13272
bsw/jbe@1309 13273 if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
bsw/jbe@1309 13274 caretNode = composer.selection.getSelection().anchorNode;
bsw/jbe@1309 13275 if (!caretNode) {
bsw/jbe@1309 13276 // selection not in editor
bsw/jbe@1309 13277 return {
bsw/jbe@1309 13278 nodes: [],
bsw/jbe@1309 13279 partial: false
bsw/jbe@1309 13280 };
bsw/jbe@1309 13281 }
bsw/jbe@1309 13282 if (caretNode.nodeType === 3) {
bsw/jbe@1309 13283 searchNodes = [caretNode];
bsw/jbe@1309 13284 }
bsw/jbe@1309 13285 }
bsw/jbe@1309 13286
bsw/jbe@1309 13287 // Handle collapsed selection caret
bsw/jbe@1309 13288 if (!searchNodes.length) {
bsw/jbe@1309 13289 range = composer.selection.getOwnRanges()[0];
bsw/jbe@1309 13290 if (range) {
bsw/jbe@1309 13291 searchNodes = [range.endContainer];
bsw/jbe@1309 13292 }
bsw/jbe@1309 13293 }
bsw/jbe@1309 13294
bsw/jbe@1309 13295 for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
bsw/jbe@1309 13296 node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
bsw/jbe@1309 13297 if (node) {
bsw/jbe@1309 13298 nodes.push(node);
bsw/jbe@1309 13299 } else {
bsw/jbe@1309 13300 partial = true;
bsw/jbe@1309 13301 }
bsw/jbe@1309 13302 }
bsw/jbe@1309 13303
bsw/jbe@1309 13304 }
bsw/jbe@1309 13305
bsw/jbe@1309 13306 return {
bsw/jbe@1309 13307 nodes: nodes,
bsw/jbe@1309 13308 partial: partial
bsw/jbe@1309 13309 };
bsw/jbe@1309 13310 }
bsw/jbe@1309 13311
bsw/jbe@1309 13312 // Returns if caret is inside a word in textnode (not on boundary)
bsw/jbe@1309 13313 // If selection anchornode is not text node, returns false
bsw/jbe@1309 13314 function caretIsInsideWord(selection) {
bsw/jbe@1309 13315 var anchor, offset, beforeChar, afterChar;
bsw/jbe@1309 13316 if (selection) {
bsw/jbe@1309 13317 anchor = selection.anchorNode;
bsw/jbe@1309 13318 offset = selection.anchorOffset;
bsw/jbe@1309 13319 if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) {
bsw/jbe@1309 13320 beforeChar = anchor.data[offset - 1];
bsw/jbe@1309 13321 afterChar = anchor.data[offset];
bsw/jbe@1309 13322 return (/\w/).test(beforeChar) && (/\w/).test(afterChar);
bsw/jbe@1309 13323 }
bsw/jbe@1309 13324 }
bsw/jbe@1309 13325 return false;
bsw/jbe@1309 13326 }
bsw/jbe@1309 13327
bsw/jbe@1309 13328 // Returns a range and textnode containing object from caret position covering a whole word
bsw/jbe@1309 13329 // wordOffsety describes the original position of caret in the new textNode
bsw/jbe@1309 13330 // Caret has to be inside a textNode.
bsw/jbe@1309 13331 function getRangeForWord(selection) {
bsw/jbe@1309 13332 var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar,
bsw/jbe@1309 13333 txtNodes = [];
bsw/jbe@1309 13334 if (selection) {
bsw/jbe@1309 13335 anchor = selection.anchorNode;
bsw/jbe@1309 13336 offset = offsetStart = offsetEnd = selection.anchorOffset;
bsw/jbe@1309 13337 doc = anchor.ownerDocument;
bsw/jbe@1309 13338 range = rangy.createRange(doc);
bsw/jbe@1309 13339
bsw/jbe@1309 13340 if (anchor && anchor.nodeType === 3) {
bsw/jbe@1309 13341
bsw/jbe@1309 13342 while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) {
bsw/jbe@1309 13343 offsetStart--;
bsw/jbe@1309 13344 }
bsw/jbe@1309 13345
bsw/jbe@1309 13346 while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) {
bsw/jbe@1309 13347 offsetEnd++;
bsw/jbe@1309 13348 }
bsw/jbe@1309 13349
bsw/jbe@1309 13350 range.setStartAndEnd(anchor, offsetStart, offsetEnd);
bsw/jbe@1309 13351 range.splitBoundaries();
bsw/jbe@1309 13352 txtNodes = range.getNodes([3], function(node) {
bsw/jbe@1309 13353 return (!wysihtml.dom.domNode(node).is.emptyTextNode());
bsw/jbe@1309 13354 });
bsw/jbe@1309 13355
bsw/jbe@1309 13356 return {
bsw/jbe@1309 13357 wordOffset: offset - offsetStart,
bsw/jbe@1309 13358 range: range,
bsw/jbe@1309 13359 textNode: txtNodes[0]
bsw/jbe@1309 13360 };
bsw/jbe@1309 13361
bsw/jbe@1309 13362 }
bsw/jbe@1309 13363 }
bsw/jbe@1309 13364 return false;
bsw/jbe@1309 13365 }
bsw/jbe@1309 13366
bsw/jbe@1309 13367 // Contents of 2 elements are merged to fitst element. second element is removed as consequence
bsw/jbe@1309 13368 function mergeContents(element1, element2) {
bsw/jbe@1309 13369 while (element2.firstChild) {
bsw/jbe@1309 13370 element1.appendChild(element2.firstChild);
bsw/jbe@1309 13371 }
bsw/jbe@1309 13372 element2.parentNode.removeChild(element2);
bsw/jbe@1309 13373 }
bsw/jbe@1309 13374
bsw/jbe@1309 13375 function mergeConsequentSimilarElements(elements) {
bsw/jbe@1309 13376 for (var i = elements.length; i--;) {
bsw/jbe@1309 13377
bsw/jbe@1309 13378 if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup
bsw/jbe@1309 13379
bsw/jbe@1309 13380 if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) {
bsw/jbe@1309 13381 mergeContents(elements[i], elements[i].nextSibling);
bsw/jbe@1309 13382 }
bsw/jbe@1309 13383
bsw/jbe@1309 13384 if (elements[i].previousSibling && isSameNode(elements[i] , elements[i].previousSibling)) {
bsw/jbe@1309 13385 mergeContents(elements[i].previousSibling, elements[i]);
bsw/jbe@1309 13386 }
bsw/jbe@1309 13387
bsw/jbe@1309 13388 }
bsw/jbe@1309 13389 }
bsw/jbe@1309 13390 }
bsw/jbe@1309 13391
bsw/jbe@1309 13392 function cleanupAndSetSelection(composer, textNodes, options) {
bsw/jbe@1309 13393 if (textNodes.length > 0) {
bsw/jbe@1309 13394 selectTextNodes(textNodes, composer);
bsw/jbe@1309 13395 }
bsw/jbe@1309 13396 mergeConsequentSimilarElements(getState(composer, options).nodes);
bsw/jbe@1309 13397 if (textNodes.length > 0) {
bsw/jbe@1309 13398 selectTextNodes(textNodes, composer);
bsw/jbe@1309 13399 }
bsw/jbe@1309 13400 }
bsw/jbe@1309 13401
bsw/jbe@1309 13402 function cleanupAndSetCaret(composer, textNode, offset, options) {
bsw/jbe@1309 13403 selectTextNode(composer, textNode, offset);
bsw/jbe@1309 13404 mergeConsequentSimilarElements(getState(composer, options).nodes);
bsw/jbe@1309 13405 selectTextNode(composer, textNode, offset);
bsw/jbe@1309 13406 }
bsw/jbe@1309 13407
bsw/jbe@1309 13408 // Formats a textnode with given options
bsw/jbe@1309 13409 function formatTextNode(textNode, options) {
bsw/jbe@1309 13410 var wrapNode = createWrapNode(textNode, options);
bsw/jbe@1309 13411
bsw/jbe@1309 13412 textNode.parentNode.insertBefore(wrapNode, textNode);
bsw/jbe@1309 13413 wrapNode.appendChild(textNode);
bsw/jbe@1309 13414 }
bsw/jbe@1309 13415
bsw/jbe@1309 13416 // Changes/toggles format of a textnode
bsw/jbe@1309 13417 function unformatTextNode(textNode, composer, options) {
bsw/jbe@1309 13418 var container = composer.element,
bsw/jbe@1309 13419 wrapNode = findSimilarTextNodeWrapper(textNode, options, container),
bsw/jbe@1309 13420 newWrapNode;
bsw/jbe@1309 13421
bsw/jbe@1309 13422 if (wrapNode) {
bsw/jbe@1309 13423 newWrapNode = wrapNode.cloneNode(false);
bsw/jbe@1309 13424
bsw/jbe@1309 13425 wysihtml.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode);
bsw/jbe@1309 13426 updateFormatOfElement(newWrapNode, options);
bsw/jbe@1309 13427 }
bsw/jbe@1309 13428 }
bsw/jbe@1309 13429
bsw/jbe@1309 13430 // Removes the format around textnode
bsw/jbe@1309 13431 function removeFormatFromTextNode(textNode, composer, options) {
bsw/jbe@1309 13432 var container = composer.element,
bsw/jbe@1309 13433 wrapNode = findSimilarTextNodeWrapper(textNode, options, container);
bsw/jbe@1309 13434
bsw/jbe@1309 13435 if (wrapNode) {
bsw/jbe@1309 13436 wysihtml.dom.domNode(textNode).escapeParent(wrapNode);
bsw/jbe@1309 13437 }
bsw/jbe@1309 13438 }
bsw/jbe@1309 13439
bsw/jbe@1309 13440 // Creates node around caret formated with options
bsw/jbe@1309 13441 function formatTextRange(range, composer, options) {
bsw/jbe@1309 13442 var wrapNode = createWrapNode(range.endContainer, options);
bsw/jbe@1309 13443
bsw/jbe@1309 13444 range.surroundContents(wrapNode);
bsw/jbe@1309 13445 composer.selection.selectNode(wrapNode);
bsw/jbe@1309 13446 }
bsw/jbe@1309 13447
bsw/jbe@1309 13448 // Changes/toggles format of whole selection
bsw/jbe@1309 13449 function updateFormat(composer, textNodes, state, options) {
bsw/jbe@1309 13450 var exactState = getState(composer, options, true),
bsw/jbe@1309 13451 selection = composer.selection.getSelection(),
bsw/jbe@1309 13452 wordObj, textNode, newNode, i;
bsw/jbe@1309 13453
bsw/jbe@1309 13454 if (!textNodes.length) {
bsw/jbe@1309 13455 // Selection is caret
bsw/jbe@1309 13456
bsw/jbe@1309 13457
bsw/jbe@1309 13458 if (options.toggle !== false) {
bsw/jbe@1309 13459 if (caretIsInsideWord(selection)) {
bsw/jbe@1309 13460
bsw/jbe@1309 13461 // Unformat whole word
bsw/jbe@1309 13462 wordObj = getRangeForWord(selection);
bsw/jbe@1309 13463 textNode = wordObj.textNode;
bsw/jbe@1309 13464 unformatTextNode(wordObj.textNode, composer, options);
bsw/jbe@1309 13465 cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
bsw/jbe@1309 13466
bsw/jbe@1309 13467 } else {
bsw/jbe@1309 13468
bsw/jbe@1309 13469 // Escape caret out of format
bsw/jbe@1309 13470 textNode = composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309 13471 newNode = state.nodes[0].cloneNode(false);
bsw/jbe@1309 13472 newNode.appendChild(textNode);
bsw/jbe@1309 13473 composer.selection.splitElementAtCaret(state.nodes[0], newNode);
bsw/jbe@1309 13474 updateFormatOfElement(newNode, options);
bsw/jbe@1309 13475 cleanupAndSetSelection(composer, [textNode], options);
bsw/jbe@1309 13476 var s = composer.selection.getSelection();
bsw/jbe@1309 13477 if (s.anchorNode && s.focusNode) {
bsw/jbe@1309 13478 // Has an error in IE when collapsing selection. probably from rangy
bsw/jbe@1309 13479 try {
bsw/jbe@1309 13480 s.collapseToEnd();
bsw/jbe@1309 13481 } catch (e) {}
bsw/jbe@1309 13482 }
bsw/jbe@1309 13483 }
bsw/jbe@1309 13484 } else {
bsw/jbe@1309 13485 // In non-toggle mode the closest state element has to be found and the state updated differently
bsw/jbe@1309 13486 for (i = state.nodes.length; i--;) {
bsw/jbe@1309 13487 updateFormatOfElement(state.nodes[i], options);
bsw/jbe@1309 13488 }
bsw/jbe@1309 13489 }
bsw/jbe@1309 13490
bsw/jbe@1309 13491 } else {
bsw/jbe@1309 13492
bsw/jbe@1309 13493 if (!exactState.partial && options.toggle !== false) {
bsw/jbe@1309 13494
bsw/jbe@1309 13495 // If whole selection (all textnodes) are in the applied format
bsw/jbe@1309 13496 // remove the format from selection
bsw/jbe@1309 13497 // Non-toggle mode never removes. Remove has to be called explicitly
bsw/jbe@1309 13498 for (i = textNodes.length; i--;) {
bsw/jbe@1309 13499 unformatTextNode(textNodes[i], composer, options);
bsw/jbe@1309 13500 }
bsw/jbe@1309 13501
bsw/jbe@1309 13502 } else {
bsw/jbe@1309 13503
bsw/jbe@1309 13504 // Selection is partially in format
bsw/jbe@1309 13505 // change it to new if format if textnode allreafy in similar state
bsw/jbe@1309 13506 // else just apply
bsw/jbe@1309 13507
bsw/jbe@1309 13508 for (i = textNodes.length; i--;) {
bsw/jbe@1309 13509
bsw/jbe@1309 13510 if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
bsw/jbe@1309 13511 unformatTextNode(textNodes[i], composer, options);
bsw/jbe@1309 13512 }
bsw/jbe@1309 13513
bsw/jbe@1309 13514 if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
bsw/jbe@1309 13515 formatTextNode(textNodes[i], options);
bsw/jbe@1309 13516 }
bsw/jbe@1309 13517 }
bsw/jbe@1309 13518
bsw/jbe@1309 13519 }
bsw/jbe@1309 13520
bsw/jbe@1309 13521 cleanupAndSetSelection(composer, textNodes, options);
bsw/jbe@1309 13522 }
bsw/jbe@1309 13523 }
bsw/jbe@1309 13524
bsw/jbe@1309 13525 // Removes format from selection
bsw/jbe@1309 13526 function removeFormat(composer, textNodes, state, options) {
bsw/jbe@1309 13527 var textNode, textOffset, newNode, i,
bsw/jbe@1309 13528 selection = composer.selection.getSelection();
bsw/jbe@1309 13529
bsw/jbe@1309 13530 if (!textNodes.length) {
bsw/jbe@1309 13531 textNode = selection.anchorNode;
bsw/jbe@1309 13532 textOffset = selection.anchorOffset;
bsw/jbe@1309 13533
bsw/jbe@1309 13534 for (i = state.nodes.length; i--;) {
bsw/jbe@1309 13535 wysihtml.dom.unwrap(state.nodes[i]);
bsw/jbe@1309 13536 }
bsw/jbe@1309 13537
bsw/jbe@1309 13538 cleanupAndSetCaret(composer, textNode, textOffset, options);
bsw/jbe@1309 13539 } else {
bsw/jbe@1309 13540 for (i = textNodes.length; i--;) {
bsw/jbe@1309 13541 removeFormatFromTextNode(textNodes[i], composer, options);
bsw/jbe@1309 13542 }
bsw/jbe@1309 13543 cleanupAndSetSelection(composer, textNodes, options);
bsw/jbe@1309 13544 }
bsw/jbe@1309 13545 }
bsw/jbe@1309 13546
bsw/jbe@1309 13547 // Adds format to selection
bsw/jbe@1309 13548 function applyFormat(composer, textNodes, options) {
bsw/jbe@1309 13549 var wordObj, i,
bsw/jbe@1309 13550 selection = composer.selection.getSelection();
bsw/jbe@1309 13551
bsw/jbe@1309 13552 if (!textNodes.length) {
bsw/jbe@1309 13553 // Handle collapsed selection caret and return
bsw/jbe@1309 13554 if (caretIsInsideWord(selection)) {
bsw/jbe@1309 13555
bsw/jbe@1309 13556 wordObj = getRangeForWord(selection);
bsw/jbe@1309 13557 formatTextNode(wordObj.textNode, options);
bsw/jbe@1309 13558 cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
bsw/jbe@1309 13559
bsw/jbe@1309 13560 } else {
bsw/jbe@1309 13561 var r = composer.selection.getOwnRanges()[0];
bsw/jbe@1309 13562 if (r) {
bsw/jbe@1309 13563 formatTextRange(r, composer, options);
bsw/jbe@1309 13564 }
bsw/jbe@1309 13565 }
bsw/jbe@1309 13566
bsw/jbe@1309 13567 } else {
bsw/jbe@1309 13568 // Handle textnodes in selection and apply format
bsw/jbe@1309 13569 for (i = textNodes.length; i--;) {
bsw/jbe@1309 13570 formatTextNode(textNodes[i], options);
bsw/jbe@1309 13571 }
bsw/jbe@1309 13572 cleanupAndSetSelection(composer, textNodes, options);
bsw/jbe@1309 13573 }
bsw/jbe@1309 13574 }
bsw/jbe@1309 13575
bsw/jbe@1309 13576 // If properties is passed as a string, correct options with that nodeName
bsw/jbe@1309 13577 function fixOptions(options) {
bsw/jbe@1309 13578 options = (typeof options === "string") ? { nodeName: options } : options;
bsw/jbe@1309 13579 if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); }
bsw/jbe@1309 13580 return options;
bsw/jbe@1309 13581 }
bsw/jbe@1309 13582
bsw/jbe@1309 13583 wysihtml.commands.formatInline = {
bsw/jbe@1309 13584
bsw/jbe@1309 13585 // Basics:
bsw/jbe@1309 13586 // In case of plain text or inline state not set wrap all non-empty textnodes with
bsw/jbe@1309 13587 // In case a similar inline wrapper node is detected on one of textnodes, the wrapper node is changed (if fully contained) or split and changed (partially contained)
bsw/jbe@1309 13588 // In case of changing mode every textnode is addressed separatly
bsw/jbe@1309 13589 exec: function(composer, command, options) {
bsw/jbe@1309 13590 options = fixOptions(options);
bsw/jbe@1309 13591
bsw/jbe@1309 13592 // Join adjactent textnodes first
bsw/jbe@1309 13593 composer.element.normalize();
bsw/jbe@1309 13594
bsw/jbe@1309 13595 var textNodes = getSelectedTextNodes(composer.selection, true),
bsw/jbe@1309 13596 state = getState(composer, options);
bsw/jbe@1309 13597 if (state.nodes.length > 0) {
bsw/jbe@1309 13598 // Text allready has the format applied
bsw/jbe@1309 13599 updateFormat(composer, textNodes, state, options);
bsw/jbe@1309 13600 } else {
bsw/jbe@1309 13601 // Selection is not in the applied format
bsw/jbe@1309 13602 applyFormat(composer, textNodes, options);
bsw/jbe@1309 13603 }
bsw/jbe@1309 13604 composer.element.normalize();
bsw/jbe@1309 13605 },
bsw/jbe@1309 13606
bsw/jbe@1309 13607 remove: function(composer, command, options) {
bsw/jbe@1309 13608 options = fixOptions(options);
bsw/jbe@1309 13609 composer.element.normalize();
bsw/jbe@1309 13610
bsw/jbe@1309 13611 var textNodes = getSelectedTextNodes(composer.selection, true),
bsw/jbe@1309 13612 state = getState(composer, options);
bsw/jbe@1309 13613
bsw/jbe@1309 13614 if (state.nodes.length > 0) {
bsw/jbe@1309 13615 // Text allready has the format applied
bsw/jbe@1309 13616 removeFormat(composer, textNodes, state, options);
bsw/jbe@1309 13617 }
bsw/jbe@1309 13618
bsw/jbe@1309 13619 composer.element.normalize();
bsw/jbe@1309 13620 },
bsw/jbe@1309 13621
bsw/jbe@1309 13622 state: function(composer, command, options) {
bsw/jbe@1309 13623 options = fixOptions(options);
bsw/jbe@1309 13624 var nodes = getState(composer, options, true).nodes;
bsw/jbe@1309 13625 return (nodes.length === 0) ? false : nodes;
bsw/jbe@1309 13626 }
bsw/jbe@1309 13627 };
bsw/jbe@1309 13628
bsw/jbe@1309 13629 })(wysihtml);
bsw/jbe@1309 13630
bsw/jbe@1309 13631 (function(wysihtml){
bsw/jbe@1309 13632 wysihtml.commands.indentList = {
bsw/jbe@1309 13633 exec: function(composer, command, value) {
bsw/jbe@1309 13634 var listEls = composer.selection.getSelectionParentsByTag('LI');
bsw/jbe@1309 13635 if (listEls) {
bsw/jbe@1309 13636 return this.tryToPushLiLevel(listEls, composer.selection);
bsw/jbe@1309 13637 }
bsw/jbe@1309 13638 return false;
bsw/jbe@1309 13639 },
bsw/jbe@1309 13640
bsw/jbe@1309 13641 state: function(composer, command) {
bsw/jbe@1309 13642 return false;
bsw/jbe@1309 13643 },
bsw/jbe@1309 13644
bsw/jbe@1309 13645 tryToPushLiLevel: function(liNodes, selection) {
bsw/jbe@1309 13646 var listTag, list, prevLi, liNode, prevLiList,
bsw/jbe@1309 13647 found = false;
bsw/jbe@1309 13648
bsw/jbe@1309 13649 selection.executeAndRestoreRangy(function() {
bsw/jbe@1309 13650
bsw/jbe@1309 13651 for (var i = liNodes.length; i--;) {
bsw/jbe@1309 13652 liNode = liNodes[i];
bsw/jbe@1309 13653 listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL';
bsw/jbe@1309 13654 list = liNode.ownerDocument.createElement(listTag);
bsw/jbe@1309 13655 prevLi = wysihtml.dom.domNode(liNode).prev({nodeTypes: [wysihtml.ELEMENT_NODE]});
bsw/jbe@1309 13656 prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null;
bsw/jbe@1309 13657
bsw/jbe@1309 13658 if (prevLi) {
bsw/jbe@1309 13659 if (prevLiList) {
bsw/jbe@1309 13660 prevLiList.appendChild(liNode);
bsw/jbe@1309 13661 } else {
bsw/jbe@1309 13662 list.appendChild(liNode);
bsw/jbe@1309 13663 prevLi.appendChild(list);
bsw/jbe@1309 13664 }
bsw/jbe@1309 13665 found = true;
bsw/jbe@1309 13666 }
bsw/jbe@1309 13667 }
bsw/jbe@1309 13668
bsw/jbe@1309 13669 });
bsw/jbe@1309 13670 return found;
bsw/jbe@1309 13671 }
bsw/jbe@1309 13672 };
bsw/jbe@1309 13673 }(wysihtml));
bsw/jbe@1309 13674
bsw/jbe@1309 13675 (function(wysihtml){
bsw/jbe@1309 13676 wysihtml.commands.insertHTML = {
bsw/jbe@1309 13677 exec: function(composer, command, html) {
bsw/jbe@1309 13678 composer.selection.insertHTML(html);
bsw/jbe@1309 13679 },
bsw/jbe@1309 13680
bsw/jbe@1309 13681 state: function() {
bsw/jbe@1309 13682 return false;
bsw/jbe@1309 13683 }
bsw/jbe@1309 13684 };
bsw/jbe@1309 13685 }(wysihtml));
bsw/jbe@1309 13686
bsw/jbe@1309 13687 (function(wysihtml) {
bsw/jbe@1309 13688 var LINE_BREAK = "<br>" + (wysihtml.browser.needsSpaceAfterLineBreak() ? " " : "");
bsw/jbe@1309 13689
bsw/jbe@1309 13690 wysihtml.commands.insertLineBreak = {
bsw/jbe@1309 13691 exec: function(composer, command) {
bsw/jbe@1309 13692 composer.selection.insertHTML(LINE_BREAK);
bsw/jbe@1309 13693 },
bsw/jbe@1309 13694
bsw/jbe@1309 13695 state: function() {
bsw/jbe@1309 13696 return false;
bsw/jbe@1309 13697 }
bsw/jbe@1309 13698 };
bsw/jbe@1309 13699 })(wysihtml);
bsw/jbe@1309 13700
bsw/jbe@1309 13701 wysihtml.commands.insertList = (function(wysihtml) {
bsw/jbe@1309 13702
bsw/jbe@1309 13703 var isNode = function(node, name) {
bsw/jbe@1309 13704 if (node && node.nodeName) {
bsw/jbe@1309 13705 if (typeof name === 'string') {
bsw/jbe@1309 13706 name = [name];
bsw/jbe@1309 13707 }
bsw/jbe@1309 13708 for (var n = name.length; n--;) {
bsw/jbe@1309 13709 if (node.nodeName === name[n]) {
bsw/jbe@1309 13710 return true;
bsw/jbe@1309 13711 }
bsw/jbe@1309 13712 }
bsw/jbe@1309 13713 }
bsw/jbe@1309 13714 return false;
bsw/jbe@1309 13715 };
bsw/jbe@1309 13716
bsw/jbe@1309 13717 var findListEl = function(node, nodeName, composer) {
bsw/jbe@1309 13718 var ret = {
bsw/jbe@1309 13719 el: null,
bsw/jbe@1309 13720 other: false
bsw/jbe@1309 13721 };
bsw/jbe@1309 13722
bsw/jbe@1309 13723 if (node) {
bsw/jbe@1309 13724 var parentLi = wysihtml.dom.getParentElement(node, { query: "li" }, false, composer.element),
bsw/jbe@1309 13725 otherNodeName = (nodeName === "UL") ? "OL" : "UL";
bsw/jbe@1309 13726
bsw/jbe@1309 13727 if (isNode(node, nodeName)) {
bsw/jbe@1309 13728 ret.el = node;
bsw/jbe@1309 13729 } else if (isNode(node, otherNodeName)) {
bsw/jbe@1309 13730 ret = {
bsw/jbe@1309 13731 el: node,
bsw/jbe@1309 13732 other: true
bsw/jbe@1309 13733 };
bsw/jbe@1309 13734 } else if (parentLi) {
bsw/jbe@1309 13735 if (isNode(parentLi.parentNode, nodeName)) {
bsw/jbe@1309 13736 ret.el = parentLi.parentNode;
bsw/jbe@1309 13737 } else if (isNode(parentLi.parentNode, otherNodeName)) {
bsw/jbe@1309 13738 ret = {
bsw/jbe@1309 13739 el : parentLi.parentNode,
bsw/jbe@1309 13740 other: true
bsw/jbe@1309 13741 };
bsw/jbe@1309 13742 }
bsw/jbe@1309 13743 }
bsw/jbe@1309 13744 }
bsw/jbe@1309 13745
bsw/jbe@1309 13746 // do not count list elements outside of composer
bsw/jbe@1309 13747 if (ret.el && !composer.element.contains(ret.el)) {
bsw/jbe@1309 13748 ret.el = null;
bsw/jbe@1309 13749 }
bsw/jbe@1309 13750
bsw/jbe@1309 13751 return ret;
bsw/jbe@1309 13752 };
bsw/jbe@1309 13753
bsw/jbe@1309 13754 var handleSameTypeList = function(el, nodeName, composer) {
bsw/jbe@1309 13755 var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
bsw/jbe@1309 13756 otherLists, innerLists;
bsw/jbe@1309 13757 // Unwrap list
bsw/jbe@1309 13758 // <ul><li>foo</li><li>bar</li></ul>
bsw/jbe@1309 13759 // becomes:
bsw/jbe@1309 13760 // foo<br>bar<br>
bsw/jbe@1309 13761
bsw/jbe@1309 13762 composer.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309 13763 otherLists = getListsInSelection(otherNodeName, composer);
bsw/jbe@1309 13764 if (otherLists.length) {
bsw/jbe@1309 13765 for (var l = otherLists.length; l--;) {
bsw/jbe@1309 13766 wysihtml.dom.renameElement(otherLists[l], nodeName.toLowerCase());
bsw/jbe@1309 13767 }
bsw/jbe@1309 13768 } else {
bsw/jbe@1309 13769 innerLists = getListsInSelection(['OL', 'UL'], composer);
bsw/jbe@1309 13770 for (var i = innerLists.length; i--;) {
bsw/jbe@1309 13771 wysihtml.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
bsw/jbe@1309 13772 }
bsw/jbe@1309 13773 if (innerLists.length === 0) {
bsw/jbe@1309 13774 wysihtml.dom.resolveList(el, composer.config.useLineBreaks);
bsw/jbe@1309 13775 }
bsw/jbe@1309 13776 }
bsw/jbe@1309 13777 });
bsw/jbe@1309 13778 };
bsw/jbe@1309 13779
bsw/jbe@1309 13780 var handleOtherTypeList = function(el, nodeName, composer) {
bsw/jbe@1309 13781 var otherNodeName = (nodeName === "UL") ? "OL" : "UL";
bsw/jbe@1309 13782 // Turn an ordered list into an unordered list
bsw/jbe@1309 13783 // <ol><li>foo</li><li>bar</li></ol>
bsw/jbe@1309 13784 // becomes:
bsw/jbe@1309 13785 // <ul><li>foo</li><li>bar</li></ul>
bsw/jbe@1309 13786 // Also rename other lists in selection
bsw/jbe@1309 13787 composer.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309 13788 var renameLists = [el].concat(getListsInSelection(otherNodeName, composer));
bsw/jbe@1309 13789
bsw/jbe@1309 13790 // All selection inner lists get renamed too
bsw/jbe@1309 13791 for (var l = renameLists.length; l--;) {
bsw/jbe@1309 13792 wysihtml.dom.renameElement(renameLists[l], nodeName.toLowerCase());
bsw/jbe@1309 13793 }
bsw/jbe@1309 13794 });
bsw/jbe@1309 13795 };
bsw/jbe@1309 13796
bsw/jbe@1309 13797 var getListsInSelection = function(nodeName, composer) {
bsw/jbe@1309 13798 var ranges = composer.selection.getOwnRanges(),
bsw/jbe@1309 13799 renameLists = [];
bsw/jbe@1309 13800
bsw/jbe@1309 13801 for (var r = ranges.length; r--;) {
bsw/jbe@1309 13802 renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
bsw/jbe@1309 13803 return isNode(node, nodeName);
bsw/jbe@1309 13804 }));
bsw/jbe@1309 13805 }
bsw/jbe@1309 13806
bsw/jbe@1309 13807 return renameLists;
bsw/jbe@1309 13808 };
bsw/jbe@1309 13809
bsw/jbe@1309 13810 var createListFallback = function(nodeName, composer) {
bsw/jbe@1309 13811 var sel = rangy.saveSelection(composer.win);
bsw/jbe@1309 13812
bsw/jbe@1309 13813 // Fallback for Create list
bsw/jbe@1309 13814 var tempClassName = "_wysihtml-temp-" + new Date().getTime(),
bsw/jbe@1309 13815 isEmpty, list;
bsw/jbe@1309 13816
bsw/jbe@1309 13817 composer.commands.exec("formatBlock", {
bsw/jbe@1309 13818 "nodeName": "div",
bsw/jbe@1309 13819 "className": tempClassName
bsw/jbe@1309 13820 });
bsw/jbe@1309 13821
bsw/jbe@1309 13822 var tempElement = composer.element.querySelector("." + tempClassName);
bsw/jbe@1309 13823
bsw/jbe@1309 13824 // This space causes new lists to never break on enter
bsw/jbe@1309 13825 var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
bsw/jbe@1309 13826 tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
bsw/jbe@1309 13827 if (tempElement) {
bsw/jbe@1309 13828 isEmpty = (/^(\s|(<br>))+$/i).test(tempElement.innerHTML);
bsw/jbe@1309 13829 list = wysihtml.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer);
bsw/jbe@1309 13830 if (sel) {
bsw/jbe@1309 13831 rangy.restoreSelection(sel);
bsw/jbe@1309 13832 }
bsw/jbe@1309 13833 if (isEmpty) {
bsw/jbe@1309 13834 composer.selection.selectNode(list.querySelector("li"), true);
bsw/jbe@1309 13835 }
bsw/jbe@1309 13836 }
bsw/jbe@1309 13837 };
bsw/jbe@1309 13838
bsw/jbe@1309 13839 return {
bsw/jbe@1309 13840 exec: function(composer, command, nodeName) {
bsw/jbe@1309 13841 var doc = composer.doc,
bsw/jbe@1309 13842 cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList",
bsw/jbe@1309 13843 s = composer.selection.getSelection(),
bsw/jbe@1309 13844 anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
bsw/jbe@1309 13845 fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode,
bsw/jbe@1309 13846 selectedNode, list;
bsw/jbe@1309 13847
bsw/jbe@1309 13848 if (s.isBackwards()) {
bsw/jbe@1309 13849 // swap variables
bsw/jbe@1309 13850 anode = [fnode, fnode = anode][0];
bsw/jbe@1309 13851 }
bsw/jbe@1309 13852
bsw/jbe@1309 13853 if (wysihtml.dom.domNode(fnode).is.emptyTextNode(true) && fnode) {
bsw/jbe@1309 13854 fnode = wysihtml.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 13855 }
bsw/jbe@1309 13856 if (wysihtml.dom.domNode(anode).is.emptyTextNode(true) && anode) {
bsw/jbe@1309 13857 anode = wysihtml.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 13858 }
bsw/jbe@1309 13859
bsw/jbe@1309 13860 if (anode && fnode) {
bsw/jbe@1309 13861 if (anode === fnode) {
bsw/jbe@1309 13862 selectedNode = anode;
bsw/jbe@1309 13863 } else {
bsw/jbe@1309 13864 selectedNode = wysihtml.dom.domNode(anode).commonAncestor(fnode, composer.element);
bsw/jbe@1309 13865 }
bsw/jbe@1309 13866 } else {
bsw/jbe@1309 13867 selectedNode = composer.selection.getSelectedNode();
bsw/jbe@1309 13868 }
bsw/jbe@1309 13869
bsw/jbe@1309 13870 list = findListEl(selectedNode, nodeName, composer);
bsw/jbe@1309 13871
bsw/jbe@1309 13872 if (!list.el) {
bsw/jbe@1309 13873 if (composer.commands.support(cmd)) {
bsw/jbe@1309 13874 doc.execCommand(cmd, false, null);
bsw/jbe@1309 13875 } else {
bsw/jbe@1309 13876 createListFallback(nodeName, composer);
bsw/jbe@1309 13877 }
bsw/jbe@1309 13878 } else if (list.other) {
bsw/jbe@1309 13879 handleOtherTypeList(list.el, nodeName, composer);
bsw/jbe@1309 13880 } else {
bsw/jbe@1309 13881 handleSameTypeList(list.el, nodeName, composer);
bsw/jbe@1309 13882 }
bsw/jbe@1309 13883 },
bsw/jbe@1309 13884
bsw/jbe@1309 13885 state: function(composer, command, nodeName) {
bsw/jbe@1309 13886 var selectedNode = composer.selection.getSelectedNode(),
bsw/jbe@1309 13887 list = findListEl(selectedNode, nodeName, composer);
bsw/jbe@1309 13888
bsw/jbe@1309 13889 return (list.el && !list.other) ? list.el : false;
bsw/jbe@1309 13890 }
bsw/jbe@1309 13891 };
bsw/jbe@1309 13892
bsw/jbe@1309 13893 })(wysihtml);
bsw/jbe@1309 13894
bsw/jbe@1309 13895 (function(wysihtml){
bsw/jbe@1309 13896
bsw/jbe@1309 13897 wysihtml.commands.outdentList = {
bsw/jbe@1309 13898 exec: function(composer, command, value) {
bsw/jbe@1309 13899 var listEls = composer.selection.getSelectionParentsByTag('LI');
bsw/jbe@1309 13900 if (listEls) {
bsw/jbe@1309 13901 return this.tryToPullLiLevel(listEls, composer);
bsw/jbe@1309 13902 }
bsw/jbe@1309 13903 return false;
bsw/jbe@1309 13904 },
bsw/jbe@1309 13905
bsw/jbe@1309 13906 state: function(composer, command) {
bsw/jbe@1309 13907 return false;
bsw/jbe@1309 13908 },
bsw/jbe@1309 13909
bsw/jbe@1309 13910 tryToPullLiLevel: function(liNodes, composer) {
bsw/jbe@1309 13911 var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
bsw/jbe@1309 13912 found = false,
bsw/jbe@1309 13913 that = this;
bsw/jbe@1309 13914
bsw/jbe@1309 13915 composer.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309 13916
bsw/jbe@1309 13917 for (var i = liNodes.length; i--;) {
bsw/jbe@1309 13918 liNode = liNodes[i];
bsw/jbe@1309 13919 if (liNode.parentNode) {
bsw/jbe@1309 13920 listNode = liNode.parentNode;
bsw/jbe@1309 13921
bsw/jbe@1309 13922 if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
bsw/jbe@1309 13923 found = true;
bsw/jbe@1309 13924
bsw/jbe@1309 13925 outerListNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element);
bsw/jbe@1309 13926 outerLiNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element);
bsw/jbe@1309 13927
bsw/jbe@1309 13928 if (outerListNode && outerLiNode) {
bsw/jbe@1309 13929
bsw/jbe@1309 13930 if (liNode.nextSibling) {
bsw/jbe@1309 13931 afterList = that.getAfterList(listNode, liNode);
bsw/jbe@1309 13932 liNode.appendChild(afterList);
bsw/jbe@1309 13933 }
bsw/jbe@1309 13934 outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
bsw/jbe@1309 13935
bsw/jbe@1309 13936 } else {
bsw/jbe@1309 13937
bsw/jbe@1309 13938 if (liNode.nextSibling) {
bsw/jbe@1309 13939 afterList = that.getAfterList(listNode, liNode);
bsw/jbe@1309 13940 liNode.appendChild(afterList);
bsw/jbe@1309 13941 }
bsw/jbe@1309 13942
bsw/jbe@1309 13943 for (var j = liNode.childNodes.length; j--;) {
bsw/jbe@1309 13944 listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
bsw/jbe@1309 13945 }
bsw/jbe@1309 13946
bsw/jbe@1309 13947 listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
bsw/jbe@1309 13948 liNode.parentNode.removeChild(liNode);
bsw/jbe@1309 13949
bsw/jbe@1309 13950 }
bsw/jbe@1309 13951
bsw/jbe@1309 13952 // cleanup
bsw/jbe@1309 13953 if (listNode.childNodes.length === 0) {
bsw/jbe@1309 13954 listNode.parentNode.removeChild(listNode);
bsw/jbe@1309 13955 }
bsw/jbe@1309 13956 }
bsw/jbe@1309 13957 }
bsw/jbe@1309 13958 }
bsw/jbe@1309 13959
bsw/jbe@1309 13960 });
bsw/jbe@1309 13961 return found;
bsw/jbe@1309 13962 },
bsw/jbe@1309 13963
bsw/jbe@1309 13964 getAfterList: function(listNode, liNode) {
bsw/jbe@1309 13965 var nodeName = listNode.nodeName,
bsw/jbe@1309 13966 newList = document.createElement(nodeName);
bsw/jbe@1309 13967
bsw/jbe@1309 13968 while (liNode.nextSibling) {
bsw/jbe@1309 13969 newList.appendChild(liNode.nextSibling);
bsw/jbe@1309 13970 }
bsw/jbe@1309 13971 return newList;
bsw/jbe@1309 13972 }
bsw/jbe@1309 13973
bsw/jbe@1309 13974 };
bsw/jbe@1309 13975 }(wysihtml));
bsw/jbe@1309 13976
bsw/jbe@1309 13977 (function(wysihtml){
bsw/jbe@1309 13978 wysihtml.commands.redo = {
bsw/jbe@1309 13979 exec: function(composer) {
bsw/jbe@1309 13980 return composer.undoManager.redo();
bsw/jbe@1309 13981 },
bsw/jbe@1309 13982
bsw/jbe@1309 13983 state: function(composer) {
bsw/jbe@1309 13984 return false;
bsw/jbe@1309 13985 }
bsw/jbe@1309 13986 };
bsw/jbe@1309 13987 }(wysihtml));
bsw/jbe@1309 13988
bsw/jbe@1309 13989 (function(wysihtml) {
bsw/jbe@1309 13990
bsw/jbe@1309 13991 var nodeOptions = {
bsw/jbe@1309 13992 nodeName: "A"
bsw/jbe@1309 13993 };
bsw/jbe@1309 13994
bsw/jbe@1309 13995 wysihtml.commands.removeLink = {
bsw/jbe@1309 13996 exec: function(composer, command) {
bsw/jbe@1309 13997 wysihtml.commands.formatInline.remove(composer, command, nodeOptions);
bsw/jbe@1309 13998 },
bsw/jbe@1309 13999
bsw/jbe@1309 14000 state: function(composer, command) {
bsw/jbe@1309 14001 return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
bsw/jbe@1309 14002 }
bsw/jbe@1309 14003 };
bsw/jbe@1309 14004
bsw/jbe@1309 14005 })(wysihtml);
bsw/jbe@1309 14006
bsw/jbe@1309 14007 (function(wysihtml){
bsw/jbe@1309 14008 wysihtml.commands.undo = {
bsw/jbe@1309 14009 exec: function(composer) {
bsw/jbe@1309 14010 return composer.undoManager.undo();
bsw/jbe@1309 14011 },
bsw/jbe@1309 14012
bsw/jbe@1309 14013 state: function(composer) {
bsw/jbe@1309 14014 return false;
bsw/jbe@1309 14015 }
bsw/jbe@1309 14016 };
bsw/jbe@1309 14017 }(wysihtml));
bsw/jbe@1309 14018
bsw/jbe@1309 14019 /**
bsw/jbe@1309 14020 * Undo Manager for wysihtml
bsw/jbe@1309 14021 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
bsw/jbe@1309 14022 */
bsw/jbe@1309 14023 (function(wysihtml) {
bsw/jbe@1309 14024 var Z_KEY = 90,
bsw/jbe@1309 14025 Y_KEY = 89,
bsw/jbe@1309 14026 BACKSPACE_KEY = 8,
bsw/jbe@1309 14027 DELETE_KEY = 46,
bsw/jbe@1309 14028 MAX_HISTORY_ENTRIES = 25,
bsw/jbe@1309 14029 DATA_ATTR_NODE = "data-wysihtml-selection-node",
bsw/jbe@1309 14030 DATA_ATTR_OFFSET = "data-wysihtml-selection-offset",
bsw/jbe@1309 14031 UNDO_HTML = '<span id="_wysihtml-undo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>',
bsw/jbe@1309 14032 REDO_HTML = '<span id="_wysihtml-redo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>',
bsw/jbe@1309 14033 dom = wysihtml.dom;
bsw/jbe@1309 14034
bsw/jbe@1309 14035 function cleanTempElements(doc) {
bsw/jbe@1309 14036 var tempElement;
bsw/jbe@1309 14037 while (tempElement = doc.querySelector("._wysihtml-temp")) {
bsw/jbe@1309 14038 tempElement.parentNode.removeChild(tempElement);
bsw/jbe@1309 14039 }
bsw/jbe@1309 14040 }
bsw/jbe@1309 14041
bsw/jbe@1309 14042 wysihtml.UndoManager = wysihtml.lang.Dispatcher.extend(
bsw/jbe@1309 14043 /** @scope wysihtml.UndoManager.prototype */ {
bsw/jbe@1309 14044 constructor: function(editor) {
bsw/jbe@1309 14045 this.editor = editor;
bsw/jbe@1309 14046 this.composer = editor.composer;
bsw/jbe@1309 14047 this.element = this.composer.element;
bsw/jbe@1309 14048
bsw/jbe@1309 14049 this.position = 0;
bsw/jbe@1309 14050 this.historyStr = [];
bsw/jbe@1309 14051 this.historyDom = [];
bsw/jbe@1309 14052
bsw/jbe@1309 14053 this.transact();
bsw/jbe@1309 14054
bsw/jbe@1309 14055 this._observe();
bsw/jbe@1309 14056 },
bsw/jbe@1309 14057
bsw/jbe@1309 14058 _observe: function() {
bsw/jbe@1309 14059 var that = this,
bsw/jbe@1309 14060 doc = this.composer.sandbox.getDocument(),
bsw/jbe@1309 14061 lastKey;
bsw/jbe@1309 14062
bsw/jbe@1309 14063 // Catch CTRL+Z and CTRL+Y
bsw/jbe@1309 14064 dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309 14065 if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
bsw/jbe@1309 14066 return;
bsw/jbe@1309 14067 }
bsw/jbe@1309 14068
bsw/jbe@1309 14069 var keyCode = event.keyCode,
bsw/jbe@1309 14070 isUndo = keyCode === Z_KEY && !event.shiftKey,
bsw/jbe@1309 14071 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
bsw/jbe@1309 14072
bsw/jbe@1309 14073 if (isUndo) {
bsw/jbe@1309 14074 that.undo();
bsw/jbe@1309 14075 event.preventDefault();
bsw/jbe@1309 14076 } else if (isRedo) {
bsw/jbe@1309 14077 that.redo();
bsw/jbe@1309 14078 event.preventDefault();
bsw/jbe@1309 14079 }
bsw/jbe@1309 14080 });
bsw/jbe@1309 14081
bsw/jbe@1309 14082 // Catch delete and backspace
bsw/jbe@1309 14083 dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309 14084 var keyCode = event.keyCode;
bsw/jbe@1309 14085 if (keyCode === lastKey) {
bsw/jbe@1309 14086 return;
bsw/jbe@1309 14087 }
bsw/jbe@1309 14088
bsw/jbe@1309 14089 lastKey = keyCode;
bsw/jbe@1309 14090
bsw/jbe@1309 14091 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
bsw/jbe@1309 14092 that.transact();
bsw/jbe@1309 14093 }
bsw/jbe@1309 14094 });
bsw/jbe@1309 14095
bsw/jbe@1309 14096 this.editor
bsw/jbe@1309 14097 .on("newword:composer", function() {
bsw/jbe@1309 14098 that.transact();
bsw/jbe@1309 14099 })
bsw/jbe@1309 14100
bsw/jbe@1309 14101 .on("beforecommand:composer", function() {
bsw/jbe@1309 14102 that.transact();
bsw/jbe@1309 14103 });
bsw/jbe@1309 14104 },
bsw/jbe@1309 14105
bsw/jbe@1309 14106 transact: function() {
bsw/jbe@1309 14107 var previousHtml = this.historyStr[this.position - 1],
bsw/jbe@1309 14108 currentHtml = this.composer.getValue(false, false),
bsw/jbe@1309 14109 composerIsVisible = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
bsw/jbe@1309 14110 range, node, offset, element, position;
bsw/jbe@1309 14111
bsw/jbe@1309 14112 if (currentHtml === previousHtml) {
bsw/jbe@1309 14113 return;
bsw/jbe@1309 14114 }
bsw/jbe@1309 14115
bsw/jbe@1309 14116 var length = this.historyStr.length = this.historyDom.length = this.position;
bsw/jbe@1309 14117 if (length > MAX_HISTORY_ENTRIES) {
bsw/jbe@1309 14118 this.historyStr.shift();
bsw/jbe@1309 14119 this.historyDom.shift();
bsw/jbe@1309 14120 this.position--;
bsw/jbe@1309 14121 }
bsw/jbe@1309 14122
bsw/jbe@1309 14123 this.position++;
bsw/jbe@1309 14124
bsw/jbe@1309 14125 if (composerIsVisible) {
bsw/jbe@1309 14126 // Do not start saving selection if composer is not visible
bsw/jbe@1309 14127 range = this.composer.selection.getRange();
bsw/jbe@1309 14128 node = (range && range.startContainer) ? range.startContainer : this.element;
bsw/jbe@1309 14129 offset = (range && range.startOffset) ? range.startOffset : 0;
bsw/jbe@1309 14130
bsw/jbe@1309 14131 if (node.nodeType === wysihtml.ELEMENT_NODE) {
bsw/jbe@1309 14132 element = node;
bsw/jbe@1309 14133 } else {
bsw/jbe@1309 14134 element = node.parentNode;
bsw/jbe@1309 14135 position = this.getChildNodeIndex(element, node);
bsw/jbe@1309 14136 }
bsw/jbe@1309 14137
bsw/jbe@1309 14138 element.setAttribute(DATA_ATTR_OFFSET, offset);
bsw/jbe@1309 14139 if (typeof(position) !== "undefined") {
bsw/jbe@1309 14140 element.setAttribute(DATA_ATTR_NODE, position);
bsw/jbe@1309 14141 }
bsw/jbe@1309 14142 }
bsw/jbe@1309 14143
bsw/jbe@1309 14144 var clone = this.element.cloneNode(!!currentHtml);
bsw/jbe@1309 14145 this.historyDom.push(clone);
bsw/jbe@1309 14146 this.historyStr.push(currentHtml);
bsw/jbe@1309 14147
bsw/jbe@1309 14148 if (element) {
bsw/jbe@1309 14149 element.removeAttribute(DATA_ATTR_OFFSET);
bsw/jbe@1309 14150 element.removeAttribute(DATA_ATTR_NODE);
bsw/jbe@1309 14151 }
bsw/jbe@1309 14152
bsw/jbe@1309 14153 },
bsw/jbe@1309 14154
bsw/jbe@1309 14155 undo: function() {
bsw/jbe@1309 14156 this.transact();
bsw/jbe@1309 14157
bsw/jbe@1309 14158 if (!this.undoPossible()) {
bsw/jbe@1309 14159 return;
bsw/jbe@1309 14160 }
bsw/jbe@1309 14161
bsw/jbe@1309 14162 this.set(this.historyDom[--this.position - 1]);
bsw/jbe@1309 14163 this.editor.fire("undo:composer");
bsw/jbe@1309 14164 },
bsw/jbe@1309 14165
bsw/jbe@1309 14166 redo: function() {
bsw/jbe@1309 14167 if (!this.redoPossible()) {
bsw/jbe@1309 14168 return;
bsw/jbe@1309 14169 }
bsw/jbe@1309 14170
bsw/jbe@1309 14171 this.set(this.historyDom[++this.position - 1]);
bsw/jbe@1309 14172 this.editor.fire("redo:composer");
bsw/jbe@1309 14173 },
bsw/jbe@1309 14174
bsw/jbe@1309 14175 undoPossible: function() {
bsw/jbe@1309 14176 return this.position > 1;
bsw/jbe@1309 14177 },
bsw/jbe@1309 14178
bsw/jbe@1309 14179 redoPossible: function() {
bsw/jbe@1309 14180 return this.position < this.historyStr.length;
bsw/jbe@1309 14181 },
bsw/jbe@1309 14182
bsw/jbe@1309 14183 set: function(historyEntry) {
bsw/jbe@1309 14184 this.element.innerHTML = "";
bsw/jbe@1309 14185
bsw/jbe@1309 14186 var i = 0,
bsw/jbe@1309 14187 childNodes = historyEntry.childNodes,
bsw/jbe@1309 14188 length = historyEntry.childNodes.length;
bsw/jbe@1309 14189
bsw/jbe@1309 14190 for (; i<length; i++) {
bsw/jbe@1309 14191 this.element.appendChild(childNodes[i].cloneNode(true));
bsw/jbe@1309 14192 }
bsw/jbe@1309 14193
bsw/jbe@1309 14194 // Restore selection
bsw/jbe@1309 14195 var offset,
bsw/jbe@1309 14196 node,
bsw/jbe@1309 14197 position;
bsw/jbe@1309 14198
bsw/jbe@1309 14199 if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
bsw/jbe@1309 14200 offset = historyEntry.getAttribute(DATA_ATTR_OFFSET);
bsw/jbe@1309 14201 position = historyEntry.getAttribute(DATA_ATTR_NODE);
bsw/jbe@1309 14202 node = this.element;
bsw/jbe@1309 14203 } else {
bsw/jbe@1309 14204 node = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element;
bsw/jbe@1309 14205 offset = node.getAttribute(DATA_ATTR_OFFSET);
bsw/jbe@1309 14206 position = node.getAttribute(DATA_ATTR_NODE);
bsw/jbe@1309 14207 node.removeAttribute(DATA_ATTR_OFFSET);
bsw/jbe@1309 14208 node.removeAttribute(DATA_ATTR_NODE);
bsw/jbe@1309 14209 }
bsw/jbe@1309 14210
bsw/jbe@1309 14211 if (position !== null) {
bsw/jbe@1309 14212 node = this.getChildNodeByIndex(node, +position);
bsw/jbe@1309 14213 }
bsw/jbe@1309 14214
bsw/jbe@1309 14215 this.composer.selection.set(node, offset);
bsw/jbe@1309 14216 },
bsw/jbe@1309 14217
bsw/jbe@1309 14218 getChildNodeIndex: function(parent, child) {
bsw/jbe@1309 14219 var i = 0,
bsw/jbe@1309 14220 childNodes = parent.childNodes,
bsw/jbe@1309 14221 length = childNodes.length;
bsw/jbe@1309 14222 for (; i<length; i++) {
bsw/jbe@1309 14223 if (childNodes[i] === child) {
bsw/jbe@1309 14224 return i;
bsw/jbe@1309 14225 }
bsw/jbe@1309 14226 }
bsw/jbe@1309 14227 },
bsw/jbe@1309 14228
bsw/jbe@1309 14229 getChildNodeByIndex: function(parent, index) {
bsw/jbe@1309 14230 return parent.childNodes[index];
bsw/jbe@1309 14231 }
bsw/jbe@1309 14232 });
bsw/jbe@1309 14233 })(wysihtml);
bsw/jbe@1309 14234
bsw/jbe@1309 14235 /**
bsw/jbe@1309 14236 * TODO: the following methods still need unit test coverage
bsw/jbe@1309 14237 */
bsw/jbe@1309 14238 wysihtml.views.View = Base.extend(
bsw/jbe@1309 14239 /** @scope wysihtml.views.View.prototype */ {
bsw/jbe@1309 14240 constructor: function(parent, textareaElement, config) {
bsw/jbe@1309 14241 this.parent = parent;
bsw/jbe@1309 14242 this.element = textareaElement;
bsw/jbe@1309 14243 this.config = config;
bsw/jbe@1309 14244 if (!this.config.noTextarea) {
bsw/jbe@1309 14245 this._observeViewChange();
bsw/jbe@1309 14246 }
bsw/jbe@1309 14247 },
bsw/jbe@1309 14248
bsw/jbe@1309 14249 _observeViewChange: function() {
bsw/jbe@1309 14250 var that = this;
bsw/jbe@1309 14251 this.parent.on("beforeload", function() {
bsw/jbe@1309 14252 that.parent.on("change_view", function(view) {
bsw/jbe@1309 14253 if (view === that.name) {
bsw/jbe@1309 14254 that.parent.currentView = that;
bsw/jbe@1309 14255 that.show();
bsw/jbe@1309 14256 // Using tiny delay here to make sure that the placeholder is set before focusing
bsw/jbe@1309 14257 setTimeout(function() { that.focus(); }, 0);
bsw/jbe@1309 14258 } else {
bsw/jbe@1309 14259 that.hide();
bsw/jbe@1309 14260 }
bsw/jbe@1309 14261 });
bsw/jbe@1309 14262 });
bsw/jbe@1309 14263 },
bsw/jbe@1309 14264
bsw/jbe@1309 14265 focus: function() {
bsw/jbe@1309 14266 if (this.element && this.element.ownerDocument && this.element.ownerDocument.querySelector(":focus") === this.element) {
bsw/jbe@1309 14267 return;
bsw/jbe@1309 14268 }
bsw/jbe@1309 14269
bsw/jbe@1309 14270 try { if(this.element) { this.element.focus(); } } catch(e) {}
bsw/jbe@1309 14271 },
bsw/jbe@1309 14272
bsw/jbe@1309 14273 hide: function() {
bsw/jbe@1309 14274 this.element.style.display = "none";
bsw/jbe@1309 14275 },
bsw/jbe@1309 14276
bsw/jbe@1309 14277 show: function() {
bsw/jbe@1309 14278 this.element.style.display = "";
bsw/jbe@1309 14279 },
bsw/jbe@1309 14280
bsw/jbe@1309 14281 disable: function() {
bsw/jbe@1309 14282 this.element.setAttribute("disabled", "disabled");
bsw/jbe@1309 14283 },
bsw/jbe@1309 14284
bsw/jbe@1309 14285 enable: function() {
bsw/jbe@1309 14286 this.element.removeAttribute("disabled");
bsw/jbe@1309 14287 }
bsw/jbe@1309 14288 });
bsw/jbe@1309 14289
bsw/jbe@1309 14290 (function(wysihtml) {
bsw/jbe@1309 14291 var dom = wysihtml.dom,
bsw/jbe@1309 14292 browser = wysihtml.browser;
bsw/jbe@1309 14293
bsw/jbe@1309 14294 wysihtml.views.Composer = wysihtml.views.View.extend(
bsw/jbe@1309 14295 /** @scope wysihtml.views.Composer.prototype */ {
bsw/jbe@1309 14296 name: "composer",
bsw/jbe@1309 14297
bsw/jbe@1309 14298 constructor: function(parent, editableElement, config) {
bsw/jbe@1309 14299 this.base(parent, editableElement, config);
bsw/jbe@1309 14300 if (!this.config.noTextarea) {
bsw/jbe@1309 14301 this.textarea = this.parent.textarea;
bsw/jbe@1309 14302 } else {
bsw/jbe@1309 14303 this.editableArea = editableElement;
bsw/jbe@1309 14304 }
bsw/jbe@1309 14305 if (this.config.contentEditableMode) {
bsw/jbe@1309 14306 this._initContentEditableArea();
bsw/jbe@1309 14307 } else {
bsw/jbe@1309 14308 this._initSandbox();
bsw/jbe@1309 14309 }
bsw/jbe@1309 14310 },
bsw/jbe@1309 14311
bsw/jbe@1309 14312 clear: function() {
bsw/jbe@1309 14313 this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : "<br>";
bsw/jbe@1309 14314 },
bsw/jbe@1309 14315
bsw/jbe@1309 14316 getValue: function(parse, clearInternals) {
bsw/jbe@1309 14317 var value = this.isEmpty() ? "" : wysihtml.quirks.getCorrectInnerHTML(this.element);
bsw/jbe@1309 14318 if (parse !== false) {
bsw/jbe@1309 14319 value = this.parent.parse(value, (clearInternals === false) ? false : true);
bsw/jbe@1309 14320 }
bsw/jbe@1309 14321 return value;
bsw/jbe@1309 14322 },
bsw/jbe@1309 14323
bsw/jbe@1309 14324 setValue: function(html, parse) {
bsw/jbe@1309 14325 if (parse !== false) {
bsw/jbe@1309 14326 html = this.parent.parse(html);
bsw/jbe@1309 14327 }
bsw/jbe@1309 14328
bsw/jbe@1309 14329 try {
bsw/jbe@1309 14330 this.element.innerHTML = html;
bsw/jbe@1309 14331 } catch (e) {
bsw/jbe@1309 14332 this.element.innerText = html;
bsw/jbe@1309 14333 }
bsw/jbe@1309 14334 },
bsw/jbe@1309 14335
bsw/jbe@1309 14336 cleanUp: function(rules) {
bsw/jbe@1309 14337 var bookmark;
bsw/jbe@1309 14338 if (this.selection && this.selection.isInThisEditable()) {
bsw/jbe@1309 14339 bookmark = rangy.saveSelection(this.win);
bsw/jbe@1309 14340 }
bsw/jbe@1309 14341 this.parent.parse(this.element, undefined, rules);
bsw/jbe@1309 14342 if (bookmark) {
bsw/jbe@1309 14343 rangy.restoreSelection(bookmark);
bsw/jbe@1309 14344 }
bsw/jbe@1309 14345 },
bsw/jbe@1309 14346
bsw/jbe@1309 14347 show: function() {
bsw/jbe@1309 14348 this.editableArea.style.display = this._displayStyle || "";
bsw/jbe@1309 14349
bsw/jbe@1309 14350 if (!this.config.noTextarea && !this.textarea.element.disabled) {
bsw/jbe@1309 14351 // Firefox needs this, otherwise contentEditable becomes uneditable
bsw/jbe@1309 14352 this.disable();
bsw/jbe@1309 14353 this.enable();
bsw/jbe@1309 14354 }
bsw/jbe@1309 14355 },
bsw/jbe@1309 14356
bsw/jbe@1309 14357 hide: function() {
bsw/jbe@1309 14358 this._displayStyle = dom.getStyle("display").from(this.editableArea);
bsw/jbe@1309 14359 if (this._displayStyle === "none") {
bsw/jbe@1309 14360 this._displayStyle = null;
bsw/jbe@1309 14361 }
bsw/jbe@1309 14362 this.editableArea.style.display = "none";
bsw/jbe@1309 14363 },
bsw/jbe@1309 14364
bsw/jbe@1309 14365 disable: function() {
bsw/jbe@1309 14366 this.parent.fire("disable:composer");
bsw/jbe@1309 14367 this.element.removeAttribute("contentEditable");
bsw/jbe@1309 14368 },
bsw/jbe@1309 14369
bsw/jbe@1309 14370 enable: function() {
bsw/jbe@1309 14371 this.parent.fire("enable:composer");
bsw/jbe@1309 14372 this.element.setAttribute("contentEditable", "true");
bsw/jbe@1309 14373 },
bsw/jbe@1309 14374
bsw/jbe@1309 14375 focus: function(setToEnd) {
bsw/jbe@1309 14376 // IE 8 fires the focus event after .focus()
bsw/jbe@1309 14377 // This is needed by our simulate_placeholder.js to work
bsw/jbe@1309 14378 // therefore we clear it ourselves this time
bsw/jbe@1309 14379 if (wysihtml.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
bsw/jbe@1309 14380 this.clear();
bsw/jbe@1309 14381 }
bsw/jbe@1309 14382
bsw/jbe@1309 14383 this.base();
bsw/jbe@1309 14384
bsw/jbe@1309 14385 var lastChild = this.element.lastChild;
bsw/jbe@1309 14386 if (setToEnd && lastChild && this.selection) {
bsw/jbe@1309 14387 if (lastChild.nodeName === "BR") {
bsw/jbe@1309 14388 this.selection.setBefore(this.element.lastChild);
bsw/jbe@1309 14389 } else {
bsw/jbe@1309 14390 this.selection.setAfter(this.element.lastChild);
bsw/jbe@1309 14391 }
bsw/jbe@1309 14392 }
bsw/jbe@1309 14393 },
bsw/jbe@1309 14394
bsw/jbe@1309 14395 getScrollPos: function() {
bsw/jbe@1309 14396 if (this.doc && this.win) {
bsw/jbe@1309 14397 var pos = {};
bsw/jbe@1309 14398
bsw/jbe@1309 14399 if (typeof this.win.pageYOffset !== "undefined") {
bsw/jbe@1309 14400 pos.y = this.win.pageYOffset;
bsw/jbe@1309 14401 } else {
bsw/jbe@1309 14402 pos.y = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollTop;
bsw/jbe@1309 14403 }
bsw/jbe@1309 14404
bsw/jbe@1309 14405 if (typeof this.win.pageXOffset !== "undefined") {
bsw/jbe@1309 14406 pos.x = this.win.pageXOffset;
bsw/jbe@1309 14407 } else {
bsw/jbe@1309 14408 pos.x = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollLeft;
bsw/jbe@1309 14409 }
bsw/jbe@1309 14410
bsw/jbe@1309 14411 return pos;
bsw/jbe@1309 14412 }
bsw/jbe@1309 14413 },
bsw/jbe@1309 14414
bsw/jbe@1309 14415 setScrollPos: function(pos) {
bsw/jbe@1309 14416 if (pos && typeof pos.x !== "undefined" && typeof pos.y !== "undefined") {
bsw/jbe@1309 14417 this.win.scrollTo(pos.x, pos.y);
bsw/jbe@1309 14418 }
bsw/jbe@1309 14419 },
bsw/jbe@1309 14420
bsw/jbe@1309 14421 getTextContent: function() {
bsw/jbe@1309 14422 return dom.getTextContent(this.element);
bsw/jbe@1309 14423 },
bsw/jbe@1309 14424
bsw/jbe@1309 14425 hasPlaceholderSet: function() {
bsw/jbe@1309 14426 return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
bsw/jbe@1309 14427 },
bsw/jbe@1309 14428
bsw/jbe@1309 14429 isEmpty: function() {
bsw/jbe@1309 14430 var innerHTML = this.element.innerHTML.toLowerCase();
bsw/jbe@1309 14431 return (/^(\s|<br>|<\/br>|<p>|<\/p>)*$/i).test(innerHTML) ||
bsw/jbe@1309 14432 innerHTML === "" ||
bsw/jbe@1309 14433 innerHTML === "<br>" ||
bsw/jbe@1309 14434 innerHTML === "<p></p>" ||
bsw/jbe@1309 14435 innerHTML === "<p><br></p>" ||
bsw/jbe@1309 14436 this.hasPlaceholderSet();
bsw/jbe@1309 14437 },
bsw/jbe@1309 14438
bsw/jbe@1309 14439 _initContentEditableArea: function() {
bsw/jbe@1309 14440 var that = this;
bsw/jbe@1309 14441 if (this.config.noTextarea) {
bsw/jbe@1309 14442 this.sandbox = new dom.ContentEditableArea(function() {
bsw/jbe@1309 14443 that._create();
bsw/jbe@1309 14444 }, {
bsw/jbe@1309 14445 className: this.config.classNames.sandbox
bsw/jbe@1309 14446 }, this.editableArea);
bsw/jbe@1309 14447 } else {
bsw/jbe@1309 14448 this.sandbox = new dom.ContentEditableArea(function() {
bsw/jbe@1309 14449 that._create();
bsw/jbe@1309 14450 }, {
bsw/jbe@1309 14451 className: this.config.classNames.sandbox
bsw/jbe@1309 14452 });
bsw/jbe@1309 14453 this.editableArea = this.sandbox.getContentEditable();
bsw/jbe@1309 14454 dom.insert(this.editableArea).after(this.textarea.element);
bsw/jbe@1309 14455 this._createWysiwygFormField();
bsw/jbe@1309 14456 }
bsw/jbe@1309 14457 },
bsw/jbe@1309 14458
bsw/jbe@1309 14459 _initSandbox: function() {
bsw/jbe@1309 14460 var that = this;
bsw/jbe@1309 14461 this.sandbox = new dom.Sandbox(function() {
bsw/jbe@1309 14462 that._create();
bsw/jbe@1309 14463 }, {
bsw/jbe@1309 14464 stylesheets: this.config.stylesheets,
bsw/jbe@1309 14465 className: this.config.classNames.sandbox
bsw/jbe@1309 14466 });
bsw/jbe@1309 14467 this.editableArea = this.sandbox.getIframe();
bsw/jbe@1309 14468
bsw/jbe@1309 14469 var textareaElement = this.textarea.element;
bsw/jbe@1309 14470 dom.insert(this.editableArea).after(textareaElement);
bsw/jbe@1309 14471
bsw/jbe@1309 14472 this._createWysiwygFormField();
bsw/jbe@1309 14473 },
bsw/jbe@1309 14474
bsw/jbe@1309 14475 // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor
bsw/jbe@1309 14476 _createWysiwygFormField: function() {
bsw/jbe@1309 14477 if (this.textarea.element.form) {
bsw/jbe@1309 14478 var hiddenField = document.createElement("input");
bsw/jbe@1309 14479 hiddenField.type = "hidden";
bsw/jbe@1309 14480 hiddenField.name = "_wysihtml_mode";
bsw/jbe@1309 14481 hiddenField.value = 1;
bsw/jbe@1309 14482 dom.insert(hiddenField).after(this.textarea.element);
bsw/jbe@1309 14483 }
bsw/jbe@1309 14484 },
bsw/jbe@1309 14485
bsw/jbe@1309 14486 _create: function() {
bsw/jbe@1309 14487 var that = this;
bsw/jbe@1309 14488 this.doc = this.sandbox.getDocument();
bsw/jbe@1309 14489 this.win = this.sandbox.getWindow();
bsw/jbe@1309 14490 this.element = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body;
bsw/jbe@1309 14491 if (!this.config.noTextarea) {
bsw/jbe@1309 14492 this.textarea = this.parent.textarea;
bsw/jbe@1309 14493 this.element.innerHTML = this.textarea.getValue(true, false);
bsw/jbe@1309 14494 } else {
bsw/jbe@1309 14495 this.cleanUp(); // cleans contenteditable on initiation as it may contain html
bsw/jbe@1309 14496 }
bsw/jbe@1309 14497
bsw/jbe@1309 14498 // Make sure our selection handler is ready
bsw/jbe@1309 14499 this.selection = new wysihtml.Selection(this.parent, this.element, this.config.classNames.uneditableContainer);
bsw/jbe@1309 14500
bsw/jbe@1309 14501 // Make sure commands dispatcher is ready
bsw/jbe@1309 14502 this.commands = new wysihtml.Commands(this.parent);
bsw/jbe@1309 14503
bsw/jbe@1309 14504 if (!this.config.noTextarea) {
bsw/jbe@1309 14505 dom.copyAttributes([
bsw/jbe@1309 14506 "className", "spellcheck", "title", "lang", "dir", "accessKey"
bsw/jbe@1309 14507 ]).from(this.textarea.element).to(this.element);
bsw/jbe@1309 14508 }
bsw/jbe@1309 14509
bsw/jbe@1309 14510 this._initAutoLinking();
bsw/jbe@1309 14511
bsw/jbe@1309 14512 dom.addClass(this.element, this.config.classNames.composer);
bsw/jbe@1309 14513 //
bsw/jbe@1309 14514 // Make the editor look like the original textarea, by syncing styles
bsw/jbe@1309 14515 if (this.config.style && !this.config.contentEditableMode) {
bsw/jbe@1309 14516 this.style();
bsw/jbe@1309 14517 }
bsw/jbe@1309 14518
bsw/jbe@1309 14519 this.observe();
bsw/jbe@1309 14520
bsw/jbe@1309 14521 var name = this.config.name;
bsw/jbe@1309 14522 if (name) {
bsw/jbe@1309 14523 dom.addClass(this.element, name);
bsw/jbe@1309 14524 if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
bsw/jbe@1309 14525 }
bsw/jbe@1309 14526
bsw/jbe@1309 14527 this.enable();
bsw/jbe@1309 14528
bsw/jbe@1309 14529 if (!this.config.noTextarea && this.textarea.element.disabled) {
bsw/jbe@1309 14530 this.disable();
bsw/jbe@1309 14531 }
bsw/jbe@1309 14532
bsw/jbe@1309 14533 // Simulate html5 placeholder attribute on contentEditable element
bsw/jbe@1309 14534 var placeholderText = typeof(this.config.placeholder) === "string"
bsw/jbe@1309 14535 ? this.config.placeholder
bsw/jbe@1309 14536 : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder"));
bsw/jbe@1309 14537 if (placeholderText) {
bsw/jbe@1309 14538 dom.simulatePlaceholder(this.parent, this, placeholderText, this.config.classNames.placeholder);
bsw/jbe@1309 14539 }
bsw/jbe@1309 14540
bsw/jbe@1309 14541 // Make sure that the browser avoids using inline styles whenever possible
bsw/jbe@1309 14542 this.commands.exec("styleWithCSS", false);
bsw/jbe@1309 14543
bsw/jbe@1309 14544 this._initObjectResizing();
bsw/jbe@1309 14545 this._initUndoManager();
bsw/jbe@1309 14546 this._initLineBreaking();
bsw/jbe@1309 14547
bsw/jbe@1309 14548 // Simulate html5 autofocus on contentEditable element
bsw/jbe@1309 14549 // This doesn't work on IOS (5.1.1)
bsw/jbe@1309 14550 if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
bsw/jbe@1309 14551 setTimeout(function() { that.focus(true); }, 100);
bsw/jbe@1309 14552 }
bsw/jbe@1309 14553
bsw/jbe@1309 14554 // IE sometimes leaves a single paragraph, which can't be removed by the user
bsw/jbe@1309 14555 if (!browser.clearsContentEditableCorrectly()) {
bsw/jbe@1309 14556 wysihtml.quirks.ensureProperClearing(this);
bsw/jbe@1309 14557 }
bsw/jbe@1309 14558
bsw/jbe@1309 14559 // Set up a sync that makes sure that textarea and editor have the same content
bsw/jbe@1309 14560 if (this.initSync && this.config.sync) {
bsw/jbe@1309 14561 this.initSync();
bsw/jbe@1309 14562 }
bsw/jbe@1309 14563
bsw/jbe@1309 14564 // Okay hide the textarea, we are ready to go
bsw/jbe@1309 14565 if (!this.config.noTextarea) { this.textarea.hide(); }
bsw/jbe@1309 14566
bsw/jbe@1309 14567 // Fire global (before-)load event
bsw/jbe@1309 14568 this.parent.fire("beforeload").fire("load");
bsw/jbe@1309 14569 },
bsw/jbe@1309 14570
bsw/jbe@1309 14571 _initAutoLinking: function() {
bsw/jbe@1309 14572 var that = this,
bsw/jbe@1309 14573 supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
bsw/jbe@1309 14574 supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
bsw/jbe@1309 14575
bsw/jbe@1309 14576 if (supportsDisablingOfAutoLinking) {
bsw/jbe@1309 14577 this.commands.exec("AutoUrlDetect", false, false);
bsw/jbe@1309 14578 }
bsw/jbe@1309 14579
bsw/jbe@1309 14580 if (!this.config.autoLink) {
bsw/jbe@1309 14581 return;
bsw/jbe@1309 14582 }
bsw/jbe@1309 14583
bsw/jbe@1309 14584 // Only do the auto linking by ourselves when the browser doesn't support auto linking
bsw/jbe@1309 14585 // OR when he supports auto linking but we were able to turn it off (IE9+)
bsw/jbe@1309 14586 if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
bsw/jbe@1309 14587 this.parent.on("newword:composer", function() {
bsw/jbe@1309 14588 if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
bsw/jbe@1309 14589 var nodeWithSelection = that.selection.getSelectedNode(),
bsw/jbe@1309 14590 uneditables = that.element.querySelectorAll("." + that.config.classNames.uneditableContainer),
bsw/jbe@1309 14591 isInUneditable = false;
bsw/jbe@1309 14592
bsw/jbe@1309 14593 for (var i = uneditables.length; i--;) {
bsw/jbe@1309 14594 if (wysihtml.dom.contains(uneditables[i], nodeWithSelection)) {
bsw/jbe@1309 14595 isInUneditable = true;
bsw/jbe@1309 14596 }
bsw/jbe@1309 14597 }
bsw/jbe@1309 14598
bsw/jbe@1309 14599 if (!isInUneditable) dom.autoLink(nodeWithSelection, [that.config.classNames.uneditableContainer]);
bsw/jbe@1309 14600 }
bsw/jbe@1309 14601 });
bsw/jbe@1309 14602
bsw/jbe@1309 14603 dom.observe(this.element, "blur", function() {
bsw/jbe@1309 14604 dom.autoLink(that.element, [that.config.classNames.uneditableContainer]);
bsw/jbe@1309 14605 });
bsw/jbe@1309 14606 }
bsw/jbe@1309 14607
bsw/jbe@1309 14608 // Assuming we have the following:
bsw/jbe@1309 14609 // <a href="http://www.google.de">http://www.google.de</a>
bsw/jbe@1309 14610 // If a user now changes the url in the innerHTML we want to make sure that
bsw/jbe@1309 14611 // it's synchronized with the href attribute (as long as the innerHTML is still a url)
bsw/jbe@1309 14612 var // Use a live NodeList to check whether there are any links in the document
bsw/jbe@1309 14613 links = this.sandbox.getDocument().getElementsByTagName("a"),
bsw/jbe@1309 14614 // The autoLink helper method reveals a reg exp to detect correct urls
bsw/jbe@1309 14615 urlRegExp = dom.autoLink.URL_REG_EXP,
bsw/jbe@1309 14616 getTextContent = function(element) {
bsw/jbe@1309 14617 var textContent = wysihtml.lang.string(dom.getTextContent(element)).trim();
bsw/jbe@1309 14618 if (textContent.substr(0, 4) === "www.") {
bsw/jbe@1309 14619 textContent = "http://" + textContent;
bsw/jbe@1309 14620 }
bsw/jbe@1309 14621 return textContent;
bsw/jbe@1309 14622 };
bsw/jbe@1309 14623
bsw/jbe@1309 14624 dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309 14625 if (!links.length) {
bsw/jbe@1309 14626 return;
bsw/jbe@1309 14627 }
bsw/jbe@1309 14628
bsw/jbe@1309 14629 var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
bsw/jbe@1309 14630 link = dom.getParentElement(selectedNode, { query: "a" }, 4),
bsw/jbe@1309 14631 textContent;
bsw/jbe@1309 14632
bsw/jbe@1309 14633 if (!link) {
bsw/jbe@1309 14634 return;
bsw/jbe@1309 14635 }
bsw/jbe@1309 14636
bsw/jbe@1309 14637 textContent = getTextContent(link);
bsw/jbe@1309 14638 // keydown is fired before the actual content is changed
bsw/jbe@1309 14639 // therefore we set a timeout to change the href
bsw/jbe@1309 14640 setTimeout(function() {
bsw/jbe@1309 14641 var newTextContent = getTextContent(link);
bsw/jbe@1309 14642 if (newTextContent === textContent) {
bsw/jbe@1309 14643 return;
bsw/jbe@1309 14644 }
bsw/jbe@1309 14645
bsw/jbe@1309 14646 // Only set href when new href looks like a valid url
bsw/jbe@1309 14647 if (newTextContent.match(urlRegExp)) {
bsw/jbe@1309 14648 link.setAttribute("href", newTextContent);
bsw/jbe@1309 14649 }
bsw/jbe@1309 14650 }, 0);
bsw/jbe@1309 14651 });
bsw/jbe@1309 14652 },
bsw/jbe@1309 14653
bsw/jbe@1309 14654 _initObjectResizing: function() {
bsw/jbe@1309 14655 this.commands.exec("enableObjectResizing", true);
bsw/jbe@1309 14656
bsw/jbe@1309 14657 // IE sets inline styles after resizing objects
bsw/jbe@1309 14658 // The following lines make sure that the width/height css properties
bsw/jbe@1309 14659 // are copied over to the width/height attributes
bsw/jbe@1309 14660 if (browser.supportsEvent("resizeend")) {
bsw/jbe@1309 14661 var properties = ["width", "height"],
bsw/jbe@1309 14662 propertiesLength = properties.length,
bsw/jbe@1309 14663 element = this.element;
bsw/jbe@1309 14664
bsw/jbe@1309 14665 dom.observe(element, "resizeend", function(event) {
bsw/jbe@1309 14666 var target = event.target || event.srcElement,
bsw/jbe@1309 14667 style = target.style,
bsw/jbe@1309 14668 i = 0,
bsw/jbe@1309 14669 property;
bsw/jbe@1309 14670
bsw/jbe@1309 14671 if (target.nodeName !== "IMG") {
bsw/jbe@1309 14672 return;
bsw/jbe@1309 14673 }
bsw/jbe@1309 14674
bsw/jbe@1309 14675 for (; i<propertiesLength; i++) {
bsw/jbe@1309 14676 property = properties[i];
bsw/jbe@1309 14677 if (style[property]) {
bsw/jbe@1309 14678 target.setAttribute(property, parseInt(style[property], 10));
bsw/jbe@1309 14679 style[property] = "";
bsw/jbe@1309 14680 }
bsw/jbe@1309 14681 }
bsw/jbe@1309 14682
bsw/jbe@1309 14683 // After resizing IE sometimes forgets to remove the old resize handles
bsw/jbe@1309 14684 wysihtml.quirks.redraw(element);
bsw/jbe@1309 14685 });
bsw/jbe@1309 14686 }
bsw/jbe@1309 14687 },
bsw/jbe@1309 14688
bsw/jbe@1309 14689 _initUndoManager: function() {
bsw/jbe@1309 14690 this.undoManager = new wysihtml.UndoManager(this.parent);
bsw/jbe@1309 14691 },
bsw/jbe@1309 14692
bsw/jbe@1309 14693 _initLineBreaking: function() {
bsw/jbe@1309 14694 var that = this,
bsw/jbe@1309 14695 USE_NATIVE_LINE_BREAK_INSIDE_TAGS = "li, p, h1, h2, h3, h4, h5, h6",
bsw/jbe@1309 14696 LIST_TAGS = "ul, ol, menu";
bsw/jbe@1309 14697
bsw/jbe@1309 14698 function adjust(selectedNode) {
bsw/jbe@1309 14699 var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2);
bsw/jbe@1309 14700 if (parentElement && dom.contains(that.element, parentElement)) {
bsw/jbe@1309 14701 that.selection.executeAndRestoreRangy(function() {
bsw/jbe@1309 14702 if (that.config.useLineBreaks) {
bsw/jbe@1309 14703 if (!parentElement.firstChild || (parentElement.firstChild === parentElement.lastChild && parentElement.firstChild.nodeType === 1 && parentElement.firstChild.classList.contains('rangySelectionBoundary'))) {
bsw/jbe@1309 14704 parentElement.appendChild(that.doc.createElement('br'));
bsw/jbe@1309 14705 }
bsw/jbe@1309 14706 dom.replaceWithChildNodes(parentElement);
bsw/jbe@1309 14707 } else if (parentElement.nodeName !== "P") {
bsw/jbe@1309 14708 dom.renameElement(parentElement, "p");
bsw/jbe@1309 14709 }
bsw/jbe@1309 14710 });
bsw/jbe@1309 14711 }
bsw/jbe@1309 14712 }
bsw/jbe@1309 14713
bsw/jbe@1309 14714 // Ensures when editor is empty and not line breaks mode, the inital state has a paragraph in it on focus with caret inside paragraph
bsw/jbe@1309 14715 if (!this.config.useLineBreaks) {
bsw/jbe@1309 14716 dom.observe(this.element, ["focus"], function() {
bsw/jbe@1309 14717 if (that.isEmpty()) {
bsw/jbe@1309 14718 setTimeout(function() {
bsw/jbe@1309 14719 var paragraph = that.doc.createElement("P");
bsw/jbe@1309 14720 that.element.innerHTML = "";
bsw/jbe@1309 14721 that.element.appendChild(paragraph);
bsw/jbe@1309 14722 if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
bsw/jbe@1309 14723 paragraph.innerHTML = "<br>";
bsw/jbe@1309 14724 that.selection.setBefore(paragraph.firstChild);
bsw/jbe@1309 14725 } else {
bsw/jbe@1309 14726 that.selection.selectNode(paragraph, true);
bsw/jbe@1309 14727 }
bsw/jbe@1309 14728 }, 0);
bsw/jbe@1309 14729 }
bsw/jbe@1309 14730 });
bsw/jbe@1309 14731 }
bsw/jbe@1309 14732
bsw/jbe@1309 14733 dom.observe(this.element, "keydown", function(event) {
bsw/jbe@1309 14734 var keyCode = event.keyCode;
bsw/jbe@1309 14735
bsw/jbe@1309 14736 if (event.shiftKey || event.ctrlKey || event.defaultPrevented) {
bsw/jbe@1309 14737 return;
bsw/jbe@1309 14738 }
bsw/jbe@1309 14739
bsw/jbe@1309 14740 if (keyCode !== wysihtml.ENTER_KEY && keyCode !== wysihtml.BACKSPACE_KEY) {
bsw/jbe@1309 14741 return;
bsw/jbe@1309 14742 }
bsw/jbe@1309 14743 var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { query: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
bsw/jbe@1309 14744 if (blockElement) {
bsw/jbe@1309 14745 setTimeout(function() {
bsw/jbe@1309 14746 // Unwrap paragraph after leaving a list or a H1-6
bsw/jbe@1309 14747 var selectedNode = that.selection.getSelectedNode(),
bsw/jbe@1309 14748 list;
bsw/jbe@1309 14749
bsw/jbe@1309 14750 if (blockElement.nodeName === "LI") {
bsw/jbe@1309 14751 if (!selectedNode) {
bsw/jbe@1309 14752 return;
bsw/jbe@1309 14753 }
bsw/jbe@1309 14754
bsw/jbe@1309 14755 list = dom.getParentElement(selectedNode, { query: LIST_TAGS }, 2);
bsw/jbe@1309 14756
bsw/jbe@1309 14757 if (!list) {
bsw/jbe@1309 14758 adjust(selectedNode);
bsw/jbe@1309 14759 }
bsw/jbe@1309 14760 }
bsw/jbe@1309 14761
bsw/jbe@1309 14762 if (keyCode === wysihtml.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) {
bsw/jbe@1309 14763 adjust(selectedNode);
bsw/jbe@1309 14764 }
bsw/jbe@1309 14765 }, 0);
bsw/jbe@1309 14766 return;
bsw/jbe@1309 14767 }
bsw/jbe@1309 14768 if (that.config.useLineBreaks && keyCode === wysihtml.ENTER_KEY && !wysihtml.browser.insertsLineBreaksOnReturn()) {
bsw/jbe@1309 14769 event.preventDefault();
bsw/jbe@1309 14770 that.commands.exec("insertLineBreak");
bsw/jbe@1309 14771 }
bsw/jbe@1309 14772 });
bsw/jbe@1309 14773 }
bsw/jbe@1309 14774 });
bsw/jbe@1309 14775 })(wysihtml);
bsw/jbe@1309 14776
bsw/jbe@1309 14777 (function(wysihtml) {
bsw/jbe@1309 14778 var dom = wysihtml.dom,
bsw/jbe@1309 14779 doc = document,
bsw/jbe@1309 14780 win = window,
bsw/jbe@1309 14781 HOST_TEMPLATE = doc.createElement("div"),
bsw/jbe@1309 14782 /**
bsw/jbe@1309 14783 * Styles to copy from textarea to the composer element
bsw/jbe@1309 14784 */
bsw/jbe@1309 14785 TEXT_FORMATTING = [
bsw/jbe@1309 14786 "background-color",
bsw/jbe@1309 14787 "color", "cursor",
bsw/jbe@1309 14788 "font-family", "font-size", "font-style", "font-variant", "font-weight",
bsw/jbe@1309 14789 "line-height", "letter-spacing",
bsw/jbe@1309 14790 "text-align", "text-decoration", "text-indent", "text-rendering",
bsw/jbe@1309 14791 "word-break", "word-wrap", "word-spacing"
bsw/jbe@1309 14792 ],
bsw/jbe@1309 14793 /**
bsw/jbe@1309 14794 * Styles to copy from textarea to the iframe
bsw/jbe@1309 14795 */
bsw/jbe@1309 14796 BOX_FORMATTING = [
bsw/jbe@1309 14797 "background-color",
bsw/jbe@1309 14798 "border-collapse",
bsw/jbe@1309 14799 "border-bottom-color", "border-bottom-style", "border-bottom-width",
bsw/jbe@1309 14800 "border-left-color", "border-left-style", "border-left-width",
bsw/jbe@1309 14801 "border-right-color", "border-right-style", "border-right-width",
bsw/jbe@1309 14802 "border-top-color", "border-top-style", "border-top-width",
bsw/jbe@1309 14803 "clear", "display", "float",
bsw/jbe@1309 14804 "margin-bottom", "margin-left", "margin-right", "margin-top",
bsw/jbe@1309 14805 "outline-color", "outline-offset", "outline-width", "outline-style",
bsw/jbe@1309 14806 "padding-left", "padding-right", "padding-top", "padding-bottom",
bsw/jbe@1309 14807 "position", "top", "left", "right", "bottom", "z-index",
bsw/jbe@1309 14808 "vertical-align", "text-align",
bsw/jbe@1309 14809 "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
bsw/jbe@1309 14810 "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
bsw/jbe@1309 14811 "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
bsw/jbe@1309 14812 "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
bsw/jbe@1309 14813 "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
bsw/jbe@1309 14814 "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
bsw/jbe@1309 14815 "width", "height"
bsw/jbe@1309 14816 ],
bsw/jbe@1309 14817 ADDITIONAL_CSS_RULES = [
bsw/jbe@1309 14818 "html { height: 100%; }",
bsw/jbe@1309 14819 "body { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }",
bsw/jbe@1309 14820 "body > p:first-child { margin-top: 0; }",
bsw/jbe@1309 14821 "._wysihtml-temp { display: none; }",
bsw/jbe@1309 14822 wysihtml.browser.isGecko ?
bsw/jbe@1309 14823 "body.placeholder { color: graytext !important; }" :
bsw/jbe@1309 14824 "body.placeholder { color: #a9a9a9 !important; }",
bsw/jbe@1309 14825 // Ensure that user see's broken images and can delete them
bsw/jbe@1309 14826 "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
bsw/jbe@1309 14827 ];
bsw/jbe@1309 14828
bsw/jbe@1309 14829 /**
bsw/jbe@1309 14830 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
bsw/jbe@1309 14831 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
bsw/jbe@1309 14832 *
bsw/jbe@1309 14833 * Other browsers need a more hacky way: (pssst don't tell my mama)
bsw/jbe@1309 14834 * In order to prevent the element being scrolled into view when focusing it, we simply
bsw/jbe@1309 14835 * move it out of the scrollable area, focus it, and reset it's position
bsw/jbe@1309 14836 */
bsw/jbe@1309 14837 var focusWithoutScrolling = function(element) {
bsw/jbe@1309 14838 if (element.setActive) {
bsw/jbe@1309 14839 // Following line could cause a js error when the textarea is invisible
bsw/jbe@1309 14840 // See https://github.com/xing/wysihtml5/issues/9
bsw/jbe@1309 14841 try { element.setActive(); } catch(e) {}
bsw/jbe@1309 14842 } else {
bsw/jbe@1309 14843 var elementStyle = element.style,
bsw/jbe@1309 14844 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
bsw/jbe@1309 14845 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
bsw/jbe@1309 14846 originalStyles = {
bsw/jbe@1309 14847 position: elementStyle.position,
bsw/jbe@1309 14848 top: elementStyle.top,
bsw/jbe@1309 14849 left: elementStyle.left,
bsw/jbe@1309 14850 WebkitUserSelect: elementStyle.WebkitUserSelect
bsw/jbe@1309 14851 };
bsw/jbe@1309 14852
bsw/jbe@1309 14853 dom.setStyles({
bsw/jbe@1309 14854 position: "absolute",
bsw/jbe@1309 14855 top: "-99999px",
bsw/jbe@1309 14856 left: "-99999px",
bsw/jbe@1309 14857 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
bsw/jbe@1309 14858 WebkitUserSelect: "none"
bsw/jbe@1309 14859 }).on(element);
bsw/jbe@1309 14860
bsw/jbe@1309 14861 element.focus();
bsw/jbe@1309 14862
bsw/jbe@1309 14863 dom.setStyles(originalStyles).on(element);
bsw/jbe@1309 14864
bsw/jbe@1309 14865 if (win.scrollTo) {
bsw/jbe@1309 14866 // Some browser extensions unset this method to prevent annoyances
bsw/jbe@1309 14867 // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
bsw/jbe@1309 14868 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
bsw/jbe@1309 14869 win.scrollTo(originalScrollLeft, originalScrollTop);
bsw/jbe@1309 14870 }
bsw/jbe@1309 14871 }
bsw/jbe@1309 14872 };
bsw/jbe@1309 14873
bsw/jbe@1309 14874
bsw/jbe@1309 14875 wysihtml.views.Composer.prototype.style = function() {
bsw/jbe@1309 14876 var that = this,
bsw/jbe@1309 14877 originalActiveElement = doc.querySelector(":focus"),
bsw/jbe@1309 14878 textareaElement = this.textarea.element,
bsw/jbe@1309 14879 hasPlaceholder = textareaElement.hasAttribute("placeholder"),
bsw/jbe@1309 14880 originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"),
bsw/jbe@1309 14881 originalDisplayValue = textareaElement.style.display,
bsw/jbe@1309 14882 originalDisabled = textareaElement.disabled,
bsw/jbe@1309 14883 displayValueForCopying;
bsw/jbe@1309 14884
bsw/jbe@1309 14885 this.focusStylesHost = HOST_TEMPLATE.cloneNode(false);
bsw/jbe@1309 14886 this.blurStylesHost = HOST_TEMPLATE.cloneNode(false);
bsw/jbe@1309 14887 this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false);
bsw/jbe@1309 14888
bsw/jbe@1309 14889 // Remove placeholder before copying (as the placeholder has an affect on the computed style)
bsw/jbe@1309 14890 if (hasPlaceholder) {
bsw/jbe@1309 14891 textareaElement.removeAttribute("placeholder");
bsw/jbe@1309 14892 }
bsw/jbe@1309 14893
bsw/jbe@1309 14894 if (textareaElement === originalActiveElement) {
bsw/jbe@1309 14895 textareaElement.blur();
bsw/jbe@1309 14896 }
bsw/jbe@1309 14897
bsw/jbe@1309 14898 // enable for copying styles
bsw/jbe@1309 14899 textareaElement.disabled = false;
bsw/jbe@1309 14900
bsw/jbe@1309 14901 // set textarea to display="none" to get cascaded styles via getComputedStyle
bsw/jbe@1309 14902 textareaElement.style.display = displayValueForCopying = "none";
bsw/jbe@1309 14903
bsw/jbe@1309 14904 if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
bsw/jbe@1309 14905 (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
bsw/jbe@1309 14906 textareaElement.style.display = displayValueForCopying = originalDisplayValue;
bsw/jbe@1309 14907 }
bsw/jbe@1309 14908
bsw/jbe@1309 14909 // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
bsw/jbe@1309 14910 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost);
bsw/jbe@1309 14911
bsw/jbe@1309 14912 // --------- editor styles ---------
bsw/jbe@1309 14913 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
bsw/jbe@1309 14914
bsw/jbe@1309 14915 // --------- apply standard rules ---------
bsw/jbe@1309 14916 dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
bsw/jbe@1309 14917
bsw/jbe@1309 14918 // --------- :disabled styles ---------
bsw/jbe@1309 14919 textareaElement.disabled = true;
bsw/jbe@1309 14920 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
bsw/jbe@1309 14921 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
bsw/jbe@1309 14922 textareaElement.disabled = originalDisabled;
bsw/jbe@1309 14923
bsw/jbe@1309 14924 // --------- :focus styles ---------
bsw/jbe@1309 14925 textareaElement.style.display = originalDisplayValue;
bsw/jbe@1309 14926 focusWithoutScrolling(textareaElement);
bsw/jbe@1309 14927 textareaElement.style.display = displayValueForCopying;
bsw/jbe@1309 14928
bsw/jbe@1309 14929 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
bsw/jbe@1309 14930 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
bsw/jbe@1309 14931
bsw/jbe@1309 14932 // reset textarea
bsw/jbe@1309 14933 textareaElement.style.display = originalDisplayValue;
bsw/jbe@1309 14934
bsw/jbe@1309 14935 dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
bsw/jbe@1309 14936
bsw/jbe@1309 14937 // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
bsw/jbe@1309 14938 // this is needed for when the change_view event is fired where the iframe is hidden and then
bsw/jbe@1309 14939 // the blur event fires and re-displays it
bsw/jbe@1309 14940 var boxFormattingStyles = wysihtml.lang.array(BOX_FORMATTING).without(["display"]);
bsw/jbe@1309 14941
bsw/jbe@1309 14942 // --------- restore focus ---------
bsw/jbe@1309 14943 if (originalActiveElement) {
bsw/jbe@1309 14944 focusWithoutScrolling(originalActiveElement);
bsw/jbe@1309 14945 } else {
bsw/jbe@1309 14946 textareaElement.blur();
bsw/jbe@1309 14947 }
bsw/jbe@1309 14948
bsw/jbe@1309 14949 // --------- restore placeholder ---------
bsw/jbe@1309 14950 if (hasPlaceholder) {
bsw/jbe@1309 14951 textareaElement.setAttribute("placeholder", originalPlaceholder);
bsw/jbe@1309 14952 }
bsw/jbe@1309 14953
bsw/jbe@1309 14954 // --------- Sync focus/blur styles ---------
bsw/jbe@1309 14955 this.parent.on("focus:composer", function() {
bsw/jbe@1309 14956 dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea);
bsw/jbe@1309 14957 dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element);
bsw/jbe@1309 14958 });
bsw/jbe@1309 14959
bsw/jbe@1309 14960 this.parent.on("blur:composer", function() {
bsw/jbe@1309 14961 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
bsw/jbe@1309 14962 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
bsw/jbe@1309 14963 });
bsw/jbe@1309 14964
bsw/jbe@1309 14965 this.parent.observe("disable:composer", function() {
bsw/jbe@1309 14966 dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea);
bsw/jbe@1309 14967 dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element);
bsw/jbe@1309 14968 });
bsw/jbe@1309 14969
bsw/jbe@1309 14970 this.parent.observe("enable:composer", function() {
bsw/jbe@1309 14971 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
bsw/jbe@1309 14972 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
bsw/jbe@1309 14973 });
bsw/jbe@1309 14974
bsw/jbe@1309 14975 return this;
bsw/jbe@1309 14976 };
bsw/jbe@1309 14977 })(wysihtml);
bsw/jbe@1309 14978
bsw/jbe@1309 14979 /**
bsw/jbe@1309 14980 * Taking care of events
bsw/jbe@1309 14981 * - Simulating 'change' event on contentEditable element
bsw/jbe@1309 14982 * - Handling drag & drop logic
bsw/jbe@1309 14983 * - Catch paste events
bsw/jbe@1309 14984 * - Dispatch proprietary newword:composer event
bsw/jbe@1309 14985 * - Keyboard shortcuts
bsw/jbe@1309 14986 */
bsw/jbe@1309 14987 (function(wysihtml) {
bsw/jbe@1309 14988 var dom = wysihtml.dom,
bsw/jbe@1309 14989 domNode = dom.domNode,
bsw/jbe@1309 14990 browser = wysihtml.browser,
bsw/jbe@1309 14991 /**
bsw/jbe@1309 14992 * Map keyCodes to query commands
bsw/jbe@1309 14993 */
bsw/jbe@1309 14994 shortcuts = {
bsw/jbe@1309 14995 "66": "bold", // B
bsw/jbe@1309 14996 "73": "italic", // I
bsw/jbe@1309 14997 "85": "underline" // U
bsw/jbe@1309 14998 };
bsw/jbe@1309 14999
bsw/jbe@1309 15000 var actions = {
bsw/jbe@1309 15001
bsw/jbe@1309 15002 // Adds multiple eventlisteners to target, bound to one callback
bsw/jbe@1309 15003 // TODO: If needed elsewhere make it part of wysihtml.dom or sth
bsw/jbe@1309 15004 addListeners: function (target, events, callback) {
bsw/jbe@1309 15005 for(var i = 0, max = events.length; i < max; i++) {
bsw/jbe@1309 15006 target.addEventListener(events[i], callback, false);
bsw/jbe@1309 15007 }
bsw/jbe@1309 15008 },
bsw/jbe@1309 15009
bsw/jbe@1309 15010 // Removes multiple eventlisteners from target, bound to one callback
bsw/jbe@1309 15011 // TODO: If needed elsewhere make it part of wysihtml.dom or sth
bsw/jbe@1309 15012 removeListeners: function (target, events, callback) {
bsw/jbe@1309 15013 for(var i = 0, max = events.length; i < max; i++) {
bsw/jbe@1309 15014 target.removeEventListener(events[i], callback, false);
bsw/jbe@1309 15015 }
bsw/jbe@1309 15016 },
bsw/jbe@1309 15017
bsw/jbe@1309 15018 // Override for giving user ability to delete last line break in table cell
bsw/jbe@1309 15019 fixLastBrDeletionInTable: function(composer, force) {
bsw/jbe@1309 15020 if (composer.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309 15021 var sel = composer.selection.getSelection(),
bsw/jbe@1309 15022 aNode = sel.anchorNode;
bsw/jbe@1309 15023 if (aNode && aNode.nodeType === 1 && (wysihtml.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) {
bsw/jbe@1309 15024 var nextNode = aNode.childNodes[sel.anchorOffset];
bsw/jbe@1309 15025 if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") {
bsw/jbe@1309 15026 nextNode.parentNode.removeChild(nextNode);
bsw/jbe@1309 15027 return true;
bsw/jbe@1309 15028 }
bsw/jbe@1309 15029 }
bsw/jbe@1309 15030 }
bsw/jbe@1309 15031 return false;
bsw/jbe@1309 15032 },
bsw/jbe@1309 15033
bsw/jbe@1309 15034 // If found an uneditable before caret then notify it before deletion
bsw/jbe@1309 15035 handleUneditableDeletion: function(composer) {
bsw/jbe@1309 15036 var before = composer.selection.getBeforeSelection(true);
bsw/jbe@1309 15037 if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) {
bsw/jbe@1309 15038 if (actions.fixLastBrDeletionInTable(composer, true)) {
bsw/jbe@1309 15039 return true;
bsw/jbe@1309 15040 }
bsw/jbe@1309 15041 try {
bsw/jbe@1309 15042 var ev = new CustomEvent("wysihtml:uneditable:delete", {bubbles: true, cancelable: false});
bsw/jbe@1309 15043 before.node.dispatchEvent(ev);
bsw/jbe@1309 15044 } catch (err) {}
bsw/jbe@1309 15045 before.node.parentNode.removeChild(before.node);
bsw/jbe@1309 15046 return true;
bsw/jbe@1309 15047 }
bsw/jbe@1309 15048 return false;
bsw/jbe@1309 15049 },
bsw/jbe@1309 15050
bsw/jbe@1309 15051 // Deletion with caret in the beginning of headings and other block elvel elements needs special attention
bsw/jbe@1309 15052 // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
bsw/jbe@1309 15053 fixDeleteInTheBeginningOfBlock: function(composer) {
bsw/jbe@1309 15054 var selection = composer.selection,
bsw/jbe@1309 15055 prevNode = selection.getPreviousNode();
bsw/jbe@1309 15056
bsw/jbe@1309 15057 if (selection.caretIsFirstInSelection(wysihtml.browser.usesControlRanges()) && prevNode) {
bsw/jbe@1309 15058 if (prevNode.nodeType === 1 &&
bsw/jbe@1309 15059 wysihtml.dom.domNode(prevNode).is.block() &&
bsw/jbe@1309 15060 !domNode(prevNode).test({
bsw/jbe@1309 15061 query: "ol, ul, table, tr, dl"
bsw/jbe@1309 15062 })
bsw/jbe@1309 15063 ) {
bsw/jbe@1309 15064 if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
bsw/jbe@1309 15065 // If heading is empty remove the heading node
bsw/jbe@1309 15066 prevNode.parentNode.removeChild(prevNode);
bsw/jbe@1309 15067 return true;
bsw/jbe@1309 15068 } else {
bsw/jbe@1309 15069 if (prevNode.lastChild) {
bsw/jbe@1309 15070 var selNode = prevNode.lastChild,
bsw/jbe@1309 15071 selectedNode = selection.getSelectedNode(),
bsw/jbe@1309 15072 commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element),
bsw/jbe@1309 15073 curNode = wysihtml.dom.getParentElement(selectedNode, {
bsw/jbe@1309 15074 query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"
bsw/jbe@1309 15075 }, false, commonAncestorNode || composer.element);
bsw/jbe@1309 15076
bsw/jbe@1309 15077 if (curNode) {
bsw/jbe@1309 15078 domNode(curNode).transferContentTo(prevNode, true);
bsw/jbe@1309 15079 selection.setAfter(selNode);
bsw/jbe@1309 15080 return true;
bsw/jbe@1309 15081 } else if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309 15082 selectedNode = selection.getCaretNode();
bsw/jbe@1309 15083 domNode(selectedNode).transferContentTo(prevNode, true);
bsw/jbe@1309 15084 selection.setAfter(selNode);
bsw/jbe@1309 15085 return true;
bsw/jbe@1309 15086 }
bsw/jbe@1309 15087 }
bsw/jbe@1309 15088 }
bsw/jbe@1309 15089 }
bsw/jbe@1309 15090 }
bsw/jbe@1309 15091 return false;
bsw/jbe@1309 15092 },
bsw/jbe@1309 15093
bsw/jbe@1309 15094 /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */
bsw/jbe@1309 15095 /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */
bsw/jbe@1309 15096 fixDeleteInTheBeginningOfLi: function(composer) {
bsw/jbe@1309 15097 if (wysihtml.browser.hasLiDeletingProblem()) {
bsw/jbe@1309 15098 var selection = composer.selection.getSelection(),
bsw/jbe@1309 15099 aNode = selection.anchorNode,
bsw/jbe@1309 15100 listNode, prevNode, firstNode,
bsw/jbe@1309 15101 isInBeginnig = composer.selection.caretIsFirstInSelection(),
bsw/jbe@1309 15102 prevNode,
bsw/jbe@1309 15103 intermediaryNode;
bsw/jbe@1309 15104
bsw/jbe@1309 15105 // Fix caret at the beginnig of first textNode in LI
bsw/jbe@1309 15106 if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) {
bsw/jbe@1309 15107 aNode = aNode.parentNode;
bsw/jbe@1309 15108 isInBeginnig = true;
bsw/jbe@1309 15109 }
bsw/jbe@1309 15110
bsw/jbe@1309 15111 if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") {
bsw/jbe@1309 15112 prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 15113 if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) {
bsw/jbe@1309 15114 prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
bsw/jbe@1309 15115 intermediaryNode = aNode.parentNode;
bsw/jbe@1309 15116 }
bsw/jbe@1309 15117 if (prevNode) {
bsw/jbe@1309 15118 firstNode = aNode.firstChild;
bsw/jbe@1309 15119 domNode(aNode).transferContentTo(prevNode, true);
bsw/jbe@1309 15120
bsw/jbe@1309 15121 if (intermediaryNode && intermediaryNode.children.length === 0){
bsw/jbe@1309 15122 intermediaryNode.remove();
bsw/jbe@1309 15123 }
bsw/jbe@1309 15124
bsw/jbe@1309 15125 if (firstNode) {
bsw/jbe@1309 15126 composer.selection.setBefore(firstNode);
bsw/jbe@1309 15127 } else if (prevNode) {
bsw/jbe@1309 15128 if (prevNode.nodeType === 1) {
bsw/jbe@1309 15129 if (prevNode.lastChild) {
bsw/jbe@1309 15130 composer.selection.setAfter(prevNode.lastChild);
bsw/jbe@1309 15131 } else {
bsw/jbe@1309 15132 composer.selection.selectNode(prevNode);
bsw/jbe@1309 15133 }
bsw/jbe@1309 15134 } else {
bsw/jbe@1309 15135 composer.selection.setAfter(prevNode);
bsw/jbe@1309 15136 }
bsw/jbe@1309 15137 }
bsw/jbe@1309 15138 return true;
bsw/jbe@1309 15139 }
bsw/jbe@1309 15140 }
bsw/jbe@1309 15141 }
bsw/jbe@1309 15142 return false;
bsw/jbe@1309 15143 },
bsw/jbe@1309 15144
bsw/jbe@1309 15145 fixDeleteInTheBeginningOfControlSelection: function(composer) {
bsw/jbe@1309 15146 var selection = composer.selection,
bsw/jbe@1309 15147 prevNode = selection.getPreviousNode(),
bsw/jbe@1309 15148 selectedNode = selection.getSelectedNode(),
bsw/jbe@1309 15149 afterCaretNode;
bsw/jbe@1309 15150
bsw/jbe@1309 15151 if (selection.caretIsFirstInSelection()) {
bsw/jbe@1309 15152 if (selectedNode.nodeType === 3) {
bsw/jbe@1309 15153 selectedNode = selectedNode.parentNode;
bsw/jbe@1309 15154 }
bsw/jbe@1309 15155 afterCaretNode = selectedNode.firstChild;
bsw/jbe@1309 15156 domNode(selectedNode).transferContentTo(prevNode, true);
bsw/jbe@1309 15157 if (afterCaretNode) {
bsw/jbe@1309 15158 composer.selection.setBefore(afterCaretNode);
bsw/jbe@1309 15159 }
bsw/jbe@1309 15160 return true;
bsw/jbe@1309 15161 }
bsw/jbe@1309 15162 return false;
bsw/jbe@1309 15163 },
bsw/jbe@1309 15164
bsw/jbe@1309 15165 // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
bsw/jbe@1309 15166 // Returns true if some corrections is applied so events know when to prevent default
bsw/jbe@1309 15167 doLineBreaksModeEnterWithCaret: function(composer) {
bsw/jbe@1309 15168 var breakNodes = "p, pre, div, blockquote",
bsw/jbe@1309 15169 caretInfo, parent, txtNode,
bsw/jbe@1309 15170 ret = false;
bsw/jbe@1309 15171
bsw/jbe@1309 15172 caretInfo = composer.selection.getNodesNearCaret();
bsw/jbe@1309 15173 if (caretInfo) {
bsw/jbe@1309 15174
bsw/jbe@1309 15175 if (caretInfo.caretNode || caretInfo.nextNode) {
bsw/jbe@1309 15176 parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2);
bsw/jbe@1309 15177 if (parent === composer.element) {
bsw/jbe@1309 15178 parent = undefined;
bsw/jbe@1309 15179 }
bsw/jbe@1309 15180 }
bsw/jbe@1309 15181
bsw/jbe@1309 15182 if (parent && caretInfo.caretNode) {
bsw/jbe@1309 15183 if (domNode(caretInfo.caretNode).is.lineBreak()) {
bsw/jbe@1309 15184
bsw/jbe@1309 15185 if (composer.config.doubleLineBreakEscapesBlock) {
bsw/jbe@1309 15186 // Double enter (enter on blank line) exits block element in useLineBreaks mode.
bsw/jbe@1309 15187 ret = true;
bsw/jbe@1309 15188 caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode);
bsw/jbe@1309 15189
bsw/jbe@1309 15190 // Ensure surplous line breaks are not added to preceding element
bsw/jbe@1309 15191 if (domNode(caretInfo.nextNode).is.lineBreak()) {
bsw/jbe@1309 15192 caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode);
bsw/jbe@1309 15193 }
bsw/jbe@1309 15194
bsw/jbe@1309 15195 var brNode = composer.doc.createElement('br');
bsw/jbe@1309 15196 if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) {
bsw/jbe@1309 15197 parent.parentNode.insertBefore(brNode, parent.nextSibling);
bsw/jbe@1309 15198 } else {
bsw/jbe@1309 15199 composer.selection.splitElementAtCaret(parent, brNode);
bsw/jbe@1309 15200 }
bsw/jbe@1309 15201
bsw/jbe@1309 15202 // Ensure surplous blank lines are not added to preceding element
bsw/jbe@1309 15203 if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) {
bsw/jbe@1309 15204 // Replaces blank lines at the beginning of textnode
bsw/jbe@1309 15205 caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, '');
bsw/jbe@1309 15206 }
bsw/jbe@1309 15207 composer.selection.setBefore(brNode);
bsw/jbe@1309 15208 }
bsw/jbe@1309 15209
bsw/jbe@1309 15210 } else if (caretInfo.caretNode.nodeType === 3 && wysihtml.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) {
bsw/jbe@1309 15211
bsw/jbe@1309 15212 // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens.
bsw/jbe@1309 15213 // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br
bsw/jbe@1309 15214 ret = true;
bsw/jbe@1309 15215 var br1 = composer.doc.createElement('br'),
bsw/jbe@1309 15216 br2 = composer.doc.createElement('br'),
bsw/jbe@1309 15217 f = composer.doc.createDocumentFragment();
bsw/jbe@1309 15218 f.appendChild(br1);
bsw/jbe@1309 15219 f.appendChild(br2);
bsw/jbe@1309 15220 composer.selection.insertNode(f);
bsw/jbe@1309 15221 composer.selection.setBefore(br2);
bsw/jbe@1309 15222
bsw/jbe@1309 15223 }
bsw/jbe@1309 15224 }
bsw/jbe@1309 15225 }
bsw/jbe@1309 15226 return ret;
bsw/jbe@1309 15227 }
bsw/jbe@1309 15228 };
bsw/jbe@1309 15229
bsw/jbe@1309 15230 var handleDeleteKeyPress = function(event, composer) {
bsw/jbe@1309 15231 var selection = composer.selection,
bsw/jbe@1309 15232 element = composer.element;
bsw/jbe@1309 15233
bsw/jbe@1309 15234 if (selection.isCollapsed()) {
bsw/jbe@1309 15235 /**
bsw/jbe@1309 15236 * when the editor is empty in useLineBreaks = false mode, preserve
bsw/jbe@1309 15237 * the default value in it which is <p><br></p>
bsw/jbe@1309 15238 */
bsw/jbe@1309 15239 if (composer.isEmpty() && !composer.config.useLineBreaks) {
bsw/jbe@1309 15240 event.preventDefault();
bsw/jbe@1309 15241 return;
bsw/jbe@1309 15242 }
bsw/jbe@1309 15243 if (actions.handleUneditableDeletion(composer)) {
bsw/jbe@1309 15244 event.preventDefault();
bsw/jbe@1309 15245 return;
bsw/jbe@1309 15246 }
bsw/jbe@1309 15247 if (actions.fixDeleteInTheBeginningOfLi(composer)) {
bsw/jbe@1309 15248 event.preventDefault();
bsw/jbe@1309 15249 return;
bsw/jbe@1309 15250 }
bsw/jbe@1309 15251 if (actions.fixDeleteInTheBeginningOfBlock(composer)) {
bsw/jbe@1309 15252 event.preventDefault();
bsw/jbe@1309 15253 return;
bsw/jbe@1309 15254 }
bsw/jbe@1309 15255 if (actions.fixLastBrDeletionInTable(composer)) {
bsw/jbe@1309 15256 event.preventDefault();
bsw/jbe@1309 15257 return;
bsw/jbe@1309 15258 }
bsw/jbe@1309 15259 if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309 15260 if (actions.fixDeleteInTheBeginningOfControlSelection(composer)) {
bsw/jbe@1309 15261 event.preventDefault();
bsw/jbe@1309 15262 return;
bsw/jbe@1309 15263 }
bsw/jbe@1309 15264 }
bsw/jbe@1309 15265 } else {
bsw/jbe@1309 15266 if (selection.containsUneditable()) {
bsw/jbe@1309 15267 event.preventDefault();
bsw/jbe@1309 15268 selection.deleteContents();
bsw/jbe@1309 15269 }
bsw/jbe@1309 15270 }
bsw/jbe@1309 15271 };
bsw/jbe@1309 15272
bsw/jbe@1309 15273 var handleEnterKeyPress = function(event, composer) {
bsw/jbe@1309 15274 if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) {
bsw/jbe@1309 15275 // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
bsw/jbe@1309 15276
bsw/jbe@1309 15277 var breakNodes = "p, pre, div, blockquote",
bsw/jbe@1309 15278 caretInfo, parent, txtNode;
bsw/jbe@1309 15279
bsw/jbe@1309 15280 if (composer.selection.isCollapsed()) {
bsw/jbe@1309 15281 if (actions.doLineBreaksModeEnterWithCaret(composer)) {
bsw/jbe@1309 15282 event.preventDefault();
bsw/jbe@1309 15283 }
bsw/jbe@1309 15284 }
bsw/jbe@1309 15285 }
bsw/jbe@1309 15286
bsw/jbe@1309 15287 if (browser.hasCaretAtLinkEndInsertionProblems() && composer.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309 15288 var target = composer.selection.getSelectedNode(true),
bsw/jbe@1309 15289 targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309 15290 invisibleSpace, space;
bsw/jbe@1309 15291
bsw/jbe@1309 15292 if (targetEl && targetEl.closest('a') && target.nodeType === 3 && target === targetEl.lastChild) {
bsw/jbe@1309 15293 // Seems like enter was pressed and caret was at the end of link node
bsw/jbe@1309 15294 // This means user wants to escape the link now (caret is last in link node too).
bsw/jbe@1309 15295 composer.selection.setAfter(targetEl);
bsw/jbe@1309 15296 }
bsw/jbe@1309 15297 }
bsw/jbe@1309 15298 };
bsw/jbe@1309 15299
bsw/jbe@1309 15300 var handleTabKeyDown = function(composer, element, shiftKey) {
bsw/jbe@1309 15301 if (!composer.selection.isCollapsed()) {
bsw/jbe@1309 15302 composer.selection.deleteContents();
bsw/jbe@1309 15303 } else if (composer.selection.caretIsInTheBeginnig('li')) {
bsw/jbe@1309 15304 if (shiftKey) {
bsw/jbe@1309 15305 if (composer.commands.exec('outdentList')) return;
bsw/jbe@1309 15306 } else {
bsw/jbe@1309 15307 if (composer.commands.exec('indentList')) return;
bsw/jbe@1309 15308 }
bsw/jbe@1309 15309 }
bsw/jbe@1309 15310
bsw/jbe@1309 15311 // Is &emsp; close enough to tab. Could not find enough counter arguments for now.
bsw/jbe@1309 15312 composer.commands.exec("insertHTML", "&emsp;");
bsw/jbe@1309 15313 };
bsw/jbe@1309 15314
bsw/jbe@1309 15315 var handleDomNodeRemoved = function(event) {
bsw/jbe@1309 15316 if (this.domNodeRemovedInterval) {
bsw/jbe@1309 15317 clearInterval(domNodeRemovedInterval);
bsw/jbe@1309 15318 }
bsw/jbe@1309 15319 this.parent.fire("destroy:composer");
bsw/jbe@1309 15320 };
bsw/jbe@1309 15321
bsw/jbe@1309 15322 // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires
bsw/jbe@1309 15323 var handleUserInteraction = function (event) {
bsw/jbe@1309 15324 this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event);
bsw/jbe@1309 15325 setTimeout((function() {
bsw/jbe@1309 15326 this.parent.fire("interaction", event).fire("interaction:composer", event);
bsw/jbe@1309 15327 }).bind(this), 0);
bsw/jbe@1309 15328 };
bsw/jbe@1309 15329
bsw/jbe@1309 15330 var handleFocus = function(event) {
bsw/jbe@1309 15331 this.parent.fire("focus", event).fire("focus:composer", event);
bsw/jbe@1309 15332
bsw/jbe@1309 15333 // Delay storing of state until all focus handler are fired
bsw/jbe@1309 15334 // especially the one which resets the placeholder
bsw/jbe@1309 15335 setTimeout((function() {
bsw/jbe@1309 15336 this.focusState = this.getValue(false, false);
bsw/jbe@1309 15337 }).bind(this), 0);
bsw/jbe@1309 15338 };
bsw/jbe@1309 15339
bsw/jbe@1309 15340 var handleBlur = function(event) {
bsw/jbe@1309 15341 if (this.focusState !== this.getValue(false, false)) {
bsw/jbe@1309 15342 //create change event if supported (all except IE8)
bsw/jbe@1309 15343 var changeevent = event;
bsw/jbe@1309 15344 if(typeof Object.create == 'function') {
bsw/jbe@1309 15345 changeevent = Object.create(event, { type: { value: 'change' } });
bsw/jbe@1309 15346 }
bsw/jbe@1309 15347 this.parent.fire("change", changeevent).fire("change:composer", changeevent);
bsw/jbe@1309 15348 }
bsw/jbe@1309 15349 this.parent.fire("blur", event).fire("blur:composer", event);
bsw/jbe@1309 15350 };
bsw/jbe@1309 15351
bsw/jbe@1309 15352 var handlePaste = function(event) {
bsw/jbe@1309 15353 this.parent.fire(event.type, event).fire(event.type + ":composer", event);
bsw/jbe@1309 15354 if (event.type === "paste") {
bsw/jbe@1309 15355 setTimeout((function() {
bsw/jbe@1309 15356 this.parent.fire("newword:composer");
bsw/jbe@1309 15357 }).bind(this), 0);
bsw/jbe@1309 15358 }
bsw/jbe@1309 15359 };
bsw/jbe@1309 15360
bsw/jbe@1309 15361 var handleCopy = function(event) {
bsw/jbe@1309 15362 if (this.config.copyedFromMarking) {
bsw/jbe@1309 15363 // If supported the copied source can be based directly on selection
bsw/jbe@1309 15364 // Very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection.
bsw/jbe@1309 15365 if (wysihtml.browser.supportsModernPaste()) {
bsw/jbe@1309 15366 event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml());
bsw/jbe@1309 15367 event.clipboardData.setData("text/plain", this.selection.getPlainText());
bsw/jbe@1309 15368 event.preventDefault();
bsw/jbe@1309 15369 }
bsw/jbe@1309 15370 this.parent.fire(event.type, event).fire(event.type + ":composer", event);
bsw/jbe@1309 15371 }
bsw/jbe@1309 15372 };
bsw/jbe@1309 15373
bsw/jbe@1309 15374 var handleKeyUp = function(event) {
bsw/jbe@1309 15375 var keyCode = event.keyCode;
bsw/jbe@1309 15376 if (keyCode === wysihtml.SPACE_KEY || keyCode === wysihtml.ENTER_KEY) {
bsw/jbe@1309 15377 this.parent.fire("newword:composer");
bsw/jbe@1309 15378 }
bsw/jbe@1309 15379 };
bsw/jbe@1309 15380
bsw/jbe@1309 15381 var handleMouseDown = function(event) {
bsw/jbe@1309 15382 if (!browser.canSelectImagesInContentEditable()) {
bsw/jbe@1309 15383 // Make sure that images are selected when clicking on them
bsw/jbe@1309 15384 var target = event.target,
bsw/jbe@1309 15385 allImages = this.element.querySelectorAll('img'),
bsw/jbe@1309 15386 notMyImages = this.element.querySelectorAll('.' + this.config.classNames.uneditableContainer + ' img'),
bsw/jbe@1309 15387 myImages = wysihtml.lang.array(allImages).without(notMyImages);
bsw/jbe@1309 15388
bsw/jbe@1309 15389 if (target.nodeName === "IMG" && wysihtml.lang.array(myImages).contains(target)) {
bsw/jbe@1309 15390 this.selection.selectNode(target);
bsw/jbe@1309 15391 }
bsw/jbe@1309 15392 }
bsw/jbe@1309 15393
bsw/jbe@1309 15394 // Saves mousedown position for IE controlSelect fix
bsw/jbe@1309 15395 if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309 15396 this.selection.lastMouseDownPos = {x: event.clientX, y: event.clientY};
bsw/jbe@1309 15397 setTimeout(function() {
bsw/jbe@1309 15398 delete this.selection.lastMouseDownPos;
bsw/jbe@1309 15399 }.bind(this), 0);
bsw/jbe@1309 15400 }
bsw/jbe@1309 15401 };
bsw/jbe@1309 15402
bsw/jbe@1309 15403 // IE has this madness of control selects of overflowed and some other elements (weird box around element on selection and second click selects text)
bsw/jbe@1309 15404 // This fix handles the second click problem by adding cursor to the right position under cursor inside when controlSelection is made
bsw/jbe@1309 15405 var handleIEControlSelect = function(event) {
bsw/jbe@1309 15406 var target = event.target,
bsw/jbe@1309 15407 pos = this.selection.lastMouseDownPos;
bsw/jbe@1309 15408 if (pos) {
bsw/jbe@1309 15409 var caretPosition = document.body.createTextRange();
bsw/jbe@1309 15410 setTimeout(function() {
bsw/jbe@1309 15411 try {
bsw/jbe@1309 15412 caretPosition.moveToPoint(pos.x, pos.y);
bsw/jbe@1309 15413 caretPosition.select();
bsw/jbe@1309 15414 } catch (e) {}
bsw/jbe@1309 15415 }.bind(this), 0);
bsw/jbe@1309 15416 }
bsw/jbe@1309 15417 };
bsw/jbe@1309 15418
bsw/jbe@1309 15419 var handleClick = function(event) {
bsw/jbe@1309 15420 if (this.config.classNames.uneditableContainer) {
bsw/jbe@1309 15421 // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text)
bsw/jbe@1309 15422 // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour
bsw/jbe@1309 15423 var uneditable = wysihtml.dom.getParentElement(event.target, { query: "." + this.config.classNames.uneditableContainer }, false, this.element);
bsw/jbe@1309 15424 if (uneditable) {
bsw/jbe@1309 15425 this.selection.setAfter(uneditable);
bsw/jbe@1309 15426 }
bsw/jbe@1309 15427 }
bsw/jbe@1309 15428 };
bsw/jbe@1309 15429
bsw/jbe@1309 15430 var handleDrop = function(event) {
bsw/jbe@1309 15431 if (!browser.canSelectImagesInContentEditable()) {
bsw/jbe@1309 15432 // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case
bsw/jbe@1309 15433 setTimeout((function() {
bsw/jbe@1309 15434 this.selection.getSelection().removeAllRanges();
bsw/jbe@1309 15435 }).bind(this), 0);
bsw/jbe@1309 15436 }
bsw/jbe@1309 15437 };
bsw/jbe@1309 15438
bsw/jbe@1309 15439 var handleKeyDown = function(event) {
bsw/jbe@1309 15440 var keyCode = event.keyCode,
bsw/jbe@1309 15441 command = shortcuts[keyCode],
bsw/jbe@1309 15442 target = this.selection.getSelectedNode(true),
bsw/jbe@1309 15443 targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309 15444 parent;
bsw/jbe@1309 15445
bsw/jbe@1309 15446 // Select all (meta/ctrl + a)
bsw/jbe@1309 15447 if ((event.ctrlKey || event.metaKey) && !event.altKey && keyCode === 65) {
bsw/jbe@1309 15448 this.selection.selectAll();
bsw/jbe@1309 15449 event.preventDefault();
bsw/jbe@1309 15450 return;
bsw/jbe@1309 15451 }
bsw/jbe@1309 15452
bsw/jbe@1309 15453 // Shortcut logic
bsw/jbe@1309 15454 if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
bsw/jbe@1309 15455 this.commands.exec(command);
bsw/jbe@1309 15456 event.preventDefault();
bsw/jbe@1309 15457 }
bsw/jbe@1309 15458
bsw/jbe@1309 15459 if (keyCode === wysihtml.BACKSPACE_KEY) {
bsw/jbe@1309 15460 // Delete key override for special cases
bsw/jbe@1309 15461 handleDeleteKeyPress(event, this);
bsw/jbe@1309 15462 }
bsw/jbe@1309 15463
bsw/jbe@1309 15464 // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor
bsw/jbe@1309 15465 if (keyCode === wysihtml.BACKSPACE_KEY || keyCode === wysihtml.DELETE_KEY) {
bsw/jbe@1309 15466 if (target && target.nodeName === "IMG") {
bsw/jbe@1309 15467 event.preventDefault();
bsw/jbe@1309 15468 parent = target.parentNode;
bsw/jbe@1309 15469 parent.removeChild(target);// delete the <img>
bsw/jbe@1309 15470 // And it's parent <a> too if it hasn't got any other child nodes
bsw/jbe@1309 15471 if (parent.nodeName === "A" && !parent.firstChild) {
bsw/jbe@1309 15472 parent.parentNode.removeChild(parent);
bsw/jbe@1309 15473 }
bsw/jbe@1309 15474 setTimeout((function() {
bsw/jbe@1309 15475 wysihtml.quirks.redraw(this.element);
bsw/jbe@1309 15476 }).bind(this), 0);
bsw/jbe@1309 15477 }
bsw/jbe@1309 15478 }
bsw/jbe@1309 15479
bsw/jbe@1309 15480 if (this.config.handleTabKey && keyCode === wysihtml.TAB_KEY) {
bsw/jbe@1309 15481 // TAB key handling
bsw/jbe@1309 15482 event.preventDefault();
bsw/jbe@1309 15483 handleTabKeyDown(this, this.element, event.shiftKey);
bsw/jbe@1309 15484 }
bsw/jbe@1309 15485
bsw/jbe@1309 15486 if (keyCode === wysihtml.ENTER_KEY) {
bsw/jbe@1309 15487 handleEnterKeyPress(event, this);
bsw/jbe@1309 15488 }
bsw/jbe@1309 15489
bsw/jbe@1309 15490 };
bsw/jbe@1309 15491
bsw/jbe@1309 15492 var handleKeyPress = function(event) {
bsw/jbe@1309 15493
bsw/jbe@1309 15494 // This block should run only if some character is inserted (nor command keys like delete, backspace, enter, etc.)
bsw/jbe@1309 15495 if (event.which !== 0) {
bsw/jbe@1309 15496
bsw/jbe@1309 15497 // Test if caret is last in a link in webkit and try to fix webkit problem,
bsw/jbe@1309 15498 // that all inserted content is added outside of link.
bsw/jbe@1309 15499 // This issue was added as a not thought through fix for getting caret after link in contenteditable if it is last in editable area.
bsw/jbe@1309 15500 // Allthough it fixes this minor case it actually introduces a cascade of problems when editing links.
bsw/jbe@1309 15501 // The standard approachi in other wysiwygs seems as a step backwards - introducing a separate modal for managing links content text.
bsw/jbe@1309 15502 // I find it to be too big of a tradeoff in terms of expected simple UI flow, thus trying to fight against it.
bsw/jbe@1309 15503 // Also adds link escaping by double space with caret at the end of link for all browsers
bsw/jbe@1309 15504
bsw/jbe@1309 15505 if (this.selection.caretIsInTheEndOfNode()) {
bsw/jbe@1309 15506 var target = this.selection.getSelectedNode(true),
bsw/jbe@1309 15507 targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
bsw/jbe@1309 15508 invisibleSpace, space;
bsw/jbe@1309 15509
bsw/jbe@1309 15510 if (targetEl && targetEl.closest('a') && target === targetEl.lastChild) {
bsw/jbe@1309 15511
bsw/jbe@1309 15512 if (event.which !== 32 || this.selection.caretIsInTheEndOfNode(true) && browser.hasCaretAtLinkEndInsertionProblems()) {
bsw/jbe@1309 15513 // Executed if there is no whitespace before caret in textnode in case of pressing space.
bsw/jbe@1309 15514 // Whitespace before marks that user wants to escape the node by pressing double space.
bsw/jbe@1309 15515 // Otherwise insert the character in the link not out as it would like to go natively
bsw/jbe@1309 15516
bsw/jbe@1309 15517 invisibleSpace = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
bsw/jbe@1309 15518 this.selection.insertNode(invisibleSpace);
bsw/jbe@1309 15519 this.selection.setBefore(invisibleSpace);
bsw/jbe@1309 15520 setTimeout(function() {
bsw/jbe@1309 15521
bsw/jbe@1309 15522 if (invisibleSpace.textContent.length > 1) {
bsw/jbe@1309 15523 invisibleSpace.textContent = invisibleSpace.textContent.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, '');
bsw/jbe@1309 15524 this.selection.setAfter(invisibleSpace);
bsw/jbe@1309 15525 } else {
bsw/jbe@1309 15526 invisibleSpace.remove();
bsw/jbe@1309 15527 }
bsw/jbe@1309 15528
bsw/jbe@1309 15529 }.bind(this), 0);
bsw/jbe@1309 15530 } else if (event.which === 32) {
bsw/jbe@1309 15531 // Seems like space was pressed and there was a space before the caret allready
bsw/jbe@1309 15532 // This means user wants to escape the link now (caret is last in link node too) so we let the native browser do it-s job and escape.
bsw/jbe@1309 15533 // But lets move the trailing space too out of link if present
bsw/jbe@1309 15534
bsw/jbe@1309 15535 if (target.nodeType === 3 && (/[\u00A0 ]$/).test(target.textContent)) {
bsw/jbe@1309 15536
bsw/jbe@1309 15537 target.textContent = target.textContent.replace(/[\u00A0 ]$/, '');
bsw/jbe@1309 15538 space = this.doc.createTextNode(' ');
bsw/jbe@1309 15539 targetEl.parentNode.insertBefore(space, targetEl.nextSibling);
bsw/jbe@1309 15540 this.selection.setAfter(space, false);
bsw/jbe@1309 15541 event.preventDefault();
bsw/jbe@1309 15542
bsw/jbe@1309 15543 }
bsw/jbe@1309 15544 }
bsw/jbe@1309 15545 }
bsw/jbe@1309 15546 }
bsw/jbe@1309 15547 }
bsw/jbe@1309 15548 }
bsw/jbe@1309 15549
bsw/jbe@1309 15550 var handleIframeFocus = function(event) {
bsw/jbe@1309 15551 setTimeout((function() {
bsw/jbe@1309 15552 if (this.doc.querySelector(":focus") !== this.element) {
bsw/jbe@1309 15553 this.focus();
bsw/jbe@1309 15554 }
bsw/jbe@1309 15555 }).bind(this), 0);
bsw/jbe@1309 15556 };
bsw/jbe@1309 15557
bsw/jbe@1309 15558 var handleIframeBlur = function(event) {
bsw/jbe@1309 15559 setTimeout((function() {
bsw/jbe@1309 15560 this.selection.getSelection().removeAllRanges();
bsw/jbe@1309 15561 }).bind(this), 0);
bsw/jbe@1309 15562 };
bsw/jbe@1309 15563
bsw/jbe@1309 15564 // Testing requires actions to be accessible from out of scope
bsw/jbe@1309 15565 wysihtml.views.Composer.prototype.observeActions = actions;
bsw/jbe@1309 15566
bsw/jbe@1309 15567 wysihtml.views.Composer.prototype.observe = function() {
bsw/jbe@1309 15568 var that = this,
bsw/jbe@1309 15569 container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
bsw/jbe@1309 15570 element = this.element,
bsw/jbe@1309 15571 focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow();
bsw/jbe@1309 15572
bsw/jbe@1309 15573 this.focusState = this.getValue(false, false);
bsw/jbe@1309 15574 this.actions = actions;
bsw/jbe@1309 15575
bsw/jbe@1309 15576 // --------- destroy:composer event ---------
bsw/jbe@1309 15577 container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false);
bsw/jbe@1309 15578
bsw/jbe@1309 15579 // DOMNodeRemoved event is not supported in IE 8
bsw/jbe@1309 15580 // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed
bsw/jbe@1309 15581 if (!browser.supportsMutationEvents()) {
bsw/jbe@1309 15582 this.domNodeRemovedInterval = setInterval(function() {
bsw/jbe@1309 15583 if (!dom.contains(document.documentElement, container)) {
bsw/jbe@1309 15584 handleDomNodeRemoved.call(this);
bsw/jbe@1309 15585 }
bsw/jbe@1309 15586 }, 250);
bsw/jbe@1309 15587 }
bsw/jbe@1309 15588
bsw/jbe@1309 15589 actions.addListeners(focusBlurElement, ['drop', 'paste', 'mouseup', 'focus', 'keyup'], handleUserInteraction.bind(this));
bsw/jbe@1309 15590 focusBlurElement.addEventListener('focus', handleFocus.bind(this), false);
bsw/jbe@1309 15591 focusBlurElement.addEventListener('blur', handleBlur.bind(this), false);
bsw/jbe@1309 15592
bsw/jbe@1309 15593 actions.addListeners(this.element, ['drop', 'paste', 'beforepaste'], handlePaste.bind(this), false);
bsw/jbe@1309 15594 this.element.addEventListener('copy', handleCopy.bind(this), false);
bsw/jbe@1309 15595 this.element.addEventListener('mousedown', handleMouseDown.bind(this), false);
bsw/jbe@1309 15596 this.element.addEventListener('click', handleClick.bind(this), false);
bsw/jbe@1309 15597 this.element.addEventListener('drop', handleDrop.bind(this), false);
bsw/jbe@1309 15598 this.element.addEventListener('keyup', handleKeyUp.bind(this), false);
bsw/jbe@1309 15599 this.element.addEventListener('keydown', handleKeyDown.bind(this), false);
bsw/jbe@1309 15600 this.element.addEventListener('keypress', handleKeyPress.bind(this), false);
bsw/jbe@1309 15601
bsw/jbe@1309 15602 // IE controlselect madness fix
bsw/jbe@1309 15603 if (wysihtml.browser.usesControlRanges()) {
bsw/jbe@1309 15604 this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false);
bsw/jbe@1309 15605 }
bsw/jbe@1309 15606
bsw/jbe@1309 15607 this.element.addEventListener("dragenter", (function() {
bsw/jbe@1309 15608 this.parent.fire("unset_placeholder");
bsw/jbe@1309 15609 }).bind(this), false);
bsw/jbe@1309 15610
bsw/jbe@1309 15611 };
bsw/jbe@1309 15612 })(wysihtml);
bsw/jbe@1309 15613
bsw/jbe@1309 15614 /**
bsw/jbe@1309 15615 * Class that takes care that the value of the composer and the textarea is always in sync
bsw/jbe@1309 15616 */
bsw/jbe@1309 15617 (function(wysihtml) {
bsw/jbe@1309 15618 var INTERVAL = 400;
bsw/jbe@1309 15619
bsw/jbe@1309 15620 wysihtml.views.Synchronizer = Base.extend(
bsw/jbe@1309 15621 /** @scope wysihtml.views.Synchronizer.prototype */ {
bsw/jbe@1309 15622
bsw/jbe@1309 15623 constructor: function(editor, textarea, composer) {
bsw/jbe@1309 15624 this.editor = editor;
bsw/jbe@1309 15625 this.textarea = textarea;
bsw/jbe@1309 15626 this.composer = composer;
bsw/jbe@1309 15627
bsw/jbe@1309 15628 this._observe();
bsw/jbe@1309 15629 },
bsw/jbe@1309 15630
bsw/jbe@1309 15631 /**
bsw/jbe@1309 15632 * Sync html from composer to textarea
bsw/jbe@1309 15633 * Takes care of placeholders
bsw/jbe@1309 15634 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
bsw/jbe@1309 15635 */
bsw/jbe@1309 15636 fromComposerToTextarea: function(shouldParseHtml) {
bsw/jbe@1309 15637 this.textarea.setValue(wysihtml.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
bsw/jbe@1309 15638 },
bsw/jbe@1309 15639
bsw/jbe@1309 15640 /**
bsw/jbe@1309 15641 * Sync value of textarea to composer
bsw/jbe@1309 15642 * Takes care of placeholders
bsw/jbe@1309 15643 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
bsw/jbe@1309 15644 */
bsw/jbe@1309 15645 fromTextareaToComposer: function(shouldParseHtml) {
bsw/jbe@1309 15646 var textareaValue = this.textarea.getValue(false, false);
bsw/jbe@1309 15647 if (textareaValue) {
bsw/jbe@1309 15648 this.composer.setValue(textareaValue, shouldParseHtml);
bsw/jbe@1309 15649 } else {
bsw/jbe@1309 15650 this.composer.clear();
bsw/jbe@1309 15651 this.editor.fire("set_placeholder");
bsw/jbe@1309 15652 }
bsw/jbe@1309 15653 },
bsw/jbe@1309 15654
bsw/jbe@1309 15655 /**
bsw/jbe@1309 15656 * Invoke syncing based on view state
bsw/jbe@1309 15657 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
bsw/jbe@1309 15658 */
bsw/jbe@1309 15659 sync: function(shouldParseHtml) {
bsw/jbe@1309 15660 if (this.editor.currentView.name === "textarea") {
bsw/jbe@1309 15661 this.fromTextareaToComposer(shouldParseHtml);
bsw/jbe@1309 15662 } else {
bsw/jbe@1309 15663 this.fromComposerToTextarea(shouldParseHtml);
bsw/jbe@1309 15664 }
bsw/jbe@1309 15665 },
bsw/jbe@1309 15666
bsw/jbe@1309 15667 /**
bsw/jbe@1309 15668 * Initializes interval-based syncing
bsw/jbe@1309 15669 * also makes sure that on-submit the composer's content is synced with the textarea
bsw/jbe@1309 15670 * immediately when the form gets submitted
bsw/jbe@1309 15671 */
bsw/jbe@1309 15672 _observe: function() {
bsw/jbe@1309 15673 var interval,
bsw/jbe@1309 15674 that = this,
bsw/jbe@1309 15675 form = this.textarea.element.form,
bsw/jbe@1309 15676 startInterval = function() {
bsw/jbe@1309 15677 interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
bsw/jbe@1309 15678 },
bsw/jbe@1309 15679 stopInterval = function() {
bsw/jbe@1309 15680 clearInterval(interval);
bsw/jbe@1309 15681 interval = null;
bsw/jbe@1309 15682 };
bsw/jbe@1309 15683
bsw/jbe@1309 15684 startInterval();
bsw/jbe@1309 15685
bsw/jbe@1309 15686 if (form) {
bsw/jbe@1309 15687 // If the textarea is in a form make sure that after onreset and onsubmit the composer
bsw/jbe@1309 15688 // has the correct state
bsw/jbe@1309 15689 wysihtml.dom.observe(form, "submit", function() {
bsw/jbe@1309 15690 that.sync(true);
bsw/jbe@1309 15691 });
bsw/jbe@1309 15692 wysihtml.dom.observe(form, "reset", function() {
bsw/jbe@1309 15693 setTimeout(function() { that.fromTextareaToComposer(); }, 0);
bsw/jbe@1309 15694 });
bsw/jbe@1309 15695 }
bsw/jbe@1309 15696
bsw/jbe@1309 15697 this.editor.on("change_view", function(view) {
bsw/jbe@1309 15698 if (view === "composer" && !interval) {
bsw/jbe@1309 15699 that.fromTextareaToComposer(true);
bsw/jbe@1309 15700 startInterval();
bsw/jbe@1309 15701 } else if (view === "textarea") {
bsw/jbe@1309 15702 that.fromComposerToTextarea(true);
bsw/jbe@1309 15703 stopInterval();
bsw/jbe@1309 15704 }
bsw/jbe@1309 15705 });
bsw/jbe@1309 15706
bsw/jbe@1309 15707 this.editor.on("destroy:composer", stopInterval);
bsw/jbe@1309 15708 }
bsw/jbe@1309 15709 });
bsw/jbe@1309 15710 })(wysihtml);
bsw/jbe@1309 15711
bsw/jbe@1309 15712 (function(wysihtml) {
bsw/jbe@1309 15713
bsw/jbe@1309 15714 wysihtml.views.SourceView = Base.extend(
bsw/jbe@1309 15715 /** @scope wysihtml.views.SourceView.prototype */ {
bsw/jbe@1309 15716
bsw/jbe@1309 15717 constructor: function(editor, composer) {
bsw/jbe@1309 15718 this.editor = editor;
bsw/jbe@1309 15719 this.composer = composer;
bsw/jbe@1309 15720
bsw/jbe@1309 15721 this._observe();
bsw/jbe@1309 15722 },
bsw/jbe@1309 15723
bsw/jbe@1309 15724 switchToTextarea: function(shouldParseHtml) {
bsw/jbe@1309 15725 var composerStyles = this.composer.win.getComputedStyle(this.composer.element),
bsw/jbe@1309 15726 width = parseFloat(composerStyles.width),
bsw/jbe@1309 15727 height = Math.max(parseFloat(composerStyles.height), 100);
bsw/jbe@1309 15728
bsw/jbe@1309 15729 if (!this.textarea) {
bsw/jbe@1309 15730 this.textarea = this.composer.doc.createElement('textarea');
bsw/jbe@1309 15731 this.textarea.className = "wysihtml-source-view";
bsw/jbe@1309 15732 }
bsw/jbe@1309 15733 this.textarea.style.width = width + 'px';
bsw/jbe@1309 15734 this.textarea.style.height = height + 'px';
bsw/jbe@1309 15735 this.textarea.value = this.editor.getValue(shouldParseHtml, true);
bsw/jbe@1309 15736 this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element);
bsw/jbe@1309 15737 this.editor.currentView = "source";
bsw/jbe@1309 15738 this.composer.element.style.display = 'none';
bsw/jbe@1309 15739 },
bsw/jbe@1309 15740
bsw/jbe@1309 15741 switchToComposer: function(shouldParseHtml) {
bsw/jbe@1309 15742 var textareaValue = this.textarea.value;
bsw/jbe@1309 15743 if (textareaValue) {
bsw/jbe@1309 15744 this.composer.setValue(textareaValue, shouldParseHtml);
bsw/jbe@1309 15745 } else {
bsw/jbe@1309 15746 this.composer.clear();
bsw/jbe@1309 15747 this.editor.fire("set_placeholder");
bsw/jbe@1309 15748 }
bsw/jbe@1309 15749 this.textarea.parentNode.removeChild(this.textarea);
bsw/jbe@1309 15750 this.editor.currentView = this.composer;
bsw/jbe@1309 15751 this.composer.element.style.display = '';
bsw/jbe@1309 15752 },
bsw/jbe@1309 15753
bsw/jbe@1309 15754 _observe: function() {
bsw/jbe@1309 15755 this.editor.on("change_view", function(view) {
bsw/jbe@1309 15756 if (view === "composer") {
bsw/jbe@1309 15757 this.switchToComposer(true);
bsw/jbe@1309 15758 } else if (view === "textarea") {
bsw/jbe@1309 15759 this.switchToTextarea(true);
bsw/jbe@1309 15760 }
bsw/jbe@1309 15761 }.bind(this));
bsw/jbe@1309 15762 }
bsw/jbe@1309 15763
bsw/jbe@1309 15764 });
bsw/jbe@1309 15765
bsw/jbe@1309 15766 })(wysihtml);
bsw/jbe@1309 15767
bsw/jbe@1309 15768 wysihtml.views.Textarea = wysihtml.views.View.extend(
bsw/jbe@1309 15769 /** @scope wysihtml.views.Textarea.prototype */ {
bsw/jbe@1309 15770 name: "textarea",
bsw/jbe@1309 15771
bsw/jbe@1309 15772 constructor: function(parent, textareaElement, config) {
bsw/jbe@1309 15773 this.base(parent, textareaElement, config);
bsw/jbe@1309 15774
bsw/jbe@1309 15775 this._observe();
bsw/jbe@1309 15776 },
bsw/jbe@1309 15777
bsw/jbe@1309 15778 clear: function() {
bsw/jbe@1309 15779 this.element.value = "";
bsw/jbe@1309 15780 },
bsw/jbe@1309 15781
bsw/jbe@1309 15782 getValue: function(parse) {
bsw/jbe@1309 15783 var value = this.isEmpty() ? "" : this.element.value;
bsw/jbe@1309 15784 if (parse !== false) {
bsw/jbe@1309 15785 value = this.parent.parse(value);
bsw/jbe@1309 15786 }
bsw/jbe@1309 15787 return value;
bsw/jbe@1309 15788 },
bsw/jbe@1309 15789
bsw/jbe@1309 15790 setValue: function(html, parse) {
bsw/jbe@1309 15791 if (parse !== false) {
bsw/jbe@1309 15792 html = this.parent.parse(html);
bsw/jbe@1309 15793 }
bsw/jbe@1309 15794 this.element.value = html;
bsw/jbe@1309 15795 },
bsw/jbe@1309 15796
bsw/jbe@1309 15797 cleanUp: function(rules) {
bsw/jbe@1309 15798 var html = this.parent.parse(this.element.value, undefined, rules);
bsw/jbe@1309 15799 this.element.value = html;
bsw/jbe@1309 15800 },
bsw/jbe@1309 15801
bsw/jbe@1309 15802 hasPlaceholderSet: function() {
bsw/jbe@1309 15803 var supportsPlaceholder = wysihtml.browser.supportsPlaceholderAttributeOn(this.element),
bsw/jbe@1309 15804 placeholderText = this.element.getAttribute("placeholder") || null,
bsw/jbe@1309 15805 value = this.element.value,
bsw/jbe@1309 15806 isEmpty = !value;
bsw/jbe@1309 15807 return (supportsPlaceholder && isEmpty) || (value === placeholderText);
bsw/jbe@1309 15808 },
bsw/jbe@1309 15809
bsw/jbe@1309 15810 isEmpty: function() {
bsw/jbe@1309 15811 return !wysihtml.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
bsw/jbe@1309 15812 },
bsw/jbe@1309 15813
bsw/jbe@1309 15814 _observe: function() {
bsw/jbe@1309 15815 var element = this.element,
bsw/jbe@1309 15816 parent = this.parent,
bsw/jbe@1309 15817 eventMapping = {
bsw/jbe@1309 15818 focusin: "focus",
bsw/jbe@1309 15819 focusout: "blur"
bsw/jbe@1309 15820 },
bsw/jbe@1309 15821 /**
bsw/jbe@1309 15822 * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
bsw/jbe@1309 15823 * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
bsw/jbe@1309 15824 */
bsw/jbe@1309 15825 events = wysihtml.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
bsw/jbe@1309 15826
bsw/jbe@1309 15827 parent.on("beforeload", function() {
bsw/jbe@1309 15828 wysihtml.dom.observe(element, events, function(event) {
bsw/jbe@1309 15829 var eventName = eventMapping[event.type] || event.type;
bsw/jbe@1309 15830 parent.fire(eventName).fire(eventName + ":textarea");
bsw/jbe@1309 15831 });
bsw/jbe@1309 15832
bsw/jbe@1309 15833 wysihtml.dom.observe(element, ["paste", "drop"], function() {
bsw/jbe@1309 15834 setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
bsw/jbe@1309 15835 });
bsw/jbe@1309 15836 });
bsw/jbe@1309 15837 }
bsw/jbe@1309 15838 });
bsw/jbe@1309 15839
bsw/jbe@1309 15840 /**
bsw/jbe@1309 15841 * WYSIHTML Editor
bsw/jbe@1309 15842 *
bsw/jbe@1309 15843 * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
bsw/jbe@1309 15844 * @param {Object} [config] See defaults object below for explanation of each individual config option
bsw/jbe@1309 15845 *
bsw/jbe@1309 15846 * @events
bsw/jbe@1309 15847 * load
bsw/jbe@1309 15848 * beforeload (for internal use only)
bsw/jbe@1309 15849 * focus
bsw/jbe@1309 15850 * focus:composer
bsw/jbe@1309 15851 * focus:textarea
bsw/jbe@1309 15852 * blur
bsw/jbe@1309 15853 * blur:composer
bsw/jbe@1309 15854 * blur:textarea
bsw/jbe@1309 15855 * change
bsw/jbe@1309 15856 * change:composer
bsw/jbe@1309 15857 * change:textarea
bsw/jbe@1309 15858 * paste
bsw/jbe@1309 15859 * paste:composer
bsw/jbe@1309 15860 * paste:textarea
bsw/jbe@1309 15861 * newword:composer
bsw/jbe@1309 15862 * destroy:composer
bsw/jbe@1309 15863 * undo:composer
bsw/jbe@1309 15864 * redo:composer
bsw/jbe@1309 15865 * beforecommand:composer
bsw/jbe@1309 15866 * aftercommand:composer
bsw/jbe@1309 15867 * enable:composer
bsw/jbe@1309 15868 * disable:composer
bsw/jbe@1309 15869 * change_view
bsw/jbe@1309 15870 */
bsw/jbe@1309 15871 (function(wysihtml) {
bsw/jbe@1309 15872 var undef;
bsw/jbe@1309 15873
bsw/jbe@1309 15874 wysihtml.Editor = wysihtml.lang.Dispatcher.extend({
bsw/jbe@1309 15875 /** @scope wysihtml.Editor.prototype */
bsw/jbe@1309 15876 defaults: {
bsw/jbe@1309 15877 // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
bsw/jbe@1309 15878 name: undef,
bsw/jbe@1309 15879 // Whether the editor should look like the textarea (by adopting styles)
bsw/jbe@1309 15880 style: true,
bsw/jbe@1309 15881 // Whether urls, entered by the user should automatically become clickable-links
bsw/jbe@1309 15882 autoLink: true,
bsw/jbe@1309 15883 // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
bsw/jbe@1309 15884 handleTabKey: true,
bsw/jbe@1309 15885 // Object which includes parser rules to apply when html gets cleaned
bsw/jbe@1309 15886 // See parser_rules/*.js for examples
bsw/jbe@1309 15887 parserRules: { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} },
bsw/jbe@1309 15888 // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
bsw/jbe@1309 15889 pasteParserRulesets: null,
bsw/jbe@1309 15890 // Parser method to use when the user inserts content
bsw/jbe@1309 15891 parser: wysihtml.dom.parse,
bsw/jbe@1309 15892 // By default wysihtml will insert a <br> for line breaks, set this to false to use <p>
bsw/jbe@1309 15893 useLineBreaks: true,
bsw/jbe@1309 15894 // Double enter (enter on blank line) exits block element in useLineBreaks mode.
bsw/jbe@1309 15895 // It enables a way of escaping out of block elements and splitting block elements
bsw/jbe@1309 15896 doubleLineBreakEscapesBlock: true,
bsw/jbe@1309 15897 // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
bsw/jbe@1309 15898 stylesheets: [],
bsw/jbe@1309 15899 // Placeholder text to use, defaults to the placeholder attribute on the textarea element
bsw/jbe@1309 15900 placeholderText: undef,
bsw/jbe@1309 15901 // Whether the rich text editor should be rendered on touch devices (wysihtml >= 0.3.0 comes with basic support for iOS 5)
bsw/jbe@1309 15902 supportTouchDevices: true,
bsw/jbe@1309 15903 // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content
bsw/jbe@1309 15904 cleanUp: true,
bsw/jbe@1309 15905 // Whether to use div instead of secure iframe
bsw/jbe@1309 15906 contentEditableMode: false,
bsw/jbe@1309 15907 classNames: {
bsw/jbe@1309 15908 // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
bsw/jbe@1309 15909 composer: "wysihtml-editor",
bsw/jbe@1309 15910 // Class name to add to the body when the wysihtml editor is supported
bsw/jbe@1309 15911 body: "wysihtml-supported",
bsw/jbe@1309 15912 // classname added to editable area element (iframe/div) on creation
bsw/jbe@1309 15913 sandbox: "wysihtml-sandbox",
bsw/jbe@1309 15914 // class on editable area with placeholder
bsw/jbe@1309 15915 placeholder: "wysihtml-placeholder",
bsw/jbe@1309 15916 // Classname of container that editor should not touch and pass through
bsw/jbe@1309 15917 uneditableContainer: "wysihtml-uneditable-container"
bsw/jbe@1309 15918 },
bsw/jbe@1309 15919 // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste)
bsw/jbe@1309 15920 // Also copied source is based directly on selection -
bsw/jbe@1309 15921 // (very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection).
bsw/jbe@1309 15922 // If falsy value is passed source override is also disabled
bsw/jbe@1309 15923 copyedFromMarking: '<meta name="copied-from" content="wysihtml">'
bsw/jbe@1309 15924 },
bsw/jbe@1309 15925
bsw/jbe@1309 15926 constructor: function(editableElement, config) {
bsw/jbe@1309 15927 this.editableElement = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement;
bsw/jbe@1309 15928 this.config = wysihtml.lang.object({}).merge(this.defaults).merge(config).get();
bsw/jbe@1309 15929 this._isCompatible = wysihtml.browser.supported();
bsw/jbe@1309 15930
bsw/jbe@1309 15931 // merge classNames
bsw/jbe@1309 15932 if (config && config.classNames) {
bsw/jbe@1309 15933 wysihtml.lang.object(this.config.classNames).merge(config.classNames);
bsw/jbe@1309 15934 }
bsw/jbe@1309 15935
bsw/jbe@1309 15936 if (this.editableElement.nodeName.toLowerCase() != "textarea") {
bsw/jbe@1309 15937 this.config.contentEditableMode = true;
bsw/jbe@1309 15938 this.config.noTextarea = true;
bsw/jbe@1309 15939 }
bsw/jbe@1309 15940 if (!this.config.noTextarea) {
bsw/jbe@1309 15941 this.textarea = new wysihtml.views.Textarea(this, this.editableElement, this.config);
bsw/jbe@1309 15942 this.currentView = this.textarea;
bsw/jbe@1309 15943 }
bsw/jbe@1309 15944
bsw/jbe@1309 15945 // Sort out unsupported/unwanted browsers here
bsw/jbe@1309 15946 if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml.browser.isTouchDevice())) {
bsw/jbe@1309 15947 var that = this;
bsw/jbe@1309 15948 setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
bsw/jbe@1309 15949 return;
bsw/jbe@1309 15950 }
bsw/jbe@1309 15951
bsw/jbe@1309 15952 // Add class name to body, to indicate that the editor is supported
bsw/jbe@1309 15953 wysihtml.dom.addClass(document.body, this.config.classNames.body);
bsw/jbe@1309 15954
bsw/jbe@1309 15955 this.composer = new wysihtml.views.Composer(this, this.editableElement, this.config);
bsw/jbe@1309 15956 this.currentView = this.composer;
bsw/jbe@1309 15957
bsw/jbe@1309 15958 if (typeof(this.config.parser) === "function") {
bsw/jbe@1309 15959 this._initParser();
bsw/jbe@1309 15960 }
bsw/jbe@1309 15961
bsw/jbe@1309 15962 this.on("beforeload", this.handleBeforeLoad);
bsw/jbe@1309 15963 },
bsw/jbe@1309 15964
bsw/jbe@1309 15965 handleBeforeLoad: function() {
bsw/jbe@1309 15966 if (!this.config.noTextarea) {
bsw/jbe@1309 15967 this.synchronizer = new wysihtml.views.Synchronizer(this, this.textarea, this.composer);
bsw/jbe@1309 15968 } else {
bsw/jbe@1309 15969 this.sourceView = new wysihtml.views.SourceView(this, this.composer);
bsw/jbe@1309 15970 }
bsw/jbe@1309 15971 this.runEditorExtenders();
bsw/jbe@1309 15972 },
bsw/jbe@1309 15973
bsw/jbe@1309 15974 runEditorExtenders: function() {
bsw/jbe@1309 15975 wysihtml.editorExtenders.forEach(function(extender) {
bsw/jbe@1309 15976 extender(this);
bsw/jbe@1309 15977 }.bind(this));
bsw/jbe@1309 15978 },
bsw/jbe@1309 15979
bsw/jbe@1309 15980 isCompatible: function() {
bsw/jbe@1309 15981 return this._isCompatible;
bsw/jbe@1309 15982 },
bsw/jbe@1309 15983
bsw/jbe@1309 15984 clear: function() {
bsw/jbe@1309 15985 this.currentView.clear();
bsw/jbe@1309 15986 return this;
bsw/jbe@1309 15987 },
bsw/jbe@1309 15988
bsw/jbe@1309 15989 getValue: function(parse, clearInternals) {
bsw/jbe@1309 15990 return this.currentView.getValue(parse, clearInternals);
bsw/jbe@1309 15991 },
bsw/jbe@1309 15992
bsw/jbe@1309 15993 setValue: function(html, parse) {
bsw/jbe@1309 15994 this.fire("unset_placeholder");
bsw/jbe@1309 15995
bsw/jbe@1309 15996 if (!html) {
bsw/jbe@1309 15997 return this.clear();
bsw/jbe@1309 15998 }
bsw/jbe@1309 15999
bsw/jbe@1309 16000 this.currentView.setValue(html, parse);
bsw/jbe@1309 16001 return this;
bsw/jbe@1309 16002 },
bsw/jbe@1309 16003
bsw/jbe@1309 16004 cleanUp: function(rules) {
bsw/jbe@1309 16005 this.currentView.cleanUp(rules);
bsw/jbe@1309 16006 },
bsw/jbe@1309 16007
bsw/jbe@1309 16008 focus: function(setToEnd) {
bsw/jbe@1309 16009 this.currentView.focus(setToEnd);
bsw/jbe@1309 16010 return this;
bsw/jbe@1309 16011 },
bsw/jbe@1309 16012
bsw/jbe@1309 16013 /**
bsw/jbe@1309 16014 * Deactivate editor (make it readonly)
bsw/jbe@1309 16015 */
bsw/jbe@1309 16016 disable: function() {
bsw/jbe@1309 16017 this.currentView.disable();
bsw/jbe@1309 16018 return this;
bsw/jbe@1309 16019 },
bsw/jbe@1309 16020
bsw/jbe@1309 16021 /**
bsw/jbe@1309 16022 * Activate editor
bsw/jbe@1309 16023 */
bsw/jbe@1309 16024 enable: function() {
bsw/jbe@1309 16025 this.currentView.enable();
bsw/jbe@1309 16026 return this;
bsw/jbe@1309 16027 },
bsw/jbe@1309 16028
bsw/jbe@1309 16029 isEmpty: function() {
bsw/jbe@1309 16030 return this.currentView.isEmpty();
bsw/jbe@1309 16031 },
bsw/jbe@1309 16032
bsw/jbe@1309 16033 hasPlaceholderSet: function() {
bsw/jbe@1309 16034 return this.currentView.hasPlaceholderSet();
bsw/jbe@1309 16035 },
bsw/jbe@1309 16036
bsw/jbe@1309 16037 destroy: function() {
bsw/jbe@1309 16038 if (this.composer && this.composer.sandbox) {
bsw/jbe@1309 16039 this.composer.sandbox.destroy();
bsw/jbe@1309 16040 }
bsw/jbe@1309 16041 this.fire("destroy:composer");
bsw/jbe@1309 16042 this.off();
bsw/jbe@1309 16043 },
bsw/jbe@1309 16044
bsw/jbe@1309 16045 parse: function(htmlOrElement, clearInternals, customRules) {
bsw/jbe@1309 16046 var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null);
bsw/jbe@1309 16047 var returnValue = this.config.parser(htmlOrElement, {
bsw/jbe@1309 16048 "rules": customRules || this.config.parserRules,
bsw/jbe@1309 16049 "cleanUp": this.config.cleanUp,
bsw/jbe@1309 16050 "context": parseContext,
bsw/jbe@1309 16051 "uneditableClass": this.config.classNames.uneditableContainer,
bsw/jbe@1309 16052 "clearInternals" : clearInternals
bsw/jbe@1309 16053 });
bsw/jbe@1309 16054 if (typeof(htmlOrElement) === "object") {
bsw/jbe@1309 16055 wysihtml.quirks.redraw(htmlOrElement);
bsw/jbe@1309 16056 }
bsw/jbe@1309 16057 return returnValue;
bsw/jbe@1309 16058 },
bsw/jbe@1309 16059
bsw/jbe@1309 16060 /**
bsw/jbe@1309 16061 * Prepare html parser logic
bsw/jbe@1309 16062 * - Observes for paste and drop
bsw/jbe@1309 16063 */
bsw/jbe@1309 16064 _initParser: function() {
bsw/jbe@1309 16065 var oldHtml;
bsw/jbe@1309 16066
bsw/jbe@1309 16067 if (wysihtml.browser.supportsModernPaste()) {
bsw/jbe@1309 16068 this.on("paste:composer", function(event) {
bsw/jbe@1309 16069 event.preventDefault();
bsw/jbe@1309 16070 oldHtml = wysihtml.dom.getPastedHtml(event);
bsw/jbe@1309 16071 if (oldHtml) {
bsw/jbe@1309 16072 this._cleanAndPaste(oldHtml);
bsw/jbe@1309 16073 }
bsw/jbe@1309 16074 }.bind(this));
bsw/jbe@1309 16075
bsw/jbe@1309 16076 } else {
bsw/jbe@1309 16077 this.on("beforepaste:composer", function(event) {
bsw/jbe@1309 16078 event.preventDefault();
bsw/jbe@1309 16079 var scrollPos = this.composer.getScrollPos();
bsw/jbe@1309 16080
bsw/jbe@1309 16081 wysihtml.dom.getPastedHtmlWithDiv(this.composer, function(pastedHTML) {
bsw/jbe@1309 16082 if (pastedHTML) {
bsw/jbe@1309 16083 this._cleanAndPaste(pastedHTML);
bsw/jbe@1309 16084 }
bsw/jbe@1309 16085 this.composer.setScrollPos(scrollPos);
bsw/jbe@1309 16086 }.bind(this));
bsw/jbe@1309 16087
bsw/jbe@1309 16088 }.bind(this));
bsw/jbe@1309 16089 }
bsw/jbe@1309 16090 },
bsw/jbe@1309 16091
bsw/jbe@1309 16092 _cleanAndPaste: function (oldHtml) {
bsw/jbe@1309 16093 var cleanHtml = wysihtml.quirks.cleanPastedHTML(oldHtml, {
bsw/jbe@1309 16094 "referenceNode": this.composer.element,
bsw/jbe@1309 16095 "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}],
bsw/jbe@1309 16096 "uneditableClass": this.config.classNames.uneditableContainer
bsw/jbe@1309 16097 });
bsw/jbe@1309 16098 this.composer.selection.deleteContents();
bsw/jbe@1309 16099 this.composer.selection.insertHTML(cleanHtml);
bsw/jbe@1309 16100 }
bsw/jbe@1309 16101 });
bsw/jbe@1309 16102 })(wysihtml);

Impressum / About Us