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