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