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