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