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