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