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