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