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