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