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