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