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