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