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