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