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