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