1 /*! 2 * This file is part of Aloha Editor 3 * Author & Copyright (c) 2010 Gentics Software GmbH, aloha@gentics.com 4 * Licensed unter the terms of http://www.aloha-editor.com/license.html 5 */ 6 (function(window, undefined) { 7 "use strict"; 8 var 9 jQuery = window.alohaQuery || window.jQuery, $ = jQuery, 10 // GENTICS = window.GENTICS, 11 // Aloha = window.Aloha, 12 DOMUtils, TextRangeUtils, selection, DOMRange, RangeIterator, DOMSelection; 13 14 /* 15 * Only execute the following code if we are in IE (check for 16 * document.attachEvent, this is a microsoft event and therefore only available 17 * in IE). 18 */ 19 20 if(document.attachEvent && document.selection) { 21 /*! 22 * DOM Ranges for Internet Explorer (m2) 23 * 24 * Copyright (c) 2009 Tim Cameron Ryan 25 * Released under the MIT/X License 26 * available at http://code.google.com/p/ierange/ 27 */ 28 29 /* 30 Range reference: 31 http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html 32 http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp 33 https://developer.mozilla.org/En/DOM:Range 34 Selection reference: 35 http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp 36 TextRange reference: 37 http://msdn.microsoft.com/en-us/library/ms535872.aspx 38 Other links: 39 http://jorgenhorstink.nl/test/javascript/range/range.js 40 http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/ 41 http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html 42 */ 43 44 //[TODO] better exception support 45 46 47 /* 48 DOM functions 49 */ 50 51 DOMUtils = { 52 findChildPosition: function (node) { 53 for (var i = 0; node = node.previousSibling; i++) 54 continue; 55 return i; 56 }, 57 isDataNode: function (node) { 58 return node && node.nodeValue !== null && node.data !== null; 59 }, 60 isAncestorOf: function (parent, node) { 61 return !DOMUtils.isDataNode(parent) && 62 (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) || 63 node.parentNode == parent); 64 }, 65 isAncestorOrSelf: function (root, node) { 66 return DOMUtils.isAncestorOf(root, node) || root == node; 67 }, 68 findClosestAncestor: function (root, node) { 69 if (DOMUtils.isAncestorOf(root, node)) 70 while (node && node.parentNode != root) 71 node = node.parentNode; 72 return node; 73 }, 74 getNodeLength: function (node) { 75 return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length; 76 }, 77 splitDataNode: function (node, offset) { 78 if (!DOMUtils.isDataNode(node)) 79 return false; 80 var newNode = node.cloneNode(false); 81 node.deleteData(offset, node.length); 82 newNode.deleteData(0, offset); 83 node.parentNode.insertBefore(newNode, node.nextSibling); 84 } 85 }; 86 87 /* 88 Text Range utilities 89 functions to simplify text range manipulation in ie 90 */ 91 92 TextRangeUtils = { 93 convertToDOMRange: function (textRange, document) { 94 var domRange,adoptBoundary; 95 96 adoptBoundary = function(domRange, textRange, bStart) { 97 // iterate backwards through parent element to find anchor location 98 var cursorNode = document.createElement('a'), 99 cursor = textRange.duplicate(), 100 parent; 101 102 cursor.collapse(bStart); 103 parent = cursor.parentElement(); 104 do { 105 parent.insertBefore(cursorNode, cursorNode.previousSibling); 106 cursor.moveToElementText(cursorNode); 107 } while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling); 108 109 // when we exceed or meet the cursor, we've found the node 110 if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) { 111 // data node 112 cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange); 113 domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length); 114 } else { 115 // element 116 domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode); 117 } 118 cursorNode.parentNode.removeChild(cursorNode); 119 }; 120 121 // return a DOM range 122 domRange = new DOMRange(document); 123 adoptBoundary(domRange, textRange, true); 124 adoptBoundary(domRange, textRange, false); 125 return domRange; 126 }, 127 128 convertFromDOMRange: function (domRange) { 129 function adoptEndPoint(textRange, domRange, bStart) { 130 // find anchor node and offset 131 var container = domRange[bStart ? 'startContainer' : 'endContainer'], 132 offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0, 133 anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset], 134 anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container, 135 cursorNode, cursor; 136 137 // visible data nodes need a text offset 138 if (container.nodeType == 3 || container.nodeType == 4) { 139 textOffset = offset; 140 } 141 142 // create a cursor element node to position range (since we can't select text nodes) 143 cursorNode = domRange._document.createElement('a'); 144 anchorParent.insertBefore(cursorNode, anchorNode); 145 cursor = domRange._document.body.createTextRange(); 146 cursor.moveToElementText(cursorNode); 147 cursorNode.parentNode.removeChild(cursorNode); 148 // move range 149 textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor); 150 textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset); 151 } 152 153 // return an IE text range 154 var textRange = domRange._document.body.createTextRange(); 155 adoptEndPoint(textRange, domRange, true); 156 adoptEndPoint(textRange, domRange, false); 157 return textRange; 158 } 159 }; 160 161 /* 162 DOM Range 163 */ 164 DOMRange = function(document) { 165 // save document parameter 166 this._document = document; 167 168 // initialize range 169 //[TODO] this should be located at document[0], document[0] 170 this.startContainer = this.endContainer = document.body; 171 this.endOffset = DOMUtils.getNodeLength(document.body); 172 }; 173 174 DOMRange.START_TO_START = 0; 175 DOMRange.START_TO_END = 1; 176 DOMRange.END_TO_END = 2; 177 DOMRange.END_TO_START = 3; 178 179 DOMRange.prototype = { 180 // public properties 181 startContainer: null, 182 startOffset: 0, 183 endContainer: null, 184 endOffset: 0, 185 commonAncestorContainer: null, 186 collapsed: false, 187 // private properties 188 189 _document: null, 190 191 // private methods 192 _refreshProperties: function () { 193 // collapsed attribute 194 this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset); 195 // find common ancestor 196 var node = this.startContainer; 197 while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer)) 198 node = node.parentNode; 199 this.commonAncestorContainer = node; 200 }, 201 202 // range methods 203 //[TODO] collapse if start is after end, end is before start 204 setStart: function(container, offset) { 205 this.startContainer = container; 206 this.startOffset = offset; 207 this._refreshProperties(); 208 }, 209 setEnd: function(container, offset) { 210 this.endContainer = container; 211 this.endOffset = offset; 212 this._refreshProperties(); 213 }, 214 setStartBefore: function (refNode) { 215 // set start to beore this node 216 this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode)); 217 }, 218 setStartAfter: function (refNode) { 219 // select next sibling 220 this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1); 221 }, 222 setEndBefore: function (refNode) { 223 // set end to beore this node 224 this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode)); 225 }, 226 setEndAfter: function (refNode) { 227 // select next sibling 228 this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1); 229 }, 230 selectNode: function (refNode) { 231 this.setStartBefore(refNode); 232 this.setEndAfter(refNode); 233 }, 234 selectNodeContents: function (refNode) { 235 this.setStart(refNode, 0); 236 this.setEnd(refNode, DOMUtils.getNodeLength(refNode)); 237 }, 238 collapse: function (toStart) { 239 if (toStart) 240 this.setEnd(this.startContainer, this.startOffset); 241 else 242 this.setStart(this.endContainer, this.endOffset); 243 }, 244 245 // editing methods 246 cloneContents: function () { 247 // clone subtree 248 return (function cloneSubtree(iterator) { 249 for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) { 250 node = node.cloneNode(!iterator.hasPartialSubtree()); 251 if (iterator.hasPartialSubtree()) 252 node.appendChild(cloneSubtree(iterator.getSubtreeIterator())); 253 frag.appendChild(node); 254 } 255 return frag; 256 })(new RangeIterator(this)); 257 }, 258 extractContents: function () { 259 // cache range and move anchor points 260 var range = this.cloneRange(); 261 if (this.startContainer != this.commonAncestorContainer) 262 this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer)); 263 this.collapse(true); 264 // extract range 265 return (function extractSubtree(iterator) { 266 for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) { 267 if ( iterator.hasPartialSubtree() ) { 268 node = node.cloneNode(false); 269 } 270 else { 271 iterator.remove(); 272 } 273 if (iterator.hasPartialSubtree()) 274 node.appendChild(extractSubtree(iterator.getSubtreeIterator())); 275 frag.appendChild(node); 276 } 277 return frag; 278 })(new RangeIterator(range)); 279 }, 280 deleteContents: function () { 281 // cache range and move anchor points 282 var range = this.cloneRange(); 283 if (this.startContainer != this.commonAncestorContainer) 284 this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer)); 285 this.collapse(true); 286 // delete range 287 (function deleteSubtree(iterator) { 288 while (iterator.next()) { 289 if ( iterator.hasPartialSubtree() ) { 290 deleteSubtree(iterator.getSubtreeIterator()); 291 } 292 else { 293 iterator.remove(); 294 } 295 } 296 })(new RangeIterator(range)); 297 }, 298 insertNode: function (newNode) { 299 // set original anchor and insert node 300 if (DOMUtils.isDataNode(this.startContainer)) { 301 DOMUtils.splitDataNode(this.startContainer, this.startOffset); 302 this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling); 303 } else { 304 this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]); 305 } 306 // resync start anchor 307 this.setStart(this.startContainer, this.startOffset); 308 }, 309 surroundContents: function (newNode) { 310 // extract and surround contents 311 var content = this.extractContents(); 312 this.insertNode(newNode); 313 newNode.appendChild(content); 314 this.selectNode(newNode); 315 }, 316 317 // other methods 318 compareBoundaryPoints: function (how, sourceRange) { 319 // get anchors 320 var containerA, offsetA, containerB, offsetB; 321 switch (how) { 322 case DOMRange.START_TO_START: 323 case DOMRange.START_TO_END: 324 containerA = this.startContainer; 325 offsetA = this.startOffset; 326 break; 327 case DOMRange.END_TO_END: 328 case DOMRange.END_TO_START: 329 containerA = this.endContainer; 330 offsetA = this.endOffset; 331 break; 332 } 333 switch (how) { 334 case DOMRange.START_TO_START: 335 case DOMRange.END_TO_START: 336 containerB = sourceRange.startContainer; 337 offsetB = sourceRange.startOffset; 338 break; 339 case DOMRange.START_TO_END: 340 case DOMRange.END_TO_END: 341 containerB = sourceRange.endContainer; 342 offsetB = sourceRange.endOffset; 343 break; 344 } 345 346 // compare 347 return containerA.sourceIndex < containerB.sourceIndex ? -1 : 348 containerA.sourceIndex == containerB.sourceIndex ? 349 offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1 350 : 1; 351 }, 352 cloneRange: function () { 353 // return cloned range 354 var range = new DOMRange(this._document); 355 range.setStart(this.startContainer, this.startOffset); 356 range.setEnd(this.endContainer, this.endOffset); 357 return range; 358 }, 359 detach: function () { 360 //[TODO] Releases Range from use to improve performance. 361 }, 362 toString: function () { 363 return TextRangeUtils.convertFromDOMRange(this).text; 364 }, 365 createContextualFragment: function (tagString) { 366 // parse the tag string in a context node 367 var 368 content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false), 369 fragment; 370 371 content.innerHTML = tagString; 372 // return a document fragment from the created node 373 for (fragment = this._document.createDocumentFragment(); content.firstChild; ) 374 fragment.appendChild(content.firstChild); 375 return fragment; 376 } 377 }; 378 379 /* 380 Range iterator 381 */ 382 RangeIterator = function(range) { 383 384 this.range = range; 385 if (range.collapsed) { 386 return; 387 } 388 389 //[TODO] ensure this works 390 // get anchors 391 var root = range.commonAncestorContainer; 392 this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ? 393 range.startContainer.childNodes[range.startOffset] : 394 DOMUtils.findClosestAncestor(root, range.startContainer); 395 this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ? 396 range.endContainer.childNodes[range.endOffset] : 397 DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling; 398 }; 399 400 RangeIterator.prototype = { 401 // public properties 402 range: null, 403 // private properties 404 _current: null, 405 406 _next: null, 407 _end: null, 408 409 // public methods 410 hasNext: function () { 411 return !!this._next; 412 }, 413 next: function () { 414 // move to next node 415 var current = this._current = this._next; 416 this._next = this._current && this._current.nextSibling != this._end ? 417 this._current.nextSibling : null; 418 419 // check for partial text nodes 420 if (DOMUtils.isDataNode(this._current)) { 421 if (this.range.endContainer == this._current) 422 (current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset); 423 if (this.range.startContainer == this._current) 424 (current = current.cloneNode(true)).deleteData(0, this.range.startOffset); 425 } 426 return current; 427 }, 428 remove: function () { 429 var end, start; 430 // check for partial text nodes 431 if (DOMUtils.isDataNode(this._current) && 432 (this.range.startContainer == this._current || this.range.endContainer == this._current)) { 433 start = this.range.startContainer == this._current ? this.range.startOffset : 0; 434 end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length; 435 this._current.deleteData(start, end - start); 436 } else 437 this._current.parentNode.removeChild(this._current); 438 }, 439 hasPartialSubtree: function () { 440 // check if this node be partially selected 441 return !DOMUtils.isDataNode(this._current) && 442 (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) || 443 DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)); 444 }, 445 getSubtreeIterator: function () { 446 // create a new range 447 var subRange = new DOMRange(this.range._document); 448 subRange.selectNodeContents(this._current); 449 // handle anchor points 450 if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer)) 451 subRange.setStart(this.range.startContainer, this.range.startOffset); 452 if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)) 453 subRange.setEnd(this.range.endContainer, this.range.endOffset); 454 // return iterator 455 return new RangeIterator(subRange); 456 } 457 }; 458 459 /* 460 DOM Selection 461 */ 462 463 //[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's 464 // implementation and without redundant features. Complete selection manipulation is still 465 // possible with just removeAllRanges/addRange/getRangeAt. 466 467 DOMSelection = function (document) { 468 // save document parameter 469 this._document = document; 470 471 // add DOM selection handler 472 var selection = this; 473 document.attachEvent('onselectionchange', function () { selection._selectionChangeHandler(); }); 474 }; 475 476 DOMSelection.prototype = { 477 // public properties 478 rangeCount: 0, 479 // private properties 480 _document: null, 481 482 // private methods 483 _selectionChangeHandler: function () { 484 // check if there exists a range 485 this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0; 486 }, 487 _selectionExists: function (textRange) { 488 // checks if a created text range exists or is an editable cursor 489 return textRange.compareEndPoints('StartToEnd', textRange) !== 0 || 490 textRange.parentElement().isContentEditable; 491 }, 492 493 // public methods 494 addRange: function (range) { 495 // add range or combine with existing range 496 var selection = this._document.selection.createRange(), textRange = TextRangeUtils.convertFromDOMRange(range); 497 if (!this._selectionExists(selection)) 498 { 499 // select range 500 textRange.select(); 501 } 502 else 503 { 504 // only modify range if it intersects with current range 505 if (textRange.compareEndPoints('StartToStart', selection) == -1) 506 if (textRange.compareEndPoints('StartToEnd', selection) > -1 && 507 textRange.compareEndPoints('EndToEnd', selection) == -1) 508 selection.setEndPoint('StartToStart', textRange); 509 else 510 if (textRange.compareEndPoints('EndToStart', selection) < 1 && 511 textRange.compareEndPoints('EndToEnd', selection) > -1) 512 selection.setEndPoint('EndToEnd', textRange); 513 selection.select(); 514 } 515 }, 516 removeAllRanges: function () { 517 // remove all ranges 518 this._document.selection.empty(); 519 520 }, 521 getRangeAt: function (index) { 522 // return any existing selection, or a cursor position in content editable mode 523 var textRange = this._document.selection.createRange(); 524 if (this._selectionExists(textRange)) 525 return TextRangeUtils.convertToDOMRange(textRange, this._document); 526 return null; 527 }, 528 toString: function () { 529 // get selection text 530 return this._document.selection.createRange().text; 531 } 532 }; 533 534 /* 535 scripting hooks 536 */ 537 538 document.createRange = function () { 539 return new DOMRange(document); 540 }; 541 542 selection = new DOMSelection(document); 543 window.getSelection = function () { 544 return selection; 545 }; 546 547 //[TODO] expose DOMRange/DOMSelection to window.? 548 } 549 550 })(window);