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