1 /* selection.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 26 * recipients can access the Corresponding Source. 27 */ 28 define([ 29 'aloha/core', 30 'jquery', 31 'util/class', 32 'util/range', 33 'util/arrays', 34 'util/strings', 35 'aloha/console', 36 'PubSub', 37 'aloha/engine', 38 'aloha/ecma5shims', 39 'aloha/rangy-core' 40 ], function ( 41 Aloha, 42 jQuery, 43 Class, 44 Range, 45 Arrays, 46 Strings, 47 console, 48 PubSub, 49 Engine, 50 e5s 51 ) { 52 "use strict"; 53 54 var GENTICS = window.GENTICS; 55 56 function isCollapsedAndEmptyOrEndBr(rangeObject) { 57 var firstChild; 58 if (rangeObject.startContainer !== rangeObject.endContainer) { 59 return false; 60 } 61 // check whether the container starts in an element node 62 if (rangeObject.startContainer.nodeType != 1) { 63 return false; 64 } 65 firstChild = rangeObject.startContainer.firstChild; 66 return (!firstChild || (!firstChild.nextSibling && firstChild.nodeName == 'BR')); 67 } 68 69 function isCollapsedAndEndBr(rangeObject) { 70 if (rangeObject.startContainer !== rangeObject.endContainer) { 71 return false; 72 } 73 if (rangeObject.startContainer.nodeType != 1) { 74 return false; 75 } 76 return Engine.isEndBreak(rangeObject.startContainer); 77 } 78 79 var prevStartContext = null; 80 var prevEndContext = null; 81 82 function makeContextHtml(node, parents) { 83 var result = [], 84 parent, 85 len, 86 i; 87 if (1 === node.nodeType && node.nodeName !== 'BODY' && node.nodeName !== 'HTML') { 88 result.push(node.cloneNode(false).outerHTML); 89 } else { 90 result.push('#' + node.nodeType); 91 } 92 for (i = 0, len = parents.length; i < len; i++) { 93 parent = parents[i]; 94 if (parent.nodeName === 'BODY' || parent.nodeName === 'HTML') { 95 // Although we limit the ancestors in most cases to the 96 // active editable, in some cases (copy&paste) the 97 // parent may be outside. 98 // On IE7 this means the following code may clone the 99 // HTML node too, which causes the browser to crash. 100 // On other browsers, this is just an optimization 101 // because the body and html elements should probably 102 // not be considered part of the context of an edit 103 // operation. 104 break; 105 } 106 result.push(parent.cloneNode(false).outerHTML); 107 } 108 return result.join(''); 109 } 110 111 function getChangedContext(node, context) { 112 var until = Aloha.activeEditable ? Aloha.activeEditable.obj.parent()[0] : null; 113 var parents = jQuery(node).parentsUntil(until).get(); 114 var html = makeContextHtml(node, parents); 115 var equal = (context && node === context.node && Arrays.equal(context.parents, parents) && html === context.html); 116 return equal ? null : { 117 node: node, 118 parents: parents, 119 html: html 120 }; 121 } 122 123 function triggerSelectionContextChanged(rangeObject, event) { 124 var startContainer = rangeObject.startContainer; 125 var endContainer = rangeObject.endContainer; 126 if (!startContainer || !endContainer) { 127 console.warn("aloha/selection", "encountered range object without start or end container"); 128 return; 129 } 130 var startContext = getChangedContext(startContainer, prevStartContext); 131 var endContext = getChangedContext(endContainer, prevEndContext); 132 if (!startContext && !endContext) { 133 return; 134 } 135 prevStartContext = startContext; 136 prevEndContext = endContext; 137 138 /** 139 * @api documented in the guides 140 */ 141 PubSub.pub('aloha.selection.context-change', { 142 range: rangeObject, 143 event: event 144 }); 145 } 146 147 /** 148 * @namespace Aloha 149 * @class Selection 150 * This singleton class always represents the current user selection 151 * @singleton 152 */ 153 var Selection = Class.extend({ 154 _constructor: function () { 155 // Pseudo Range Clone being cleaned up for better HTML wrapping support 156 this.rangeObject = {}; 157 158 this.preventSelectionChangedFlag = false; // will remember if someone urged us to skip the next aloha-selection-changed event 159 160 // define basics first 161 this.tagHierarchy = { 162 'textNode': {}, 163 'abbr': { 164 'textNode': true 165 }, 166 'b': { 167 'textNode': true, 168 'b': true, 169 'i': true, 170 'em': true, 171 'sup': true, 172 'sub': true, 173 'br': true, 174 'span': true, 175 'img': true, 176 'a': true, 177 'del': true, 178 'ins': true, 179 'u': true, 180 'cite': true, 181 'q': true, 182 'code': true, 183 'abbr': true, 184 'strong': true 185 }, 186 'pre': { 187 'textNode': true, 188 'b': true, 189 'i': true, 190 'em': true, 191 'sup': true, 192 'sub': true, 193 'br': true, 194 'span': true, 195 'img': true, 196 'a': true, 197 'del': true, 198 'ins': true, 199 'u': true, 200 'cite': true, 201 'q': true, 202 'code': true, 203 'abbr': true 204 }, 205 'blockquote': { 206 'textNode': true, 207 'b': true, 208 'i': true, 209 'em': true, 210 'sup': true, 211 'sub': true, 212 'br': true, 213 'span': true, 214 'img': true, 215 'a': true, 216 'del': true, 217 'ins': true, 218 'u': true, 219 'cite': true, 220 221 'q': true, 222 'code': true, 223 'abbr': true, 224 'p': true, 225 'h1': true, 226 'h2': true, 227 'h3': true, 228 'h4': true, 229 'h5': true, 230 'h6': true 231 }, 232 'ins': { 233 'textNode': true, 234 'b': true, 235 'i': true, 236 'em': true, 237 'sup': true, 238 'sub': true, 239 'br': true, 240 'span': true, 241 'img': true, 242 'a': true, 243 'u': true, 244 'p': true, 245 'h1': true, 246 'h2': true, 247 'h3': true, 248 'h4': true, 249 'h5': true, 250 'h6': true 251 }, 252 'ul': { 253 'li': true 254 }, 255 'ol': { 256 'li': true 257 }, 258 'li': { 259 'textNode': true, 260 'b': true, 261 'i': true, 262 'em': true, 263 'sup': true, 264 'sub': true, 265 'br': true, 266 'span': true, 267 'img': true, 268 'ul': true, 269 'ol': true, 270 'h1': true, 271 'h2': true, 272 'h3': true, 273 'h4': true, 274 'h5': true, 275 'h6': true, 276 'del': true, 277 'ins': true, 278 'u': true, 279 'a': true 280 }, 281 'tr': { 282 'td': true, 283 'th': true 284 }, 285 'table': { 286 'tr': true 287 }, 288 'div': { 289 'textNode': true, 290 'b': true, 291 'i': true, 292 'em': true, 293 'sup': true, 294 'sub': true, 295 'br': true, 296 'span': true, 297 'img': true, 298 'ul': true, 299 'ol': true, 300 'table': true, 301 'h1': true, 302 'h2': true, 303 'h3': true, 304 'h4': true, 305 'h5': true, 306 'h6': true, 307 'del': true, 308 'ins': true, 309 'u': true, 310 'p': true, 311 'div': true, 312 'pre': true, 313 'blockquote': true, 314 'a': true 315 }, 316 'h1': { 317 'textNode': true, 318 'b': true, 319 'i': true, 320 'em': true, 321 'sup': true, 322 'sub': true, 323 'br': true, 324 'span': true, 325 'img': true, 326 'a': true, 327 'del': true, 328 'ins': true, 329 'u': true 330 } 331 }; 332 333 // now reference the basics for all other equal tags (important: don't forget to include 334 // the basics itself as reference: 'b' : this.tagHierarchy.b 335 this.tagHierarchy = { 336 'textNode': this.tagHierarchy.textNode, 337 'abbr': this.tagHierarchy.abbr, 338 'br': this.tagHierarchy.textNode, 339 'img': this.tagHierarchy.textNode, 340 'b': this.tagHierarchy.b, 341 'strong': this.tagHierarchy.b, 342 'code': this.tagHierarchy.b, 343 'q': this.tagHierarchy.b, 344 'blockquote': this.tagHierarchy.blockquote, 345 'cite': this.tagHierarchy.b, 346 'i': this.tagHierarchy.b, 347 'em': this.tagHierarchy.b, 348 'sup': this.tagHierarchy.b, 349 'sub': this.tagHierarchy.b, 350 'span': this.tagHierarchy.b, 351 'del': this.tagHierarchy.del, 352 'ins': this.tagHierarchy.ins, 353 'u': this.tagHierarchy.b, 354 'p': this.tagHierarchy.b, 355 'pre': this.tagHierarchy.pre, 356 'a': this.tagHierarchy.b, 357 'ul': this.tagHierarchy.ul, 358 'ol': this.tagHierarchy.ol, 359 'li': this.tagHierarchy.li, 360 'td': this.tagHierarchy.li, 361 'div': this.tagHierarchy.div, 362 'h1': this.tagHierarchy.h1, 363 'h2': this.tagHierarchy.h1, 364 'h3': this.tagHierarchy.h1, 365 'h4': this.tagHierarchy.h1, 366 'h5': this.tagHierarchy.h1, 367 'h6': this.tagHierarchy.h1, 368 'table': this.tagHierarchy.table 369 }; 370 371 // When applying this elements to selection they will replace the assigned elements 372 this.replacingElements = { 373 'h1': { 374 'p': true, 375 'h1': true, 376 'h2': true, 377 'h3': true, 378 'h4': true, 379 'h5': true, 380 'h6': true, 381 'pre': true, 382 'blockquote': true 383 } 384 }; 385 this.replacingElements = { 386 'h1': this.replacingElements.h1, 387 'h2': this.replacingElements.h1, 388 'h3': this.replacingElements.h1, 389 'h4': this.replacingElements.h1, 390 'h5': this.replacingElements.h1, 391 'h6': this.replacingElements.h1, 392 'pre': this.replacingElements.h1, 393 'p': this.replacingElements.h1, 394 'blockquote': this.replacingElements.h1 395 }; 396 this.allowedToStealElements = { 397 'h1': { 398 'textNode': true 399 } 400 }; 401 this.allowedToStealElements = { 402 'h1': this.allowedToStealElements.h1, 403 'h2': this.allowedToStealElements.h1, 404 'h3': this.allowedToStealElements.h1, 405 'h4': this.allowedToStealElements.h1, 406 'h5': this.allowedToStealElements.h1, 407 'h6': this.allowedToStealElements.h1, 408 'p': this.tagHierarchy.b 409 }; 410 }, 411 412 /** 413 * Class definition of a SelectionTree (relevant for all formatting / markup changes) 414 * TODO: remove this (was moved to range.js) 415 * Structure: 416 * + 417 * |-domobj: <reference to the DOM Object> (NOT jQuery) 418 * |-selection: defines if this node is marked by user [none|partial|full] 419 * |-children: recursive structure like this 420 * @hide 421 */ 422 SelectionTree: function () { 423 this.domobj = {}; 424 this.selection = undefined; 425 this.children = []; 426 }, 427 428 /** 429 * INFO: Method is used for integration with Gentics Aloha, has no use otherwise 430 * Updates the rangeObject according to the current user selection 431 * Method is always called on selection change 432 * @param objectClicked Object that triggered the selectionChange event 433 * @return true when rangeObject was modified, false otherwise 434 * @hide 435 */ 436 onChange: function (objectClicked, event, timeout, editableChanged) { 437 if (this.updateSelectionTimeout) { 438 window.clearTimeout(this.updateSelectionTimeout); 439 } 440 441 // We have to update the selection in a timeout due to an IE 442 // bug that is is caused by selecting some text and then 443 // clicking once inside the selection (which collapses the 444 // selection inside the previous selection). 445 var selection = this; 446 this.updateSelectionTimeout = window.setTimeout(function () { 447 var range = new Aloha.Selection.SelectionRange(true); 448 // We have to work around an IE bug that causes the user 449 // selection to be incorrectly set on the body element 450 // when the updateSelectionTimeout triggers. The 451 // selection corrects itself after waiting a while. 452 if (!range.startContainer || 'HTML' === range.startContainer.nodeName || 'BODY' === range.startContainer.nodeName) { 453 if (!this.updateSelectionTimeout) { 454 // First wait 5 millis, then 20 millis, 50 millis, 110 millis etc. 455 selection.onChange(objectClicked, event, 10 + (timeout || 5) * 2); 456 } 457 return; 458 } else { 459 // And yet another IE workaround. Somehow the caret is not 460 // positioned inside the clicked editable. This occures only 461 // when switching editables in IE. In those cases the caret is 462 // invisible. I tried to trace the origin of the issue but i 463 // could not find the place where the caret is mispositioned. 464 // I noticed that IE is sometimes adding drag handles to 465 // editables. Aloha is removing those handles. 466 // If those handles are visible it apears that two clicks are needed 467 // to activate the editable. The first click is to select the 468 // editable and the second to enable it and activeate it. I added a 469 // range select call that will cirumvent this issue by resetting 470 // the selection. I also checked the range object. In all cases 471 // i found the range object contained correct properties. The 472 // workaround will only be applied for IE. 473 if (jQuery.browser.msie && editableChanged) { 474 range.select(); 475 } 476 } 477 Aloha.Selection._updateSelection(event, range); 478 }, timeout || 5); 479 }, 480 481 /** 482 * prevents the next aloha-selection-changed event from being triggered 483 */ 484 preventSelectionChanged: function () { 485 this.preventSelectionChangedFlag = true; 486 }, 487 488 /** 489 * will return wheter selection change event was prevented or not, and reset the preventSelectionChangedFlag 490 * @return {Boolean} true if aloha-selection-change event was prevented 491 */ 492 isSelectionChangedPrevented: function () { 493 var prevented = this.preventSelectionChangedFlag; 494 this.preventSelectionChangedFlag = false; 495 return prevented; 496 }, 497 498 /** 499 * Checks if the current rangeObject common ancector container is edtiable 500 * @return {Boolean} true if current common ancestor is editable 501 */ 502 isSelectionEditable: function () { 503 return (this.rangeObject.commonAncestorContainer && jQuery(this.rangeObject.commonAncestorContainer).contentEditable()); 504 }, 505 506 /** 507 * This method checks, if the current rangeObject common ancestor container has a 'data-aloha-floatingmenu-visible' Attribute. 508 * Needed in Floating Menu for exceptional display of floatingmenu. 509 */ 510 isFloatingMenuVisible: function () { 511 var visible = jQuery(Aloha.Selection.rangeObject.commonAncestorContainer).attr('data-aloha-floatingmenu-visible'); 512 if (visible !== 'undefined') { 513 if (visible === 'true') { 514 return true; 515 } 516 return false; 517 } 518 return false; 519 }, 520 521 /** 522 * INFO: Method is used for integration with Gentics Aloha, has no use otherwise 523 * Updates the rangeObject according to the current user selection 524 * Method is always called on selection change 525 * @param event jQuery browser event object 526 * @return true when rangeObject was modified, false otherwise 527 * @hide 528 */ 529 updateSelection: function (event) { 530 return this._updateSelection(event, null); 531 }, 532 533 /** 534 * Internal version of updateSelection that adds the range parameter to be 535 * able to work around an IE bug that caused the current user selection 536 * sometimes to be on the body element. 537 * @param {Object} event 538 * @param {Object} range a substitute for the current user selection. if not provided, 539 * the current user selection will be used. 540 * @hide 541 */ 542 _updateSelection: function (event, range) { 543 if (event && event.originalEvent && 544 true === event.originalEvent.stopSelectionUpdate) { 545 return false; 546 } 547 548 if (typeof range === 'undefined') { 549 return false; 550 } 551 552 this.rangeObject = range = 553 range || new Aloha.Selection.SelectionRange(true); 554 555 // workaround for FF selection bug, where it is possible to move the selection INTO a hr 556 if (range && range.startContainer 557 && 'HR' === range.startContainer.nodeName 558 && range.endContainer 559 && 'HR' === range.endContainer.nodeName) { 560 Aloha.getSelection().removeAllRanges(); 561 return true; 562 } 563 564 // Determine the common ancestor container and update the selection 565 // tree. 566 range.update(); 567 568 // Workaround for nasty IE bug that allows the user to select 569 // text nodes inside areas with contenteditable "false" 570 if (range && range.startContainer && range.endContainer) { 571 var inEditable = 572 jQuery(range.commonAncestorContainer) 573 .closest('.aloha-editable').length > 0; 574 575 if (inEditable) { 576 var validStartPosition = !(3 === range.startContainer.nodeType && 577 !jQuery(range.startContainer.parentNode).contentEditable()); 578 579 var validEndPosition = !(3 === range.endContainer.nodeType && 580 !jQuery(range.endContainer.parentNode).contentEditable()); 581 582 if (!validStartPosition || !validEndPosition) { 583 584 Aloha.getSelection().removeAllRanges(); 585 return true; 586 } 587 } 588 } 589 590 // check if aloha-selection-changed event has been prevented 591 if (this.isSelectionChangedPrevented()) { 592 return true; 593 } 594 595 Aloha.trigger('aloha-selection-changed-before', [this.rangeObject, event]); 596 597 // throw the event that the selection has changed. Plugins now have the 598 // chance to react on the currentElements[childCount].children.lengthged selection 599 Aloha.trigger('aloha-selection-changed', [this.rangeObject, event]); 600 601 triggerSelectionContextChanged(this.rangeObject, event); 602 603 Aloha.trigger('aloha-selection-changed-after', [this.rangeObject, event]); 604 605 return true; 606 }, 607 608 /** 609 * creates an object with x items containing all relevant dom objects. 610 * Structure: 611 * + 612 * |-domobj: <reference to the DOM Object> (NOT jQuery) 613 * |-selection: defines if this node is marked by user [none|partial|full] 614 * |-children: recursive structure like this ("x.." because it's then shown last in DOM Browsers...) 615 * TODO: remove this (was moved to range.js) 616 * 617 * @param rangeObject "Aloha clean" range object including a commonAncestorContainer 618 * @return obj selection 619 * @hide 620 */ 621 getSelectionTree: function (rangeObject) { 622 if (!rangeObject) { // if called without any parameters, the method acts as getter for this.selectionTree 623 return this.rangeObject.getSelectionTree(); 624 } 625 if (!rangeObject.commonAncestorContainer) { 626 Aloha.Log.error(this, 'the rangeObject is missing the commonAncestorContainer'); 627 return false; 628 } 629 630 this.inselection = false; 631 632 // before getting the selection tree, we do a cleanup 633 if (GENTICS.Utils.Dom.doCleanup({ 'merge': true }, rangeObject)) { 634 rangeObject.update(); 635 rangeObject.select(); 636 } 637 638 return this.recursiveGetSelectionTree(rangeObject, rangeObject.commonAncestorContainer); 639 }, 640 641 /** 642 * Recursive inner function for generating the selection tree. 643 * TODO: remove this (was moved to range.js) 644 * @param rangeObject range object 645 * @param currentObject current DOM object for which the selection tree shall be generated 646 * @return array of SelectionTree objects for the children of the current DOM object 647 * @hide 648 */ 649 recursiveGetSelectionTree: function (rangeObject, currentObject) { 650 // get all direct children of the given object 651 var jQueryCurrentObject = jQuery(currentObject), 652 childCount = 0, 653 that = this, 654 currentElements = []; 655 656 jQueryCurrentObject.contents().each(function (index) { 657 var selectionType = 'none', 658 startOffset = false, 659 endOffset = false, 660 collapsedFound = false, 661 i, 662 elementsLength, 663 noneFound = false, 664 partialFound = false, 665 fullFound = false; 666 667 // check for collapsed selections between nodes 668 if (rangeObject.isCollapsed() && currentObject === rangeObject.startContainer && rangeObject.startOffset == index) { 669 // insert an extra selectiontree object for the collapsed selection here 670 currentElements[childCount] = new Aloha.Selection.SelectionTree(); 671 currentElements[childCount].selection = 'collapsed'; 672 currentElements[childCount].domobj = undefined; 673 that.inselection = false; 674 collapsedFound = true; 675 childCount++; 676 } 677 678 if (!that.inselection && !collapsedFound) { 679 // the start of the selection was not yet found, so look for it now 680 // check whether the start of the selection is found here 681 682 // Try to read the nodeType property and return if we do not have permission 683 // ie.: frame document to an external URL 684 var nodeType; 685 try { 686 nodeType = this.nodeType; 687 } catch (e) { 688 return; 689 } 690 691 // check is dependent on the node type 692 switch (nodeType) { 693 case 3: 694 // text node 695 if (this === rangeObject.startContainer) { 696 // the selection starts here 697 that.inselection = true; 698 699 // when the startoffset is > 0, the selection type is only partial 700 selectionType = rangeObject.startOffset > 0 ? 'partial' : 'full'; 701 startOffset = rangeObject.startOffset; 702 endOffset = this.length; 703 } 704 break; 705 case 1: 706 // element node 707 if (this === rangeObject.startContainer && rangeObject.startOffset === 0) { 708 // the selection starts here 709 that.inselection = true; 710 selectionType = 'full'; 711 } 712 if (currentObject === rangeObject.startContainer && rangeObject.startOffset === index) { 713 // the selection starts here 714 that.inselection = true; 715 selectionType = 'full'; 716 } 717 break; 718 } 719 } 720 721 if (that.inselection && !collapsedFound) { 722 if (selectionType == 'none') { 723 selectionType = 'full'; 724 } 725 // we already found the start of the selection, so look for the end of the selection now 726 // check whether the end of the selection is found here 727 728 switch (this.nodeType) { 729 case 3: 730 // text node 731 if (this === rangeObject.endContainer) { 732 // the selection ends here 733 that.inselection = false; 734 735 // check for partial selection here 736 if (rangeObject.endOffset < this.length) { 737 selectionType = 'partial'; 738 } 739 if (startOffset === false) { 740 startOffset = 0; 741 } 742 endOffset = rangeObject.endOffset; 743 } 744 break; 745 case 1: 746 // element node 747 if (this === rangeObject.endContainer && rangeObject.endOffset === 0) { 748 that.inselection = false; 749 } 750 break; 751 } 752 if (currentObject === rangeObject.endContainer && rangeObject.endOffset <= index) { 753 that.inselection = false; 754 selectionType = 'none'; 755 } 756 } 757 758 // create the current selection tree entry 759 currentElements[childCount] = new Aloha.Selection.SelectionTree(); 760 currentElements[childCount].domobj = this; 761 currentElements[childCount].selection = selectionType; 762 if (selectionType == 'partial') { 763 currentElements[childCount].startOffset = startOffset; 764 currentElements[childCount].endOffset = endOffset; 765 } 766 767 // now do the recursion step into the current object 768 currentElements[childCount].children = that.recursiveGetSelectionTree(rangeObject, this); 769 elementsLength = currentElements[childCount].children.length; 770 771 // check whether a selection was found within the children 772 if (elementsLength > 0) { 773 for (i = 0; i < elementsLength; ++i) { 774 switch (currentElements[childCount].children[i].selection) { 775 case 'none': 776 noneFound = true; 777 break; 778 case 'full': 779 fullFound = true; 780 break; 781 case 'partial': 782 partialFound = true; 783 break; 784 } 785 } 786 787 if (partialFound || (fullFound && noneFound)) { 788 // found at least one 'partial' selection in the children, or both 'full' and 'none', so this element is also 'partial' selected 789 currentElements[childCount].selection = 'partial'; 790 } else if (fullFound && !partialFound && !noneFound) { 791 // only found 'full' selected children, so this element is also 'full' selected 792 currentElements[childCount].selection = 'full'; 793 } 794 } 795 796 childCount++; 797 }); 798 799 // extra check for collapsed selections at the end of the current element 800 if (rangeObject.isCollapsed() && currentObject === rangeObject.startContainer && rangeObject.startOffset == currentObject.childNodes.length) { 801 currentElements[childCount] = new Aloha.Selection.SelectionTree(); 802 currentElements[childCount].selection = 'collapsed'; 803 currentElements[childCount].domobj = undefined; 804 } 805 806 return currentElements; 807 }, 808 809 /** 810 * Get the currently selected range 811 * @return {Aloha.Selection.SelectionRange} currently selected range 812 * @method 813 */ 814 getRangeObject: function () { 815 return this.rangeObject; 816 }, 817 818 /** 819 * method finds out, if a node is within a certain markup or not 820 * @param rangeObj Aloha rangeObject 821 * @param startOrEnd boolean; defines, if start or endContainer should be used: false for start, true for end 822 * @param markupObject jQuery object of the markup to look for 823 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 824 * @param limitObject dom object which limits the search are within the dom. normally this will be the active Editable 825 * @return true, if the markup is effective on the range objects start or end node 826 * @hide 827 */ 828 isRangeObjectWithinMarkup: function (rangeObject, startOrEnd, markupObject, tagComparator, limitObject) { 829 var domObj = !startOrEnd ? rangeObject.startContainer : rangeObject.endContainer, 830 that = this, 831 parents = jQuery(domObj).parents(), 832 returnVal = false, 833 i = -1; 834 835 // check if a comparison method was passed as parameter ... 836 if (typeof tagComparator !== 'undefined' && typeof tagComparator !== 'function') { 837 Aloha.Log.error(this, 'parameter tagComparator is not a function'); 838 } 839 // ... if not use this as standard tag comparison method 840 if (typeof tagComparator === 'undefined') { 841 tagComparator = function (domobj, markupObject) { 842 return that.standardTextLevelSemanticsComparator(domobj, markupObject); // TODO should actually be this.getStandardTagComparator(markupObject) 843 }; 844 } 845 846 if (parents.length > 0) { 847 parents.each(function () { 848 // the limit object was reached (normally the Editable Element) 849 if (this === limitObject) { 850 Aloha.Log.debug(that, 'reached limit dom obj'); 851 return false; // break() of jQuery .each(); THIS IS NOT THE FUNCTION RETURN VALUE 852 } 853 if (tagComparator(this, markupObject)) { 854 if (returnVal === false) { 855 returnVal = []; 856 } 857 Aloha.Log.debug(that, 'reached object equal to markup'); 858 i++; 859 returnVal[i] = this; 860 return true; // continue() of jQuery .each(); THIS IS NOT THE FUNCTION RETURN VALUE 861 } 862 }); 863 } 864 return returnVal; 865 }, 866 867 /** 868 * standard method, to compare a domobj and a jquery object for sections and grouping content (e.g. p, h1, h2, ul, ....). 869 * is always used when no other tag comparator is passed as parameter 870 * @param domobj domobject to compare with markup 871 * @param markupObject jQuery object of the markup to compare with domobj 872 * @return true if objects are equal and false if not 873 * @hide 874 */ 875 standardSectionsAndGroupingContentComparator: function (domobj, markupObject) { 876 if (domobj.nodeType !== 1) { 877 878 Aloha.Log.debug(this, 'only element nodes (nodeType == 1) can be compared'); 879 return false; 880 } 881 if (!markupObject[0].nodeName) { 882 883 return false; 884 } 885 var elemMap = Aloha.Selection.replacingElements[domobj.nodeName.toLowerCase()]; 886 return elemMap && elemMap[markupObject[0].nodeName.toLowerCase()]; 887 }, 888 889 /** 890 * standard method, to compare a domobj and a jquery object for their tagName (aka span elements, e.g. b, i, sup, span, ...). 891 * is always used when no other tag comparator is passed as parameter 892 * @param domobj domobject to compare with markup 893 * @param markupObject jQuery object of the markup to compare with domobj 894 * @return true if objects are equal and false if not 895 * @hide 896 */ 897 standardTagNameComparator: function (domobj, markupObject) { 898 if (domobj.nodeType === 1) { 899 if (domobj.nodeName != markupObject[0].nodeName) { 900 return false; 901 } 902 return true; 903 } 904 Aloha.Log.debug(this, 'only element nodes (nodeType == 1) can be compared'); 905 return false; 906 }, 907 908 /** 909 * standard method, to compare a domobj and a jquery object for text level semantics (aka span elements, e.g. b, i, sup, span, ...). 910 * is always used when no other tag comparator is passed as parameter 911 * @param domobj domobject to compare with markup 912 * @param markupObject jQuery object of the markup to compare with domobj 913 * @return true if objects are equal and false if not 914 * @hide 915 */ 916 standardTextLevelSemanticsComparator: function (domobj, markupObject) { 917 // only element nodes can be compared 918 if (domobj.nodeType === 1) { 919 if (domobj.nodeName != markupObject[0].nodeName) { 920 return false; 921 } 922 if (!this.standardAttributesComparator(domobj, markupObject)) { 923 return false; 924 } 925 return true; 926 } 927 Aloha.Log.debug(this, 'only element nodes (nodeType == 1) can be compared'); 928 return false; 929 }, 930 931 932 /** 933 * standard method, to compare attributes of one dom obj and one markup obj (jQuery) 934 * @param domobj domobject to compare with markup 935 * @param markupObject jQuery object of the markup to compare with domobj 936 * @return true if objects are equal and false if not 937 * @hide 938 */ 939 standardAttributesComparator: function (domobj, markupObject) { 940 var classesA = Strings.words((domobj && domobj.className) || ''); 941 var classesB = Strings.words((markupObject.length && markupObject[0].className) || ''); 942 Arrays.sortUnique(classesA); 943 Arrays.sortUnique(classesB); 944 return Arrays.equal(classesA, classesB); 945 }, 946 947 /** 948 * method finds out, if a node is within a certain markup or not 949 * @param rangeObj Aloha rangeObject 950 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 951 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 952 * @return void; TODO: should return true if the markup applied successfully and false if not 953 * @hide 954 */ 955 changeMarkup: function (rangeObject, markupObject, tagComparator) { 956 var tagName = markupObject[0].tagName.toLowerCase(), 957 newCAC, 958 limitObject, 959 backupRangeObject, 960 relevantMarkupObjectsAtSelectionStart = this.isRangeObjectWithinMarkup(rangeObject, false, markupObject, tagComparator, limitObject), 961 relevantMarkupObjectsAtSelectionEnd = this.isRangeObjectWithinMarkup(rangeObject, true, markupObject, tagComparator, limitObject), 962 nextSibling, 963 relevantMarkupObjectAfterSelection, 964 prevSibling, 965 relevantMarkupObjectBeforeSelection, 966 extendedRangeObject; 967 var parentElement; 968 969 // if the element is a replacing element (like p/h1/h2/h3/h4/h5/h6...), which must not wrap each other 970 // use a clone of rangeObject 971 if (this.replacingElements[tagName]) { 972 // backup rangeObject for later selection; 973 backupRangeObject = rangeObject; 974 975 // create a new range object to not modify the orginal 976 rangeObject = new this.SelectionRange(rangeObject); 977 978 // either select the active Editable as new commonAncestorContainer (CAC) or use the body 979 if (Aloha.activeEditable) { 980 newCAC = Aloha.activeEditable.obj.get(0); 981 } else { 982 newCAC = jQuery('body'); 983 } 984 // update rangeObject by setting the newCAC and automatically recalculating the selectionTree 985 rangeObject.update(newCAC); 986 987 // store the information, that the markupObject can be replaced (not must be!!) inside the jQuery markup object 988 markupObject.isReplacingElement = true; 989 } else { 990 // if the element is NOT a replacing element, then something needs to be selected, otherwise it can not be wrapped 991 // therefor the method can return false, if nothing is selected ( = rangeObject is collapsed) 992 if (rangeObject.isCollapsed()) { 993 Aloha.Log.debug(this, 'early returning from applying markup because nothing is currently selected'); 994 return false; 995 } 996 } 997 998 // is Start/End DOM Obj inside the markup to change 999 if (Aloha.activeEditable) { 1000 limitObject = Aloha.activeEditable.obj[0]; 1001 } else { 1002 limitObject = jQuery('body'); 1003 } 1004 1005 if (!markupObject.isReplacingElement && rangeObject.startOffset === 0) { // don't care about replacers, because they never extend 1006 if (null != (prevSibling = this.getTextNodeSibling(false, rangeObject.commonAncestorContainer.parentNode, rangeObject.startContainer))) { 1007 relevantMarkupObjectBeforeSelection = this.isRangeObjectWithinMarkup({ 1008 startContainer: prevSibling, 1009 startOffset: 0 1010 }, false, markupObject, tagComparator, limitObject); 1011 } 1012 } 1013 if (!markupObject.isReplacingElement && (rangeObject.endOffset === rangeObject.endContainer.length)) { // don't care about replacers, because they never extend 1014 if (null != (nextSibling = this.getTextNodeSibling(true, rangeObject.commonAncestorContainer.parentNode, rangeObject.endContainer))) { 1015 relevantMarkupObjectAfterSelection = this.isRangeObjectWithinMarkup({ 1016 startContainer: nextSibling, 1017 startOffset: 0 1018 }, false, markupObject, tagComparator, limitObject); 1019 } 1020 } 1021 1022 // decide what to do (expand or reduce markup) 1023 // Alternative A: from markup to no-markup: markup will be removed in selection; 1024 // reapplied from original markup start to selection start 1025 if (!markupObject.isReplacingElement && (relevantMarkupObjectsAtSelectionStart && !relevantMarkupObjectsAtSelectionEnd)) { 1026 Aloha.Log.info(this, 'markup 2 non-markup'); 1027 this.prepareForRemoval(rangeObject.getSelectionTree(), markupObject, tagComparator); 1028 jQuery(relevantMarkupObjectsAtSelectionStart).addClass('preparedForRemoval'); 1029 this.insertCroppedMarkups(relevantMarkupObjectsAtSelectionStart, rangeObject, false, tagComparator); 1030 } else if (!markupObject.isReplacingElement && relevantMarkupObjectsAtSelectionStart && relevantMarkupObjectsAtSelectionEnd) { 1031 // Alternative B: from markup to markup: 1032 // remove selected markup (=split existing markup if single, shrink if two different) 1033 Aloha.Log.info(this, 'markup 2 markup'); 1034 this.prepareForRemoval(rangeObject.getSelectionTree(), markupObject, tagComparator); 1035 this.splitRelevantMarkupObject(relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject, tagComparator); 1036 } else if (!markupObject.isReplacingElement && ((!relevantMarkupObjectsAtSelectionStart && relevantMarkupObjectsAtSelectionEnd) || relevantMarkupObjectAfterSelection || relevantMarkupObjectBeforeSelection)) { // 1037 // Alternative C: from no-markup to markup OR with next2markup: 1038 // new markup is wrapped from selection start to end of originalmarkup, original is remove afterwards 1039 Aloha.Log.info(this, 'non-markup 2 markup OR with next2markup'); 1040 // move end of rangeObject to end of relevant markups 1041 if (relevantMarkupObjectBeforeSelection && relevantMarkupObjectAfterSelection) { 1042 extendedRangeObject = new Aloha.Selection.SelectionRange(rangeObject); 1043 extendedRangeObject.startContainer = jQuery(relevantMarkupObjectBeforeSelection[relevantMarkupObjectBeforeSelection.length - 1]).textNodes()[0]; 1044 extendedRangeObject.startOffset = 0; 1045 extendedRangeObject.endContainer = jQuery(relevantMarkupObjectAfterSelection[relevantMarkupObjectAfterSelection.length - 1]).textNodes().last()[0]; 1046 extendedRangeObject.endOffset = extendedRangeObject.endContainer.length; 1047 extendedRangeObject.update(); 1048 this.applyMarkup(extendedRangeObject.getSelectionTree(), rangeObject, markupObject, tagComparator); 1049 Aloha.Log.info(this, 'double extending previous markup(previous and after selection), actually wrapping it ...'); 1050 1051 } else if (relevantMarkupObjectBeforeSelection && !relevantMarkupObjectAfterSelection && !relevantMarkupObjectsAtSelectionEnd) { 1052 this.extendExistingMarkupWithSelection(relevantMarkupObjectBeforeSelection, rangeObject, false, tagComparator); 1053 Aloha.Log.info(this, 'extending previous markup'); 1054 1055 } else if (relevantMarkupObjectBeforeSelection && !relevantMarkupObjectAfterSelection && relevantMarkupObjectsAtSelectionEnd) { 1056 extendedRangeObject = new Aloha.Selection.SelectionRange(rangeObject); 1057 extendedRangeObject.startContainer = jQuery(relevantMarkupObjectBeforeSelection[relevantMarkupObjectBeforeSelection.length - 1]).textNodes()[0]; 1058 extendedRangeObject.startOffset = 0; 1059 extendedRangeObject.endContainer = jQuery(relevantMarkupObjectsAtSelectionEnd[relevantMarkupObjectsAtSelectionEnd.length - 1]).textNodes().last()[0]; 1060 extendedRangeObject.endOffset = extendedRangeObject.endContainer.length; 1061 extendedRangeObject.update(); 1062 this.applyMarkup(extendedRangeObject.getSelectionTree(), rangeObject, markupObject, tagComparator); 1063 Aloha.Log.info(this, 'double extending previous markup(previous and relevant at the end), actually wrapping it ...'); 1064 1065 } else if (!relevantMarkupObjectBeforeSelection && relevantMarkupObjectAfterSelection) { 1066 this.extendExistingMarkupWithSelection(relevantMarkupObjectAfterSelection, rangeObject, true, tagComparator); 1067 Aloha.Log.info(this, 'extending following markup backwards'); 1068 1069 } else { 1070 this.extendExistingMarkupWithSelection(relevantMarkupObjectsAtSelectionEnd, rangeObject, true, tagComparator); 1071 } 1072 } else if (markupObject.isReplacingElement || (!relevantMarkupObjectsAtSelectionStart && !relevantMarkupObjectsAtSelectionEnd && !relevantMarkupObjectBeforeSelection && !relevantMarkupObjectAfterSelection)) { 1073 // Alternative D: no-markup to no-markup: easy 1074 Aloha.Log.info(this, 'non-markup 2 non-markup'); 1075 1076 // workaround to keep the caret at the right position if it's an empty element 1077 // applyMarkup was not working correctly and has a lot of overhead we don't need in that case 1078 if (isCollapsedAndEmptyOrEndBr(rangeObject)) { 1079 var newMarkup = markupObject.clone(); 1080 1081 if (isCollapsedAndEndBr(rangeObject)) { 1082 newMarkup[0].appendChild(Engine.createEndBreak()); 1083 } 1084 1085 // setting the focus is needed for mozilla and IE 7 to have a working rangeObject.select() 1086 if (Aloha.activeEditable && jQuery.browser.mozilla) { 1087 Aloha.activeEditable.obj.focus(); 1088 } 1089 1090 if (Engine.isEditable(rangeObject.startContainer)) { 1091 Engine.copyAttributes(rangeObject.startContainer, newMarkup[0]); 1092 jQuery(rangeObject.startContainer).after(newMarkup[0]).remove(); 1093 } else if (Engine.isEditingHost(rangeObject.startContainer)) { 1094 jQuery(rangeObject.startContainer).append(newMarkup[0]); 1095 Engine.ensureContainerEditable(newMarkup[0]); 1096 } 1097 1098 backupRangeObject.startContainer = newMarkup[0]; 1099 backupRangeObject.endContainer = newMarkup[0]; 1100 backupRangeObject.startOffset = 0; 1101 backupRangeObject.endOffset = 0; 1102 return; 1103 } 1104 this.applyMarkup(rangeObject.getSelectionTree(), rangeObject, markupObject, tagComparator, { 1105 setRangeObject2NewMarkup: true 1106 }); 1107 backupRangeObject.startContainer = rangeObject.startContainer; 1108 backupRangeObject.endContainer = rangeObject.endContainer; 1109 backupRangeObject.startOffset = rangeObject.startOffset; 1110 backupRangeObject.endOffset = rangeObject.endOffset; 1111 } 1112 1113 if (markupObject.isReplacingElement) { 1114 //Check if the startContainer is one of the zapped elements 1115 if (backupRangeObject && backupRangeObject.startContainer.className && backupRangeObject.startContainer.className.indexOf('preparedForRemoval') > -1) { 1116 //var parentElement = jQuery(backupRangeObject.startContainer).closest(markupObject[0].tagName).get(0); 1117 parentElement = jQuery(backupRangeObject.startContainer).parents(markupObject[0].tagName).get(0); 1118 backupRangeObject.startContainer = parentElement; 1119 rangeObject.startContainer = parentElement; 1120 } 1121 //check if the endContainer is one of the zapped elements 1122 if (backupRangeObject && backupRangeObject.endContainer.className && backupRangeObject.endContainer.className.indexOf('preparedForRemoval') > -1) { 1123 //var parentElement = jQuery(backupRangeObject.endContainer).closest(markupObject[0].tagName).get(0); 1124 parentElement = jQuery(backupRangeObject.endContainer).parents(markupObject[0].tagName).get(0); 1125 backupRangeObject.endContainer = parentElement; 1126 rangeObject.endContainer = parentElement; 1127 } 1128 } 1129 // remove all marked items 1130 jQuery('.preparedForRemoval').zap(); 1131 1132 // recalculate cac and selectionTree 1133 1134 // update selection 1135 if (markupObject.isReplacingElement) { 1136 //After the zapping we have to check for wrong offsets 1137 if (e5s.Node.ELEMENT_NODE === backupRangeObject.startContainer.nodeType && backupRangeObject.startContainer.childNodes && backupRangeObject.startContainer.childNodes.length < backupRangeObject.startOffset) { 1138 backupRangeObject.startOffset = backupRangeObject.startContainer.childNodes.length; 1139 rangeObject.startOffset = backupRangeObject.startContainer.childNodes.length; 1140 } 1141 if (e5s.Node.ELEMENT_NODE === backupRangeObject.endContainer.nodeType && backupRangeObject.endContainer.childNodes && backupRangeObject.endContainer.childNodes.length < backupRangeObject.endOffset) { 1142 backupRangeObject.endOffset = backupRangeObject.endContainer.childNodes.length; 1143 rangeObject.endOffset = backupRangeObject.endContainer.childNodes.length; 1144 } 1145 rangeObject.endContainer = backupRangeObject.endContainer; 1146 rangeObject.endOffset = backupRangeObject.endOffset; 1147 1148 rangeObject.startContainer = backupRangeObject.startContainer; 1149 rangeObject.startOffset = backupRangeObject.startOffset; 1150 backupRangeObject.update(); 1151 backupRangeObject.select(); 1152 } else { 1153 rangeObject.update(); 1154 rangeObject.select(); 1155 } 1156 }, 1157 1158 /** 1159 * method compares a JS array of domobjects with a range object and decides, if the rangeObject spans the whole markup objects. method is used to decide if a markup2markup selection can be completely remove or if it must be splitted into 2 separate markups 1160 * @param relevantMarkupObjectsAtSelectionStart JS Array of dom objects, which are parents to the rangeObject.startContainer 1161 * @param relevantMarkupObjectsAtSelectionEnd JS Array of dom objects, which are parents to the rangeObject.endContainer 1162 * @param rangeObj Aloha rangeObject 1163 * @return true, if rangeObjects and markup objects are identical, false otherwise 1164 * @hide 1165 */ 1166 areMarkupObjectsAsLongAsRangeObject: function (relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject) { 1167 var i, el, textNode, relMarkupEnd, relMarkupStart; 1168 1169 if (rangeObject.startOffset !== 0) { 1170 return false; 1171 } 1172 1173 for (i = 0, relMarkupStart = relevantMarkupObjectsAtSelectionStart.length; i < relMarkupStart; i++) { 1174 el = jQuery(relevantMarkupObjectsAtSelectionStart[i]); 1175 if (el.textNodes().first()[0] !== rangeObject.startContainer) { 1176 return false; 1177 } 1178 } 1179 1180 for (i = 0, relMarkupEnd = relevantMarkupObjectsAtSelectionEnd.length; i < relMarkupEnd; i++) { 1181 el = jQuery(relevantMarkupObjectsAtSelectionEnd[i]); 1182 textNode = el.textNodes().last()[0]; 1183 if (textNode !== rangeObject.endContainer || textNode.length != rangeObject.endOffset) { 1184 return false; 1185 } 1186 } 1187 1188 return true; 1189 }, 1190 1191 /** 1192 * method used to remove/split markup from a "markup2markup" selection 1193 * @param relevantMarkupObjectsAtSelectionStart JS Array of dom objects, which are parents to the rangeObject.startContainer 1194 * @param relevantMarkupObjectsAtSelectionEnd JS Array of dom objects, which are parents to the rangeObject.endContainer 1195 * @param rangeObj Aloha rangeObject 1196 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1197 * @return true (always, since no "false" case is currently known...but might be added) 1198 * @hide 1199 */ 1200 splitRelevantMarkupObject: function (relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject, tagComparator) { 1201 // mark them to be deleted 1202 jQuery(relevantMarkupObjectsAtSelectionStart).addClass('preparedForRemoval'); 1203 jQuery(relevantMarkupObjectsAtSelectionEnd).addClass('preparedForRemoval'); 1204 1205 // check if the rangeObject is identical with the relevantMarkupObjects (in this case the markup can simply be removed) 1206 if (this.areMarkupObjectsAsLongAsRangeObject(relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject)) { 1207 return true; 1208 } 1209 1210 // find intersection (this can always only be one dom element (namely the highest) because all others will be removed 1211 var relevantMarkupObjectAtSelectionStartAndEnd = this.intersectRelevantMarkupObjects(relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd); 1212 1213 if (relevantMarkupObjectAtSelectionStartAndEnd) { 1214 this.insertCroppedMarkups([relevantMarkupObjectAtSelectionStartAndEnd], rangeObject, false, tagComparator); 1215 this.insertCroppedMarkups([relevantMarkupObjectAtSelectionStartAndEnd], rangeObject, true, tagComparator); 1216 } else { 1217 this.insertCroppedMarkups(relevantMarkupObjectsAtSelectionStart, rangeObject, false, tagComparator); 1218 this.insertCroppedMarkups(relevantMarkupObjectsAtSelectionEnd, rangeObject, true, tagComparator); 1219 } 1220 return true; 1221 }, 1222 1223 /** 1224 * method takes two arrays of bottom up dom objects, compares them and returns either the object closest to the root or false 1225 * @param relevantMarkupObjectsAtSelectionStart JS Array of dom objects 1226 * @param relevantMarkupObjectsAtSelectionEnd JS Array of dom objects 1227 * @return dom object closest to the root or false 1228 * @hide 1229 */ 1230 intersectRelevantMarkupObjects: function (relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd) { 1231 var intersection = false, i, elStart, j, elEnd, relMarkupStart, relMarkupEnd; 1232 if (!relevantMarkupObjectsAtSelectionStart || !relevantMarkupObjectsAtSelectionEnd) { 1233 return intersection; // we can only intersect, if we have to arrays! 1234 } 1235 relMarkupStart = relevantMarkupObjectsAtSelectionStart.length; 1236 relMarkupEnd = relevantMarkupObjectsAtSelectionEnd.length; 1237 for (i = 0; i < relMarkupStart; i++) { 1238 elStart = relevantMarkupObjectsAtSelectionStart[i]; 1239 for (j = 0; j < relMarkupEnd; j++) { 1240 elEnd = relevantMarkupObjectsAtSelectionEnd[j]; 1241 if (elStart === elEnd) { 1242 intersection = elStart; 1243 } 1244 } 1245 } 1246 return intersection; 1247 }, 1248 1249 /** 1250 * method used to add markup to a nonmarkup2markup selection 1251 * @param relevantMarkupObjects JS Array of dom objects effecting either the start or endContainer of a selection (which should be extended) 1252 * @param rangeObject Aloha rangeObject the markups should be extended to 1253 * @param startOrEnd boolean; defines, if the existing markups should be extended forwards or backwards (is propably redundant and could be found out by comparing start or end container with the markup array dom objects) 1254 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1255 * @return true 1256 * @hide 1257 1258 */ 1259 extendExistingMarkupWithSelection: function (relevantMarkupObjects, rangeObject, startOrEnd, tagComparator) { 1260 var extendMarkupsAtStart, extendMarkupsAtEnd, objects, i, relMarkupLength, el, textnodes, nodeNr; 1261 if (!startOrEnd) { // = Start 1262 // start part of rangeObject should be used, therefor existing markups are cropped at the end 1263 extendMarkupsAtStart = true; 1264 } 1265 if (startOrEnd) { // = End 1266 // end part of rangeObject should be used, therefor existing markups are cropped at start (beginning) 1267 extendMarkupsAtEnd = true; 1268 } 1269 objects = []; 1270 for (i = 0, relMarkupLength = relevantMarkupObjects.length; i < relMarkupLength; i++) { 1271 objects[i] = new this.SelectionRange(); 1272 el = relevantMarkupObjects[i]; 1273 if (extendMarkupsAtEnd && !extendMarkupsAtStart) { 1274 objects[i].startContainer = rangeObject.startContainer; // jQuery(el).contents()[0]; 1275 objects[i].startOffset = rangeObject.startOffset; 1276 textnodes = jQuery(el).textNodes(true); 1277 1278 nodeNr = textnodes.length - 1; 1279 1280 objects[i].endContainer = textnodes[nodeNr]; 1281 objects[i].endOffset = textnodes[nodeNr].length; 1282 objects[i].update(); 1283 this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, { 1284 setRangeObject2NewMarkup: true 1285 }); 1286 } 1287 if (!extendMarkupsAtEnd && extendMarkupsAtStart) { 1288 textnodes = jQuery(el).textNodes(true); 1289 objects[i].startContainer = textnodes[0]; // jQuery(el).contents()[0]; 1290 objects[i].startOffset = 0; 1291 objects[i].endContainer = rangeObject.endContainer; 1292 objects[i].endOffset = rangeObject.endOffset; 1293 objects[i].update(); 1294 this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, { 1295 setRangeObject2NewMarkup: true 1296 }); 1297 } 1298 } 1299 return true; 1300 }, 1301 1302 /** 1303 * method creates an empty markup jQuery object from a dom object passed as paramter 1304 * @param domobj domobject to be cloned, cleaned and emptied 1305 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1306 * @return jQuery wrapper object to be passed to e.g. this.applyMarkup(...) 1307 * @hide 1308 */ 1309 getClonedMarkup4Wrapping: function (domobj) { 1310 var wrapper = jQuery(domobj.outerHTML).removeClass('preparedForRemoval').empty(); 1311 if (wrapper.attr('class').length === 0) { 1312 wrapper.removeAttr('class'); 1313 } 1314 1315 return wrapper; 1316 }, 1317 1318 /** 1319 * method used to subtract the range object from existing markup. in other words: certain markup is removed from the selections defined by the rangeObject 1320 * @param relevantMarkupObjects JS Array of dom objects effecting either the start or endContainer of a selection (which should be extended) 1321 * @param rangeObject Aloha rangeObject the markups should be removed from 1322 * @param startOrEnd boolean; defines, if the existing markups should be reduced at the beginning of the tag or at the end (is propably redundant and could be found out by comparing start or end container with the markup array dom objects) 1323 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1324 * @return true 1325 * @hide 1326 */ 1327 insertCroppedMarkups: function (relevantMarkupObjects, rangeObject, startOrEnd, tagComparator) { 1328 var cropMarkupsAtEnd, cropMarkupsAtStart, textnodes, objects, i, el, textNodes; 1329 if (!startOrEnd) { // = Start 1330 // start part of rangeObject should be used, therefor existing markups are cropped at the end 1331 cropMarkupsAtEnd = true; 1332 } else { // = End 1333 // end part of rangeObject should be used, therefor existing markups are cropped at start (beginning) 1334 cropMarkupsAtStart = true; 1335 } 1336 objects = []; 1337 for (i = 0; i < relevantMarkupObjects.length; i++) { 1338 objects[i] = new this.SelectionRange(); 1339 el = relevantMarkupObjects[i]; 1340 if (cropMarkupsAtEnd && !cropMarkupsAtStart) { 1341 textNodes = jQuery(el).textNodes(true); 1342 objects[i].startContainer = textNodes[0]; 1343 objects[i].startOffset = 0; 1344 // if the existing markup startContainer & startOffset are equal to the rangeObject startContainer and startOffset, 1345 // then markupobject does not have to be added again, because it would have no content (zero-length) 1346 if (objects[i].startContainer === rangeObject.startContainer && objects[i].startOffset === rangeObject.startOffset) { 1347 continue; 1348 } 1349 if (rangeObject.startOffset === 0) { 1350 objects[i].endContainer = this.getTextNodeSibling(false, el, rangeObject.startContainer); 1351 objects[i].endOffset = objects[i].endContainer.length; 1352 } else { 1353 objects[i].endContainer = rangeObject.startContainer; 1354 objects[i].endOffset = rangeObject.startOffset; 1355 } 1356 1357 objects[i].update(); 1358 1359 this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, { 1360 setRangeObject2NextSibling: true 1361 }); 1362 } 1363 1364 if (!cropMarkupsAtEnd && cropMarkupsAtStart) { 1365 objects[i].startContainer = rangeObject.endContainer; // jQuery(el).contents()[0]; 1366 objects[i].startOffset = rangeObject.endOffset; 1367 textnodes = jQuery(el).textNodes(true); 1368 objects[i].endContainer = textnodes[textnodes.length - 1]; 1369 objects[i].endOffset = textnodes[textnodes.length - 1].length; 1370 objects[i].update(); 1371 this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, { 1372 setRangeObject2PreviousSibling: true 1373 }); 1374 } 1375 } 1376 return true; 1377 }, 1378 1379 /** 1380 * apply a certain markup to the current selection 1381 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1382 * @return void 1383 * @hide 1384 */ 1385 changeMarkupOnSelection: function (markupObject) { 1386 var rangeObject = this.getRangeObject(); 1387 1388 // change the markup 1389 this.changeMarkup(rangeObject, markupObject, this.getStandardTagComparator(markupObject)); 1390 1391 // merge text nodes 1392 GENTICS.Utils.Dom.doCleanup({ 1393 'merge': true 1394 }, rangeObject); 1395 1396 // update the range and select it 1397 rangeObject.update(); 1398 rangeObject.select(); 1399 this.rangeObject = rangeObject; 1400 }, 1401 1402 /** 1403 * apply a certain markup to the selection Tree 1404 * @param selectionTree SelectionTree Object markup should be applied to 1405 * @param rangeObject Aloha rangeObject which will be modified to reflect the dom changes, after the markup was applied (only if activated via options) 1406 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1407 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1408 * @param options JS object, with the following boolean properties: setRangeObject2NewMarkup, setRangeObject2NextSibling, setRangeObject2PreviousSibling 1409 * @return void 1410 * @hide 1411 */ 1412 applyMarkup: function (selectionTree, rangeObject, markupObject, tagComparator, options) { 1413 var optimizedSelectionTree, i, el, breakpoint; 1414 options = options || {}; 1415 // first same tags from within fully selected nodes for removal 1416 this.prepareForRemoval(selectionTree, markupObject, tagComparator); 1417 1418 // first let's optimize the selection Tree in useful groups which can be wrapped together 1419 optimizedSelectionTree = this.optimizeSelectionTree4Markup(selectionTree, markupObject, tagComparator); 1420 breakpoint = true; 1421 1422 // now iterate over grouped elements and either recursively dive into object or wrap it as a whole 1423 for (i = 0; i < optimizedSelectionTree.length; i++) { 1424 el = optimizedSelectionTree[i]; 1425 if (el.wrappable) { 1426 this.wrapMarkupAroundSelectionTree(el.elements, rangeObject, markupObject, tagComparator, options); 1427 } else { 1428 Aloha.Log.debug(this, 'dive further into non-wrappable object'); 1429 this.applyMarkup(el.element.children, rangeObject, markupObject, tagComparator, options); 1430 } 1431 } 1432 }, 1433 1434 /** 1435 * returns the type of the given markup (trying to match HTML5) 1436 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1437 * @return string name of the markup type 1438 * @hide 1439 */ 1440 getMarkupType: function (markupObject) { 1441 var nn = jQuery(markupObject)[0].nodeName.toLowerCase(); 1442 if (markupObject.outerHtml) { 1443 Aloha.Log.debug(this, 'Node name detected: ' + nn + ' for: ' + markupObject.outerHtml()); 1444 } 1445 if (nn == '#text') { 1446 return 'textNode'; 1447 } 1448 if (this.replacingElements[nn]) { 1449 return 'sectionOrGroupingContent'; 1450 } 1451 if (this.tagHierarchy[nn]) { 1452 return 'textLevelSemantics'; 1453 } 1454 Aloha.Log.warn(this, 'unknown markup passed to this.getMarkupType(...): ' + markupObject.outerHtml()); 1455 }, 1456 1457 /** 1458 * returns the standard tag comparator for the given markup object 1459 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1460 * @return function tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1461 * @hide 1462 */ 1463 getStandardTagComparator: function (markupObject) { 1464 var that = this, 1465 result; 1466 switch (this.getMarkupType(markupObject)) { 1467 case 'textNode': 1468 result = function (p1, p2) { 1469 return false; 1470 }; 1471 break; 1472 1473 case 'sectionOrGroupingContent': 1474 result = function (domobj, markupObject) { 1475 return that.standardSectionsAndGroupingContentComparator(domobj, markupObject); 1476 }; 1477 break; 1478 1479 //case 'textLevelSemantics' covered by default 1480 default: 1481 result = function (domobj, markupObject) { 1482 return that.standardTextLevelSemanticsComparator(domobj, markupObject); 1483 }; 1484 break; 1485 } 1486 return result; 1487 }, 1488 1489 /** 1490 * searches for fully selected equal markup tags 1491 * @param selectionTree SelectionTree Object markup should be applied to 1492 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1493 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1494 * @return void 1495 * @hide 1496 */ 1497 prepareForRemoval: function (selectionTree, markupObject, tagComparator) { 1498 var that = this, i, el; 1499 1500 // check if a comparison method was passed as parameter ... 1501 if (typeof tagComparator !== 'undefined' && typeof tagComparator !== 'function') { 1502 Aloha.Log.error(this, 'parameter tagComparator is not a function'); 1503 } 1504 // ... if not use this as standard tag comparison method 1505 if (typeof tagComparator === 'undefined') { 1506 tagComparator = this.getStandardTagComparator(markupObject); 1507 } 1508 for (i = 0; i < selectionTree.length; i++) { 1509 el = selectionTree[i]; 1510 if (el.domobj && (el.selection == 'full' || (el.selection == 'partial' && markupObject.isReplacingElement))) { 1511 // mark for removal 1512 if (el.domobj.nodeType === 1 && tagComparator(el.domobj, markupObject)) { 1513 Aloha.Log.debug(this, 'Marking for removal: ' + el.domobj.nodeName); 1514 jQuery(el.domobj).addClass('preparedForRemoval'); 1515 } 1516 } 1517 if (el.selection != 'none' && el.children.length > 0) { 1518 this.prepareForRemoval(el.children, markupObject, tagComparator); 1519 } 1520 1521 } 1522 }, 1523 1524 /** 1525 * searches for fully selected equal markup tags 1526 * @param selectionTree SelectionTree Object markup should be applied to 1527 * @param rangeObject Aloha rangeObject the markup will be applied to 1528 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1529 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used 1530 * @param options JS object, with the following boolean properties: setRangeObject2NewMarkup, setRangeObject2NextSibling, setRangeObject2PreviousSibling 1531 * @return void 1532 * @hide 1533 */ 1534 wrapMarkupAroundSelectionTree: function (selectionTree, rangeObject, markupObject, tagComparator, options) { 1535 // first let's find out if theoretically the whole selection can be wrapped with one tag and save it for later use 1536 var objects2wrap = [], // // this will be used later to collect objects 1537 j = -1, // internal counter, 1538 breakpoint = true, 1539 preText = '', 1540 postText = '', 1541 prevOrNext, 1542 textNode2Start, 1543 textnodes, 1544 newMarkup, 1545 i, 1546 el, 1547 middleText; 1548 1549 Aloha.Log.debug(this, 'The formatting <' + markupObject[0].tagName + '> will be wrapped around the selection'); 1550 1551 // now lets iterate over the elements 1552 for (i = 0; i < selectionTree.length; i++) { 1553 el = selectionTree[i]; 1554 1555 // check if markup is allowed inside the elements parent 1556 if (el.domobj && !this.canTag1WrapTag2(el.domobj.parentNode.tagName.toLowerCase(), markupObject[0].tagName.toLowerCase())) { 1557 Aloha.Log.info(this, 'Skipping the wrapping of <' + markupObject[0].tagName.toLowerCase() + '> because this tag is not allowed inside <' + el.domobj.parentNode.tagName.toLowerCase() + '>'); 1558 continue; 1559 } 1560 1561 // skip empty text nodes 1562 if (el.domobj && el.domobj.nodeType === 3 && jQuery.trim(el.domobj.nodeValue).length === 0) { 1563 continue; 1564 } 1565 1566 // partial element, can either be a textnode and therefore be wrapped (at least partially) 1567 // or can be a nodeType == 1 (tag) which must be dived into 1568 if (el.domobj && el.selection == 'partial' && !markupObject.isReplacingElement) { 1569 if (el.startOffset !== undefined && el.endOffset === undefined) { 1570 j++; 1571 preText += el.domobj.data.substr(0, el.startOffset); 1572 el.domobj.data = el.domobj.data.substr(el.startOffset, el.domobj.data.length - el.startOffset); 1573 objects2wrap[j] = el.domobj; 1574 } else if (el.endOffset !== undefined && el.startOffset === undefined) { 1575 j++; 1576 postText += el.domobj.data.substr(el.endOffset, el.domobj.data.length - el.endOffset); 1577 el.domobj.data = el.domobj.data.substr(0, el.endOffset); 1578 objects2wrap[j] = el.domobj; 1579 } else if (el.endOffset !== undefined && el.startOffset !== undefined) { 1580 if (el.startOffset == el.endOffset) { // do not wrap empty selections 1581 Aloha.Log.debug(this, 'skipping empty selection'); 1582 continue; 1583 } 1584 j++; 1585 preText += el.domobj.data.substr(0, el.startOffset); 1586 middleText = el.domobj.data.substr(el.startOffset, el.endOffset - el.startOffset); 1587 postText += el.domobj.data.substr(el.endOffset, el.domobj.data.length - el.endOffset); 1588 el.domobj.data = middleText; 1589 objects2wrap[j] = el.domobj; 1590 } else { 1591 // a partially selected item without selectionStart/EndOffset is a nodeType 1 Element on the way to the textnode 1592 Aloha.Log.debug(this, 'diving into object'); 1593 this.applyMarkup(el.children, rangeObject, markupObject, tagComparator, options); 1594 } 1595 } 1596 // fully selected dom elements can be wrapped as whole element 1597 if (el.domobj && (el.selection == 'full' || (el.selection == 'partial' && markupObject.isReplacingElement))) { 1598 j++; 1599 objects2wrap[j] = el.domobj; 1600 } 1601 } 1602 1603 if (objects2wrap.length > 0) { 1604 // wrap collected DOM object with markupObject 1605 objects2wrap = jQuery(objects2wrap); 1606 1607 // make a fix for text nodes in <li>'s in ie 1608 jQuery.each(objects2wrap, function (index, element) { 1609 if (jQuery.browser.msie && element.nodeType == 3 && !element.nextSibling && !element.previousSibling && element.parentNode && element.parentNode.nodeName.toLowerCase() == 'li') { 1610 element.data = jQuery.trim(element.data); 1611 } 1612 }); 1613 1614 newMarkup = objects2wrap.wrapAll(markupObject).parent(); 1615 newMarkup.before(preText).after(postText); 1616 1617 if (options.setRangeObject2NewMarkup) { // this is used, when markup is added to normal/normal Text 1618 textnodes = objects2wrap.textNodes(); 1619 1620 if (textnodes.index(rangeObject.startContainer) != -1) { 1621 rangeObject.startOffset = 0; 1622 } 1623 if (textnodes.index(rangeObject.endContainer) != -1) { 1624 rangeObject.endOffset = rangeObject.endContainer.length; 1625 } 1626 breakpoint = true; 1627 } 1628 if (options.setRangeObject2NextSibling) { 1629 prevOrNext = true; 1630 textNode2Start = newMarkup.textNodes(true).last()[0]; 1631 if (objects2wrap.index(rangeObject.startContainer) != -1) { 1632 rangeObject.startContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start); 1633 rangeObject.startOffset = 0; 1634 } 1635 if (objects2wrap.index(rangeObject.endContainer) != -1) { 1636 rangeObject.endContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start); 1637 rangeObject.endOffset = rangeObject.endOffset - textNode2Start.length; 1638 } 1639 } 1640 if (options.setRangeObject2PreviousSibling) { 1641 prevOrNext = false; 1642 textNode2Start = newMarkup.textNodes(true).first()[0]; 1643 if (objects2wrap.index(rangeObject.startContainer) != -1) { 1644 rangeObject.startContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start); 1645 rangeObject.startOffset = 0; 1646 } 1647 if (objects2wrap.index(rangeObject.endContainer) != -1) { 1648 rangeObject.endContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start); 1649 rangeObject.endOffset = rangeObject.endContainer.length; 1650 } 1651 } 1652 } 1653 }, 1654 1655 /** 1656 * takes a text node and return either the next recursive text node sibling or the previous 1657 * @param previousOrNext boolean, false for previous, true for next sibling 1658 * @param commonAncestorContainer dom object to be used as root for the sibling search 1659 * @param currentTextNode dom object of the originating text node 1660 * @return dom object of the sibling text node 1661 * @hide 1662 */ 1663 getTextNodeSibling: function (previousOrNext, commonAncestorContainer, currentTextNode) { 1664 var textNodes = jQuery(commonAncestorContainer).textNodes(true), newIndex, index; 1665 1666 index = textNodes.index(currentTextNode); 1667 if (index == -1) { // currentTextNode was not found 1668 return false; 1669 } 1670 newIndex = index + (!previousOrNext ? -1 : 1); 1671 return textNodes[newIndex] || false; 1672 }, 1673 1674 /** 1675 * takes a selection tree and groups it into markup wrappable selection trees 1676 * @param selectionTree rangeObject selection tree 1677 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); ) 1678 * @return JS array of wrappable selection trees 1679 * @hide 1680 */ 1681 optimizeSelectionTree4Markup: function (selectionTree, markupObject, tagComparator) { 1682 var groupMap = [], 1683 outerGroupIndex = 0, 1684 innerGroupIndex = 0, 1685 that = this, 1686 i, 1687 j, 1688 endPosition, 1689 startPosition; 1690 1691 if (typeof tagComparator === 'undefined') { 1692 tagComparator = function (domobj, markupObject) { 1693 return that.standardTextLevelSemanticsComparator(markupObject); 1694 }; 1695 } 1696 for (i = 0; i < selectionTree.length; i++) { 1697 // we are just interested in selected item, but not in non-selected items 1698 if (selectionTree[i].domobj && selectionTree[i].selection != 'none') { 1699 if (markupObject.isReplacingElement && tagComparator(markupObject[0], jQuery(selectionTree[i].domobj))) { 1700 if (groupMap[outerGroupIndex] !== undefined) { 1701 outerGroupIndex++; 1702 } 1703 groupMap[outerGroupIndex] = {}; 1704 groupMap[outerGroupIndex].wrappable = true; 1705 groupMap[outerGroupIndex].elements = []; 1706 groupMap[outerGroupIndex].elements[innerGroupIndex] = selectionTree[i]; 1707 outerGroupIndex++; 1708 1709 } else if (this.canMarkupBeApplied2ElementAsWhole([selectionTree[i]], markupObject)) { 1710 // now check, if the children of our item could be wrapped all together by the markup object 1711 // if yes, add it to the current group 1712 if (groupMap[outerGroupIndex] === undefined) { 1713 groupMap[outerGroupIndex] = {}; 1714 groupMap[outerGroupIndex].wrappable = true; 1715 groupMap[outerGroupIndex].elements = []; 1716 } 1717 if (markupObject.isReplacingElement) { // && selectionTree[i].domobj.nodeType === 3 1718 /* we found the node to wrap for a replacing element. however there might 1719 * be siblings which should be included as well 1720 * although they are actually not selected. example: 1721 * li 1722 * |-textNode ( .selection = 'none') 1723 * |-textNode (cursor inside, therefor .selection = 'partial') 1724 * |-textNode ( .selection = 'none') 1725 * 1726 * in this case it would be useful to select the previous and following textNodes as well (they might result from a previous DOM manipulation) 1727 * Think about other cases, where the parent is the Editable. In this case we propably only want to select from and until the next <br /> ?? 1728 * .... many possibilities, here I realize the two described cases 1729 */ 1730 1731 // first find start element starting from the current element going backwards until sibling 0 1732 startPosition = i; 1733 for (j = i - 1; j >= 0; j--) { 1734 if (this.canMarkupBeApplied2ElementAsWhole([selectionTree[j]], markupObject) && this.isMarkupAllowedToStealSelectionTreeElement(selectionTree[j], markupObject)) { 1735 startPosition = j; 1736 } else { 1737 break; 1738 } 1739 } 1740 1741 // now find the end element starting from the current element going forward until the last sibling 1742 endPosition = i; 1743 for (j = i + 1; j < selectionTree.length; j++) { 1744 if (this.canMarkupBeApplied2ElementAsWhole([selectionTree[j]], markupObject) && this.isMarkupAllowedToStealSelectionTreeElement(selectionTree[j], markupObject)) { 1745 endPosition = j; 1746 } else { 1747 break; 1748 } 1749 } 1750 1751 // now add the elements to the groupMap 1752 innerGroupIndex = 0; 1753 for (j = startPosition; j <= endPosition; j++) { 1754 groupMap[outerGroupIndex].elements[innerGroupIndex] = selectionTree[j]; 1755 groupMap[outerGroupIndex].elements[innerGroupIndex].selection = 'full'; 1756 innerGroupIndex++; 1757 } 1758 innerGroupIndex = 0; 1759 } else { 1760 // normal text level semantics object, no siblings need to be selected 1761 groupMap[outerGroupIndex].elements[innerGroupIndex] = selectionTree[i]; 1762 innerGroupIndex++; 1763 } 1764 } else { 1765 // if no, isolate it in its own group 1766 if (groupMap[outerGroupIndex] !== undefined) { 1767 outerGroupIndex++; 1768 } 1769 groupMap[outerGroupIndex] = {}; 1770 groupMap[outerGroupIndex].wrappable = false; 1771 groupMap[outerGroupIndex].element = selectionTree[i]; 1772 innerGroupIndex = 0; 1773 outerGroupIndex++; 1774 } 1775 } 1776 } 1777 return groupMap; 1778 }, 1779 1780 /** 1781 * very tricky method, which decides, if a certain markup (normally a replacing markup element like p, h1, blockquote) 1782 * is allowed to extend the user selection to other dom objects (represented as selectionTreeElement) 1783 * to understand the purpose: if the user selection is collapsed inside e.g. some text, which is currently not 1784 * wrapped by the markup to be applied, and therefor the markup does not have an equal markup to replace, then the DOM 1785 * manipulator has to decide which objects to wrap. real example: 1786 * <div> 1787 * <h1>headline</h1> 1788 * some text blabla bla<br> 1789 * more text HERE THE | CURSOR BLINKING and <b>even more bold text</b> 1790 * </div> 1791 * when the user now wants to apply e.g. a <p> tag, what will be wrapped? it could be useful if the manipulator would actually 1792 * wrap everything inside the div except the <h1>. but for this purpose someone has to decide, if the markup is 1793 * allowed to wrap certain dom elements in this case the question would be, if the <p> is allowed to wrap 1794 * textNodes, <br> and <b> and <h1>. therefore this tricky method should answer the question for those 3 elements 1795 * with true, but for for the <h1> it should return false. and since the method does not know this, there is a configuration 1796 * for this 1797 * 1798 * @param selectionTree rangeObject selection tree element (only one, not an array of) 1799 * @param markupObject lowercase string of the tag to be verified (e.g. "b") 1800 * @return true if the markup is allowed to wrap the selection tree element, false otherwise 1801 * @hide 1802 */ 1803 isMarkupAllowedToStealSelectionTreeElement: function (selectionTreeElement, markupObject) { 1804 if (!selectionTreeElement.domobj) { 1805 return false; 1806 } 1807 var maybeTextNodeName = selectionTreeElement.domobj.nodeName.toLowerCase(), 1808 nodeName = (maybeTextNodeName == '#text') ? 'textNode' : maybeTextNodeName, 1809 markupName = markupObject[0].nodeName.toLowerCase(), 1810 elemMap = this.allowedToStealElements[markupName]; 1811 return elemMap && elemMap[nodeName]; 1812 }, 1813 1814 /** 1815 * checks if a selection can be completey wrapped by a certain html tags (helper method for this.optimizeSelectionTree4Markup 1816 * @param selectionTree rangeObject selection tree 1817 * @param markupObject lowercase string of the tag to be verified (e.g. "b") 1818 * @return true if selection can be applied as whole, false otherwise 1819 * @hide 1820 */ 1821 canMarkupBeApplied2ElementAsWhole: function (selectionTree, markupObject) { 1822 var htmlTag, i, el, returnVal; 1823 1824 if (markupObject.jquery) { 1825 htmlTag = markupObject[0].tagName; 1826 } 1827 if (markupObject.tagName) { 1828 htmlTag = markupObject.tagName; 1829 } 1830 1831 returnVal = true; 1832 for (i = 0; i < selectionTree.length; i++) { 1833 el = selectionTree[i]; 1834 if (el.domobj && (el.selection != "none" || markupObject.isReplacingElement)) { 1835 // Aloha.Log.debug(this, 'Checking, if <' + htmlTag + '> can be applied to ' + el.domobj.nodeName); 1836 if (!this.canTag1WrapTag2(htmlTag, el.domobj.nodeName)) { 1837 return false; 1838 } 1839 if (el.children.length > 0 && !this.canMarkupBeApplied2ElementAsWhole(el.children, markupObject)) { 1840 return false; 1841 } 1842 } 1843 } 1844 return returnVal; 1845 }, 1846 1847 /** 1848 * checks if a tag 1 (first parameter) can wrap tag 2 (second parameter). 1849 * IMPORTANT: the method does not verify, if there have to be other tags in between 1850 * Example: this.canTag1WrapTag2("table", "td") will return true, because the method does not take into account, that there has to be a "tr" in between 1851 * @param t1 string: tagname of outer tag to verify, e.g. "b" 1852 * @param t2 string: tagname of inner tag to verify, e.g. "b" 1853 * @return true if tag 1 can wrap tag 2, false otherwise 1854 * @hide 1855 */ 1856 canTag1WrapTag2: function (t1, t2) { 1857 t1 = (t1 == '#text') ? 'textNode' : t1.toLowerCase(); 1858 t2 = (t2 == '#text') ? 'textNode' : t2.toLowerCase(); 1859 var t1Map = this.tagHierarchy[t1]; 1860 if (!t1Map) { 1861 return true; 1862 } 1863 if (!this.tagHierarchy[t2]) { 1864 return true; 1865 } 1866 return t1Map[t2]; 1867 }, 1868 1869 /** 1870 * Check whether it is allowed to insert the given tag at the start of the 1871 * current selection. This method will check whether the markup effective for 1872 * the start and outside of the editable part (starting with the editable tag 1873 * itself) may wrap the given tag. 1874 * @param tagName {String} name of the tag which shall be inserted 1875 * @return true when it is allowed to insert that tag, false if not 1876 * @hide 1877 */ 1878 mayInsertTag: function (tagName) { 1879 var i; 1880 if (typeof this.rangeObject.unmodifiableMarkupAtStart == 'object') { 1881 // iterate over all DOM elements outside of the editable part 1882 for (i = 0; i < this.rangeObject.unmodifiableMarkupAtStart.length; ++i) { 1883 // check whether an element may not wrap the given 1884 if (!this.canTag1WrapTag2(this.rangeObject.unmodifiableMarkupAtStart[i].nodeName, tagName)) { 1885 // found a DOM element which forbids to insert the given tag, we are done 1886 return false; 1887 } 1888 } 1889 1890 // all of the found DOM elements allow inserting the given tag 1891 return true; 1892 } 1893 Aloha.Log.warn(this, 'Unable to determine whether tag ' + tagName + ' may be inserted'); 1894 return true; 1895 }, 1896 1897 /** 1898 * String representation 1899 * @return "Aloha.Selection" 1900 * @hide 1901 */ 1902 toString: function () { 1903 return 'Aloha.Selection'; 1904 }, 1905 1906 /** 1907 * @namespace Aloha.Selection 1908 1909 * @class SelectionRange 1910 * @extends GENTICS.Utils.RangeObject 1911 * Constructor for a range object. 1912 * Optionally you can pass in a range object that's properties will be assigned to the new range object. 1913 * @param rangeObject A range object thats properties will be assigned to the new range object. 1914 * @constructor 1915 */ 1916 SelectionRange: GENTICS.Utils.RangeObject.extend({ 1917 _constructor: function (rangeObject) { 1918 this._super(rangeObject); 1919 // If a range object was passed in we apply the values to the new range object 1920 if (rangeObject) { 1921 if (rangeObject.commonAncestorContainer) { 1922 this.commonAncestorContainer = rangeObject.commonAncestorContainer; 1923 } 1924 if (rangeObject.selectionTree) { 1925 this.selectionTree = rangeObject.selectionTree; 1926 } 1927 if (rangeObject.limitObject) { 1928 this.limitObject = rangeObject.limitObject; 1929 } 1930 if (rangeObject.markupEffectiveAtStart) { 1931 this.markupEffectiveAtStart = rangeObject.markupEffectiveAtStart; 1932 } 1933 if (rangeObject.unmodifiableMarkupAtStart) { 1934 this.unmodifiableMarkupAtStart = rangeObject.unmodifiableMarkupAtStart; 1935 } 1936 if (rangeObject.splitObject) { 1937 this.splitObject = rangeObject.splitObject; 1938 } 1939 } 1940 }, 1941 1942 /** 1943 * DOM object of the common ancestor from startContainer and endContainer 1944 * @hide 1945 */ 1946 commonAncestorContainer: undefined, 1947 1948 /** 1949 * The selection tree 1950 * @hide 1951 */ 1952 selectionTree: undefined, 1953 1954 /** 1955 * Array of DOM objects effective for the start container and inside the 1956 * editable part (inside the limit object). relevant for the button status 1957 * @hide 1958 */ 1959 markupEffectiveAtStart: [], 1960 1961 /** 1962 * Array of DOM objects effective for the start container, which lies 1963 * outside of the editable portion (starting with the limit object) 1964 * @hide 1965 */ 1966 unmodifiableMarkupAtStart: [], 1967 1968 /** 1969 * DOM object being the limit for all markup relevant activities 1970 * @hide 1971 */ 1972 limitObject: undefined, 1973 1974 /** 1975 * DOM object being split when enter key gets hit 1976 * @hide 1977 */ 1978 splitObject: undefined, 1979 1980 /** 1981 * Sets the visible selection in the Browser based on the range object. 1982 * If the selection is collapsed, this will result in a blinking cursor, 1983 * otherwise in a text selection. 1984 * @method 1985 */ 1986 select: function () { 1987 // Call Utils' select() 1988 this._super(); 1989 1990 // update the selection 1991 Aloha.Selection.updateSelection(); 1992 }, 1993 1994 /** 1995 * Method to update a range object internally 1996 * @param commonAncestorContainer (DOM Object); optional Parameter; if set, the parameter 1997 * will be used instead of the automatically calculated CAC 1998 * @return void 1999 * @hide 2000 */ 2001 update: function (commonAncestorContainer) { 2002 this.updatelimitObject(); 2003 this.updateMarkupEffectiveAtStart(); 2004 this.updateCommonAncestorContainer(commonAncestorContainer); 2005 2006 // reset the selectiontree (must be recalculated) 2007 this.selectionTree = undefined; 2008 }, 2009 2010 /** 2011 * Get the selection tree for this range 2012 * TODO: remove this (was moved to range.js) 2013 * @return selection tree 2014 * @hide 2015 */ 2016 getSelectionTree: function () { 2017 // if not yet calculated, do this now 2018 if (!this.selectionTree) { 2019 this.selectionTree = Aloha.Selection.getSelectionTree(this); 2020 } 2021 2022 return this.selectionTree; 2023 }, 2024 2025 /** 2026 * TODO: move this to range.js 2027 * Get an array of domobj (in dom tree order) of siblings of the given domobj, which are contained in the selection 2028 * @param domobj dom object to start with 2029 * @return array of siblings of the given domobj, which are also selected 2030 * @hide 2031 */ 2032 getSelectedSiblings: function (domobj) { 2033 var selectionTree = this.getSelectionTree(); 2034 2035 return this.recursionGetSelectedSiblings(domobj, selectionTree); 2036 }, 2037 2038 /** 2039 * TODO: move this to range.js 2040 * Recursive method to find the selected siblings of the given domobj (which should be selected as well) 2041 * @param domobj dom object for which the selected siblings shall be found 2042 * @param selectionTree current level of the selection tree 2043 * @return array of selected siblings of dom objects or false if none found 2044 * @hide 2045 */ 2046 recursionGetSelectedSiblings: function (domobj, selectionTree) { 2047 var selectedSiblings = false, 2048 foundObj = false, 2049 i; 2050 2051 for (i = 0; i < selectionTree.length; ++i) { 2052 if (selectionTree[i].domobj === domobj) { 2053 foundObj = true; 2054 selectedSiblings = []; 2055 } else if (!foundObj && selectionTree[i].children) { 2056 // do the recursion 2057 selectedSiblings = this.recursionGetSelectedSiblings(domobj, selectionTree[i].children); 2058 if (selectedSiblings !== false) { 2059 break; 2060 } 2061 } else if (foundObj && selectionTree[i].domobj && selectionTree[i].selection != 'collapsed' && selectionTree[i].selection != 'none') { 2062 selectedSiblings.push(selectionTree[i].domobj); 2063 } else if (foundObj && selectionTree[i].selection == 'none') { 2064 break; 2065 } 2066 } 2067 2068 return selectedSiblings; 2069 }, 2070 2071 /** 2072 * TODO: move this to range.js 2073 * Method updates member var markupEffectiveAtStart and splitObject, which is relevant primarily for button status and enter key behaviour 2074 * @return void 2075 * @hide 2076 */ 2077 updateMarkupEffectiveAtStart: function () { 2078 // reset the current markup 2079 this.markupEffectiveAtStart = []; 2080 this.unmodifiableMarkupAtStart = []; 2081 2082 var parents = this.getStartContainerParents(), 2083 limitFound = false, 2084 splitObjectWasSet, 2085 i, 2086 el; 2087 2088 for (i = 0; i < parents.length; i++) { 2089 el = parents[i]; 2090 if (!limitFound && (el !== this.limitObject)) { 2091 this.markupEffectiveAtStart[i] = el; 2092 if (!splitObjectWasSet && GENTICS.Utils.Dom.isSplitObject(el)) { 2093 splitObjectWasSet = true; 2094 this.splitObject = el; 2095 } 2096 } else { 2097 limitFound = true; 2098 this.unmodifiableMarkupAtStart.push(el); 2099 } 2100 } 2101 if (!splitObjectWasSet) { 2102 this.splitObject = false; 2103 } 2104 return; 2105 }, 2106 2107 /** 2108 * TODO: remove this 2109 * Method updates member var markupEffectiveAtStart, which is relevant primarily for button status 2110 * @return void 2111 * @hide 2112 */ 2113 updatelimitObject: function () { 2114 if (Aloha.editables && Aloha.editables.length > 0) { 2115 var parents = this.getStartContainerParents(), 2116 editables = Aloha.editables, 2117 i, 2118 el, 2119 j, 2120 editable; 2121 for (i = 0; i < parents.length; i++) { 2122 el = parents[i]; 2123 for (j = 0; j < editables.length; j++) { 2124 editable = editables[j].obj[0]; 2125 if (el === editable) { 2126 this.limitObject = el; 2127 return true; 2128 } 2129 } 2130 } 2131 } 2132 this.limitObject = jQuery('body'); 2133 return true; 2134 }, 2135 2136 /** 2137 * string representation of the range object 2138 * @param verbose set to true for verbose output 2139 * @return string representation of the range object 2140 * @hide 2141 */ 2142 toString: function (verbose) { 2143 if (!verbose) { 2144 return 'Aloha.Selection.SelectionRange'; 2145 } 2146 return 'Aloha.Selection.SelectionRange {start [' + this.startContainer.nodeValue + '] offset ' + this.startOffset + ', end [' + this.endContainer.nodeValue + '] offset ' + this.endOffset + '}'; 2147 } 2148 2149 }) // SelectionRange 2150 2151 }); // Selection 2152 2153 2154 /** 2155 * This method implements an ugly workaround for a selection problem in ie: 2156 * when the cursor shall be placed at the end of a text node in a li element, that is followed by a nested list, 2157 * the selection would always snap into the first li of the nested list 2158 * therefore, we make sure that the text node ends with a space and place the cursor right before it 2159 */ 2160 function nestedListInIEWorkaround(range) { 2161 var nextSibling; 2162 if (jQuery.browser.msie && range.startContainer === range.endContainer && range.startOffset === range.endOffset && range.startContainer.nodeType == 3 && range.startOffset == range.startContainer.data.length && range.startContainer.nextSibling) { 2163 nextSibling = range.startContainer.nextSibling; 2164 if ('OL' === nextSibling.nodeName || 'UL' === nextSibling.nodeName) { 2165 if (range.startContainer.data[range.startContainer.data.length - 1] == ' ') { 2166 range.startOffset = range.endOffset = range.startOffset - 1; 2167 } else { 2168 range.startContainer.data = range.startContainer.data + ' '; 2169 } 2170 } 2171 } 2172 } 2173 2174 function correctRange(range) { 2175 nestedListInIEWorkaround(range); 2176 return range; 2177 } 2178 2179 /** 2180 * Implements Selection http://html5.org/specs/dom-range.html#selection 2181 * @namespace Aloha 2182 * @class Selection This singleton class always represents the 2183 * current user selection 2184 * @singleton 2185 */ 2186 var AlohaSelection = Class.extend({ 2187 2188 _constructor: function (nativeSelection) { 2189 2190 this._nativeSelection = nativeSelection; 2191 this.ranges = []; 2192 2193 // will remember if urged to not change the selection 2194 this.preventChange = false; 2195 2196 }, 2197 2198 /** 2199 * Returns the element that contains the start of the selection. Returns null if there's no selection. 2200 * @readonly 2201 * @type Node 2202 */ 2203 anchorNode: null, 2204 2205 /** 2206 * Returns the offset of the start of the selection relative to the element that contains the start 2207 * of the selection. Returns 0 if there's no selection. 2208 * @readonly 2209 * @type int 2210 */ 2211 anchorOffset: 0, 2212 2213 /** 2214 * Returns the element that contains the end of the selection. 2215 * Returns null if there's no selection. 2216 * @readonly 2217 * @type Node 2218 */ 2219 focusNode: null, 2220 2221 /** 2222 * Returns the offset of the end of the selection relative to the element that contains the end 2223 * of the selection. Returns 0 if there's no selection. 2224 * @readonly 2225 * @type int 2226 */ 2227 focusOffset: 0, 2228 2229 /** 2230 * Returns true if there's no selection or if the selection is empty. Otherwise, returns false. 2231 * @readonly 2232 * @type boolean 2233 */ 2234 isCollapsed: false, 2235 2236 /** 2237 * Returns the number of ranges in the selection. 2238 * @readonly 2239 * @type int 2240 */ 2241 rangeCount: 0, 2242 2243 /** 2244 * Replaces the selection with an empty one at the given position. 2245 * @throws a WRONG_DOCUMENT_ERR exception if the given node is in a different document. 2246 * @param parentNode Node of new selection 2247 * @param offest offest of new Selection in parentNode 2248 * @void 2249 */ 2250 collapse: function (parentNode, offset) { 2251 this._nativeSelection.collapse(parentNode, offset); 2252 }, 2253 2254 /** 2255 * Replaces the selection with an empty one at the position of the start of the current selection. 2256 * @throws an INVALID_STATE_ERR exception if there is no selection. 2257 * @void 2258 */ 2259 collapseToStart: function () { 2260 throw "NOT_IMPLEMENTED"; 2261 }, 2262 2263 /** 2264 * @void 2265 */ 2266 extend: function (parentNode, offset) { 2267 2268 }, 2269 2270 /** 2271 * @param alter DOMString 2272 * @param direction DOMString 2273 * @param granularity DOMString 2274 * @void 2275 */ 2276 modify: function (alter, direction, granularity) { 2277 2278 }, 2279 2280 /** 2281 * Replaces the selection with an empty one at the position of the end of the current selection. 2282 * @throws an INVALID_STATE_ERR exception if there is no selection. 2283 * @void 2284 */ 2285 collapseToEnd: function () { 2286 this._nativeSelection.collapseToEnd(); 2287 }, 2288 2289 /** 2290 * Replaces the selection with one that contains all the contents of the given element. 2291 * @throws a WRONG_DOCUMENT_ERR exception if the given node is in a different document. 2292 * @param parentNode Node the Node fully select 2293 * @void 2294 */ 2295 selectAllChildren: function (parentNode) { 2296 throw "NOT_IMPLEMENTED"; 2297 }, 2298 2299 /** 2300 * Deletes the contents of the selection 2301 */ 2302 deleteFromDocument: function () { 2303 throw "NOT_IMPLEMENTED"; 2304 }, 2305 2306 /** 2307 * NB! 2308 * We have serious problem in IE. 2309 * The range that we get in IE is not the same as the range we had set, 2310 * so even if we normalize it during getRangeAt, in IE, we will be 2311 * correcting the range to the "correct" place, but still not the place 2312 * where it was originally set. 2313 * 2314 * Returns the given range. 2315 * The getRangeAt(index) method returns the indexth range in the list. 2316 * NOTE: Aloha Editor only support 1 range! index can only be 0 2317 * @throws INDEX_SIZE_ERR DOM exception if index is less than zero or 2318 * greater or equal to the value returned by the rangeCount. 2319 * @param index int 2320 * @return Range return the selected range from index 2321 */ 2322 getRangeAt: function (index) { 2323 return correctRange(this._nativeSelection.getRangeAt(index)); 2324 //if ( index < 0 || this.rangeCount ) { 2325 // throw "INDEX_SIZE_ERR DOM"; 2326 //} 2327 //return this._ranges[index]; 2328 }, 2329 2330 /** 2331 * Adds the given range to the selection. 2332 * The addRange(range) method adds the given range Range object to the list of 2333 * selections, at the end (so the newly added range is the new last range). 2334 * NOTE: Aloha Editor only support 1 range! The added range will replace the 2335 * range at index 0 2336 * see http://html5.org/specs/dom-range.html#selection note about addRange 2337 * @throws an INVALID_NODE_TYPE_ERR exception if the given Range has a boundary point 2338 * node that's not a Text or Element node, and an INVALID_MODIFICATION_ERR exception 2339 * if it has a boundary point node that doesn't descend from a Document. 2340 * @param range Range adds the range to the selection 2341 * @void 2342 */ 2343 addRange: function (range) { 2344 // set readonly attributes 2345 this._nativeSelection.addRange(range); 2346 // We will correct the range after rangy has processed the native 2347 // selection range, so that our correction will be the final fix on 2348 // the range according to the guarentee's that Aloha wants to make 2349 this._nativeSelection._ranges[0] = correctRange(range); 2350 2351 // make sure, the old Aloha selection will be updated (until all implementations use the new AlohaSelection) 2352 Aloha.Selection.updateSelection(); 2353 }, 2354 2355 /** 2356 * Removes the given range from the selection, if the range was one of the ones in the selection. 2357 * NOTE: Aloha Editor only support 1 range! The added range will replace the 2358 * range at with index 0 2359 * @param range Range removes the range from the selection 2360 * @void 2361 */ 2362 removeRange: function (range) { 2363 this._nativeSelection.removeRange(); 2364 }, 2365 2366 /** 2367 * Removes all the ranges in the selection. 2368 * @viod 2369 */ 2370 removeAllRanges: function () { 2371 this._nativeSelection.removeAllRanges(); 2372 }, 2373 2374 /** 2375 * INFO: Method is used for integration with Gentics 2376 * Aloha, has no use otherwise Updates the rangeObject 2377 * according to the current user selection Method is 2378 * always called on selection change 2379 * 2380 * @param event 2381 * jQuery browser event object 2382 * @return true when rangeObject was modified, false 2383 * otherwise 2384 * @hide 2385 */ 2386 refresh: function (event) { 2387 2388 }, 2389 2390 /** 2391 * String representation 2392 * 2393 * @return "Aloha.Selection" 2394 * @hide 2395 */ 2396 toString: function () { 2397 return 'Aloha.Selection'; 2398 }, 2399 2400 getRangeCount: function () { 2401 return this._nativeSelection.rangeCount; 2402 } 2403 2404 }); 2405 2406 /** 2407 * A wrapper for the function of the same name in the rangy core-depdency. 2408 * This function should be preferred as it hides the global rangy object. 2409 * For more information look at the following sites: 2410 * http://html5.org/specs/dom-range.html 2411 * @param window optional - specifices the window to get the selection of 2412 */ 2413 Aloha.getSelection = function (target) { 2414 target = (target !== document || target !== window) ? window : target; 2415 // Aloha.Selection.refresh() 2416 // implement Aloha Selection 2417 // TODO cache 2418 return new AlohaSelection(window.rangy.getSelection(target)); 2419 }; 2420 2421 /** 2422 * A wrapper for the function of the same name in the rangy core-depdency. 2423 * This function should be preferred as it hides the global rangy object. 2424 * Please note: when the range object is not needed anymore, 2425 * invoke the detach method on it. It is currently unknown to me why 2426 * this is required, but that's what it says in the rangy specification. 2427 * For more information look at the following sites: 2428 * http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html 2429 * @param document optional - specifies which document to create the range for 2430 */ 2431 Aloha.createRange = function (givenWindow) { 2432 return window.rangy.createRange(givenWindow); 2433 }; 2434 2435 var selection = new Selection(); 2436 Aloha.Selection = selection; 2437 2438 return selection; 2439 }); 2440