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