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