1 /*! 2 * This file is part of Aloha Editor Project http://aloha-editor.org 3 * Copyright © 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 define( 22 [ 'aloha/core', 'util/class', 'aloha/jquery' ], 23 function( Aloha, Class, jQuery ) { 24 "use strict"; 25 26 var GENTICS = window.GENTICS; 27 28 /** 29 * Markup object 30 */ 31 Aloha.Markup = Class.extend( { 32 33 /** 34 * Key handlers for special key codes 35 */ 36 keyHandlers: {}, 37 38 /** 39 * Add a key handler for the given key code 40 * @param keyCode key code 41 * @param handler handler function 42 */ 43 addKeyHandler: function( keyCode, handler ) { 44 if ( !this.keyHandlers[ keyCode ] ) { 45 this.keyHandlers[ keyCode ] = []; 46 } 47 48 this.keyHandlers[ keyCode ].push( handler ); 49 }, 50 51 insertBreak: function() { 52 var range = Aloha.Selection.rangeObject, 53 onWSIndex, 54 nextTextNode, 55 newBreak; 56 57 if ( !range.isCollapsed() ) { 58 this.removeSelectedMarkup(); 59 } 60 61 newBreak = jQuery( '<br/>' ); 62 GENTICS.Utils.Dom.insertIntoDOM( newBreak, range, Aloha.activeEditable.obj ); 63 64 nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode( 65 newBreak.parent().get( 0 ), 66 GENTICS.Utils.Dom.getIndexInParent( newBreak.get( 0 ) ) + 1, 67 false 68 ); 69 70 if ( nextTextNode ) { 71 // trim leading whitespace 72 73 nonWSIndex = nextTextNode.data.search( /\S/ ); 74 if ( nonWSIndex > 0 ) { 75 nextTextNode.data = nextTextNode.data.substring( nonWSIndex ); 76 } 77 } 78 79 range.startContainer = range.endContainer = newBreak.get( 0 ).parentNode; 80 range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent( newBreak.get( 0 ) ) + 1; 81 range.correctRange(); 82 range.clearCaches(); 83 range.select(); 84 }, 85 86 /** 87 * first method to handle key strokes 88 * @param event DOM event 89 * @param rangeObject as provided by Aloha.Selection.getRangeObject(); 90 * @return "Aloha.Selection" 91 */ 92 preProcessKeyStrokes: function( event ) { 93 if ( event.type !== 'keydown' ) { 94 return false; 95 } 96 97 var rangeObject = Aloha.Selection.rangeObject, 98 handlers, 99 i; 100 101 if ( this.keyHandlers[ event.keyCode ] ) { 102 handlers = this.keyHandlers[ event.keyCode ]; 103 for ( i = 0; i < handlers.length; ++i ) { 104 if ( !handlers[i]( event ) ) { 105 return false; 106 } 107 } 108 } 109 110 // handle left (37) and right (39) keys for block detection 111 if ( event.keyCode === 37 || event.keyCode === 39 ) { 112 return this.processCursor( rangeObject, event.keyCode ); 113 } 114 115 // BACKSPACE 116 117 if ( event.keyCode === 8 ) { 118 event.preventDefault(); // prevent history.back() even on exception 119 Aloha.execCommand( 'delete', false ); 120 return false; 121 } 122 123 // DELETE 124 if ( event.keyCode === 46 ) { 125 Aloha.execCommand( 'forwarddelete', false ); 126 return false; 127 } 128 129 // ENTER 130 if ( event.keyCode === 13 ) { 131 if ( event.shiftKey ) { 132 Aloha.execCommand( 'insertlinebreak', false ); 133 return false; 134 } else { 135 Aloha.execCommand( 'insertparagraph', false ); 136 return false; 137 } 138 } 139 140 return true; 141 }, 142 143 /** 144 * Processing of cursor keys 145 * will currently detect blocks (elements with contenteditable=false) 146 * and selects them (normally the cursor would jump right past them) 147 * 148 * For each block an 'aloha-block-selected' event will be triggered. 149 * 150 * @param range the current range object 151 * @param keyCode keyCode of current keypress 152 * @return false if a block was found to prevent further events, true otherwise 153 */ 154 processCursor: function( range, keyCode ) { 155 var rt = range.getRangeTree(), // RangeTree reference 156 i = 0, 157 cursorLeft = keyCode === 37, 158 cursorRight = keyCode === 39, 159 nextSiblingIsBlock = false, // check whether the next sibling is a block (contenteditable = false) 160 cursorIsWithinBlock = false, // check whether the cursor is positioned within a block (contenteditable = false) 161 cursorAtLastPos = false, // check if the cursor is within the last position of the currently active dom element 162 obj; // will contain references to dom objects 163 164 if ( !range.isCollapsed() ) { 165 return true; 166 } 167 168 for (;i < rt.length; i++) { 169 cursorAtLastPos = range.startOffset === rt[i].domobj.length; 170 if ( !cursorAtLastPos || typeof rt[i].domobj === 'undefined' ) { 171 continue; 172 } 173 174 if ( cursorAtLastPos ) { 175 nextSiblingIsBlock = jQuery( rt[i].domobj.nextSibling ).attr('contenteditable') === 'false'; 176 cursorIsWithinBlock = jQuery( rt[i].domobj ).parents('[contenteditable=false]').length > 0; 177 178 if ( cursorRight && nextSiblingIsBlock ) { 179 obj = rt[i].domobj.nextSibling; 180 GENTICS.Utils.Dom.selectDomNode( obj ); 181 Aloha.trigger( 'aloha-block-selected', obj ); 182 Aloha.Selection.preventSelectionChanged(); 183 return false; 184 } 185 186 if ( cursorLeft && cursorIsWithinBlock ) { 187 obj = jQuery( rt[i].domobj ).parents('[contenteditable=false]').get(0); 188 if ( jQuery( obj ).parent().hasClass('aloha-editable') ) { 189 GENTICS.Utils.Dom.selectDomNode( obj ); 190 Aloha.trigger( 'aloha-block-selected', obj ); 191 Aloha.Selection.preventSelectionChanged(); 192 return false; 193 } 194 } 195 } 196 } 197 }, 198 199 /** 200 * method handling shiftEnter 201 * @param Aloha.Selection.SelectionRange of the current selection 202 * @return void 203 */ 204 processShiftEnter: function( rangeObject ) { 205 this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject ); 206 }, 207 208 /** 209 * method handling Enter 210 * @param Aloha.Selection.SelectionRange of the current selection 211 * @return void 212 */ 213 processEnter: function( rangeObject ) { 214 if ( rangeObject.splitObject ) { 215 // now comes a very evil hack for ie, when the enter is pressed in a text node in an li element, we just append an empty text node 216 // if ( jQuery.browser.msie 217 // && GENTICS.Utils.Dom 218 // .isListElement( rangeObject.splitObject ) ) { 219 // jQuery( rangeObject.splitObject ).append( 220 // jQuery( document.createTextNode( '' ) ) ); 221 // } 222 this.splitRangeObject( rangeObject ); 223 } else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2) 224 this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject ); 225 } 226 }, 227 228 /** 229 * Insert the given html markup at the current selection 230 * @param html html markup to be inserted 231 */ 232 insertHTMLCode: function( html ) { 233 var rangeObject = Aloha.Selection.rangeObject; 234 this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject, jQuery( html ) ); 235 }, 236 237 /** 238 * insert an HTML Break <br /> into current selection 239 * @param Aloha.Selection.SelectionRange of the current selection 240 * @return void 241 */ 242 insertHTMLBreak: function( selectionTree, rangeObject, inBetweenMarkup ) { 243 var i, 244 treeLength, 245 el, 246 jqEl, 247 jqElBefore, 248 jqElAfter, 249 tmpObject, 250 offset, 251 checkObj; 252 253 inBetweenMarkup = inBetweenMarkup ? inBetweenMarkup: jQuery( '<br/>' ); 254 255 for ( i = 0, treeLength = selectionTree.length; i < treeLength; ++i ) { 256 el = selectionTree[ i ]; 257 jqEl = el.domobj ? jQuery( el.domobj ) : undefined; 258 259 if ( el.selection !== 'none' ) { // before cursor, leave this part inside the splitObject 260 if ( el.selection == 'collapsed' ) { 261 // collapsed selection found (between nodes) 262 if ( i > 0 ) { 263 // not at the start, so get the element to the left 264 jqElBefore = jQuery( selectionTree[ i - 1 ].domobj ); 265 266 // and insert the break after it 267 jqElBefore.after( inBetweenMarkup ); 268 269 } else { 270 // at the start, so get the element to the right 271 jqElAfter = jQuery( selectionTree[1].domobj ); 272 273 // and insert the break before it 274 jqElAfter.before( inBetweenMarkup ); 275 } 276 277 // now set the range 278 rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode; 279 rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent( inBetweenMarkup[0] ) + 1; 280 rangeObject.correctRange(); 281 282 } else if ( el.domobj && el.domobj.nodeType === 3 ) { // textNode 283 // when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between 284 if ( el.domobj.nextSibling 285 && el.domobj.nextSibling.nodeType == 1 286 && Aloha.Selection.replacingElements[ 287 el.domobj.nextSibling.nodeName.toLowerCase() 288 ] ) { 289 // TODO check whether this depends on the browser 290 jqEl.after( '<br/>' ); 291 } 292 293 if ( this.needEndingBreak() ) { 294 // when the textnode is the last inside a blocklevel element 295 // (like p, h1, ...) we need to add an additional br as very 296 // last object in the blocklevel element 297 checkObj = el.domobj; 298 299 while ( checkObj ) { 300 if ( checkObj.nextSibling ) { 301 checkObj = false; 302 } else { 303 // go to the parent 304 checkObj = checkObj.parentNode; 305 306 // found a blocklevel or list element, we are done 307 if ( GENTICS.Utils.Dom.isBlockLevelElement( checkObj ) 308 || GENTICS.Utils.Dom.isListElement( checkObj ) ) { 309 break; 310 } 311 312 // reached the limit object, we are done 313 if ( checkObj === rangeObject.limitObject ) { 314 checkObj = false; 315 } 316 } 317 } 318 319 // when we found a blocklevel element, insert a break at the 320 // end. Mark the break so that it is cleaned when the 321 // content is fetched. 322 if ( checkObj ) { 323 jQuery( checkObj ).append( '<br class="aloha-cleanme" />' ); 324 } 325 } 326 327 // insert the break 328 jqEl.between( inBetweenMarkup, el.startOffset ); 329 330 // correct the range 331 // count the number of previous siblings 332 offset = 0; 333 tmpObject = inBetweenMarkup[0]; 334 while ( tmpObject ) { 335 tmpObject = tmpObject.previousSibling; 336 ++offset; 337 } 338 339 rangeObject.startContainer = inBetweenMarkup[0].parentNode; 340 rangeObject.endContainer = inBetweenMarkup[0].parentNode; 341 rangeObject.startOffset = offset; 342 rangeObject.endOffset = offset; 343 rangeObject.correctRange(); 344 345 } else if ( el.domobj && el.domobj.nodeType === 1 ) { // other node, normally a break 346 if ( jqEl.parent().find( 'br.aloha-ephemera' ).length === 0 ) { 347 // but before putting it, remove all: 348 jQuery( rangeObject.limitObject ).find( 'br.aloha-ephemera' ).remove(); 349 350 // now put it: 351 jQuery( rangeObject.commonAncestorContainer ) 352 .append( this.getFillUpElement( rangeObject.splitObject ) ); 353 } 354 355 jqEl.after( inBetweenMarkup ); 356 357 // now set the selection. Since we just added one break do the currect el 358 // the new position must be el's position + 1. el's position is the index 359 // of the el in the selection tree, which is i. then we must add 360 // another +1 because we want to be AFTER the object, not before. therefor +2 361 rangeObject.startContainer = rangeObject.commonAncestorContainer; 362 rangeObject.endContainer = rangeObject.startContainer; 363 rangeObject.startOffset = i + 2; 364 rangeObject.endOffset = i + 2; 365 rangeObject.update(); 366 } 367 } 368 } 369 rangeObject.select(); 370 }, 371 372 /** 373 * Check whether blocklevel elements need breaks at the end to visibly render a newline 374 * @return true if an ending break is necessary, false if not 375 */ 376 needEndingBreak: function() { 377 // currently, all browser except IE need ending breaks 378 return !jQuery.browser.msie; 379 }, 380 381 /** 382 * Get the currently selected text or false if nothing is selected (or the selection is collapsed) 383 * @return selected text 384 */ 385 getSelectedText: function() { 386 var rangeObject = Aloha.Selection.rangeObject; 387 388 if ( rangeObject.isCollapsed() ) { 389 return false; 390 } 391 392 return this.getFromSelectionTree( rangeObject.getSelectionTree(), true ); 393 }, 394 395 /** 396 * Recursive function to get the selected text from the selection tree starting at the given level 397 * @param selectionTree array of selectiontree elements 398 * @param astext true when the contents shall be fetched as text, false for getting as html markup 399 * @return selected text from that level (incluiding all sublevels) 400 */ 401 getFromSelectionTree: function( selectionTree, astext ) { 402 var text = '', i, treeLength, el, clone; 403 for ( i = 0, treeLength = selectionTree.length; i < treeLength; i++ ) { 404 el = selectionTree[i]; 405 if ( el.selection == 'partial' ) { 406 if ( el.domobj.nodeType === 3 ) { 407 // partial text node selected, get the selected part 408 text += el.domobj.data.substring( el.startOffset, el.endOffset ); 409 } else if ( el.domobj.nodeType === 1 && el.children ) { 410 // partial element node selected, do the recursion into the children 411 if ( astext ) { 412 text += this.getFromSelectionTree( el.children, astext ); 413 } else { 414 // when the html shall be fetched, we create a clone of the element and remove all the children 415 clone = jQuery( el.domobj ).clone( false ).empty(); 416 // then we do the recursion and add the selection into the clone 417 clone.html( this.getFromSelectionTree( el.children, astext ) ); 418 // finally we get the html of the clone 419 text += clone.outerHTML(); 420 } 421 } 422 } else if ( el.selection == 'full' ) { 423 if ( el.domobj.nodeType === 3 ) { 424 // full text node selected, get the text 425 text += jQuery( el.domobj ).text(); 426 } else if ( el.domobj.nodeType === 1 && el.children ) { 427 // full element node selected, get the html of the node and all children 428 text += astext ? jQuery( el.domobj ).text() : jQuery( el.domobj ).outerHTML(); 429 } 430 } 431 } 432 433 return text; 434 }, 435 436 /** 437 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed) 438 * @return {?String} 439 */ 440 getSelectedMarkup: function() { 441 var rangeObject = Aloha.Selection.rangeObject; 442 return rangeObject.isCollapsed() ? null 443 444 : this.getFromSelectionTree( rangeObject.getSelectionTree(), false ); 445 }, 446 447 /** 448 * Remove the currently selected markup 449 */ 450 removeSelectedMarkup: function() { 451 var rangeObject = Aloha.Selection.rangeObject, newRange; 452 453 if ( rangeObject.isCollapsed() ) { 454 return; 455 } 456 457 newRange = new Aloha.Selection.SelectionRange(); 458 // remove the selection 459 this.removeFromSelectionTree( rangeObject.getSelectionTree(), newRange ); 460 461 // do a cleanup now (starting with the commonancestorcontainer) 462 newRange.update(); 463 GENTICS.Utils.Dom.doCleanup( { 'merge' : true, 'removeempty' : true }, Aloha.Selection.rangeObject ); 464 Aloha.Selection.rangeObject = newRange; 465 466 // need to set the collapsed selection now 467 newRange.correctRange(); 468 newRange.update(); 469 newRange.select(); 470 Aloha.Selection.updateSelection(); 471 }, 472 473 /** 474 * Recursively remove the selected items, starting with the given level in the selectiontree 475 * @param selectionTree current level of the selectiontree 476 * @param newRange new collapsed range to be set after the removal 477 */ 478 removeFromSelectionTree: function( selectionTree, newRange ) { 479 // remember the first found partially selected element node (in case we need 480 // to merge it with the last found partially selected element node) 481 var firstPartialElement, 482 newdata, 483 i, 484 el, 485 adjacentTextNode, 486 treeLength; 487 488 // iterate through the selection tree 489 for ( i = 0, treeLength = selectionTree.length; i < treeLength; i++ ) { 490 el = selectionTree[ i ]; 491 492 // check the type of selection 493 if ( el.selection == 'partial' ) { 494 if ( el.domobj.nodeType === 3 ) { 495 // partial text node selected, so remove the selected portion 496 newdata = ''; 497 if ( el.startOffset > 0 ) { 498 newdata += el.domobj.data.substring( 0, el.startOffset ); 499 } 500 if ( el.endOffset < el.domobj.data.length ) { 501 newdata += el.domobj.data.substring( el.endOffset, el.domobj.data.length ); 502 } 503 el.domobj.data = newdata; 504 505 // eventually set the new range (if not done before) 506 if ( !newRange.startContainer ) { 507 newRange.startContainer = newRange.endContainer = el.domobj; 508 newRange.startOffset = newRange.endOffset = el.startOffset; 509 } 510 } else if ( el.domobj.nodeType === 1 && el.children ) { 511 // partial element node selected, so do the recursion into the children 512 this.removeFromSelectionTree( el.children, newRange ); 513 514 if ( firstPartialElement ) { 515 // when the first parially selected element is the same type 516 // of element, we need to merge them 517 if ( firstPartialElement.nodeName == el.domobj.nodeName ) { 518 // merge the nodes 519 jQuery( firstPartialElement ).append( jQuery( el.domobj ).contents() ); 520 521 // and remove the latter one 522 jQuery( el.domobj ).remove(); 523 } 524 525 } else { 526 // remember this element as first partially selected element 527 firstPartialElement = el.domobj; 528 } 529 } 530 531 } else if ( el.selection == 'full' ) { 532 // eventually set the new range (if not done before) 533 if ( !newRange.startContainer ) { 534 adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode( 535 el.domobj.parentNode, 536 GENTICS.Utils.Dom.getIndexInParent( el.domobj ) + 1, 537 false, 538 { 'blocklevel' : false } 539 ); 540 541 if ( adjacentTextNode ) { 542 newRange.startContainer = newRange.endContainer = adjacentTextNode; 543 newRange.startOffset = newRange.endOffset = 0; 544 } else { 545 newRange.startContainer = newRange.endContainer = el.domobj.parentNode; 546 newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent( el.domobj ) + 1; 547 } 548 } 549 550 // full node selected, so just remove it (will also remove all children) 551 jQuery( el.domobj ).remove(); 552 } 553 } 554 }, 555 556 /** 557 * split passed rangeObject without or with optional markup 558 * @param Aloha.Selection.SelectionRange of the current selection 559 * @param markup object (jQuery) to insert in between the split elements 560 * @return void 561 */ 562 splitRangeObject: function( rangeObject, markup ) { 563 // UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split 564 // object which is split up 565 var 566 splitObject = jQuery( rangeObject.splitObject ), 567 selectionTree, insertAfterObject, followUpContainer; 568 569 // update the commonAncestor with the splitObject (so that the selectionTree is correct) 570 rangeObject.update( rangeObject.splitObject ); // set the splitObject as new commonAncestorContainer and update the selectionTree 571 572 // calculate the selection tree. NOTE: it is necessary to do this before 573 // getting the followupcontainer, since getting the selection tree might 574 // possibly merge text nodes, which would lead to differences in the followupcontainer 575 selectionTree = rangeObject.getSelectionTree(); 576 577 // object to be inserted after the splitObject 578 followUpContainer = this.getSplitFollowUpContainer( rangeObject ); 579 580 // now split up the splitObject into itself AND the followUpContainer 581 this.splitRangeObjectHelper( selectionTree, rangeObject, followUpContainer ); // split the current object into itself and the followUpContainer 582 583 // check whether the followupcontainer is still marked for removal 584 if ( followUpContainer.hasClass( 'preparedForRemoval' ) ) { 585 // TODO shall we just remove the class or shall we not use the followupcontainer? 586 followUpContainer.removeClass( 'preparedForRemoval' ); 587 } 588 589 // now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in 590 // some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list) 591 insertAfterObject = this.getInsertAfterObject( rangeObject, followUpContainer ); 592 593 // now insert the followUpContainer 594 jQuery( followUpContainer ).insertAfter( insertAfterObject ); // attach the followUpContainer right after the insertAfterObject 595 596 // in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice) 597 if ( rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator( rangeObject.splitObject, followUpContainer ) ) { 598 jQuery( rangeObject.splitObject ).remove(); 599 } 600 601 rangeObject.startContainer = null; 602 // first check whether the followUpContainer starts with a <br/> 603 // if so, place the cursor right before the <br/> 604 var followContents = followUpContainer.contents(); 605 if ( followContents.length > 0 606 && followContents.get( 0 ).nodeType == 1 607 && followContents.get( 0 ).nodeName.toLowerCase() === 'br' ) { 608 rangeObject.startContainer = followUpContainer.get( 0 ); 609 } 610 611 if ( !rangeObject.startContainer ) { 612 // find a possible text node in the followUpContainer and set the selection to it 613 // if no textnode is available, set the selection to the followup container itself 614 rangeObject.startContainer = followUpContainer.textNodes( true, true ).first().get( 0 ); 615 } 616 if ( !rangeObject.startContainer ) { // if no text node was found, select the parent object of <br class="aloha-ephemera" /> 617 rangeObject.startContainer = followUpContainer.textNodes( false ).first().parent().get( 0 ); 618 } 619 if ( rangeObject.startContainer ) { 620 // the cursor is always at the beginning of the followUp 621 rangeObject.endContainer = rangeObject.startContainer; 622 rangeObject.startOffset = 0; 623 rangeObject.endOffset = 0; 624 } else { 625 rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get( 0 ); 626 rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent( followUpContainer.get( 0 ) ); 627 } 628 629 // finally update the range object again 630 rangeObject.update(); 631 632 // now set the selection 633 rangeObject.select(); 634 }, 635 636 /** 637 * method to get the object after which the followUpContainer can be inserted during splitup 638 * this is a helper method, not needed anywhere else 639 * @param rangeObject Aloha.Selection.SelectionRange of the current selection 640 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object 641 * @return object after which the followUpContainer can be inserted 642 */ 643 getInsertAfterObject: function( rangeObject, followUpContainer ) { 644 var passedSplitObject, i, el; 645 646 for ( i = 0; i < rangeObject.markupEffectiveAtStart.length; i++ ) { 647 el = rangeObject.markupEffectiveAtStart[ i ]; 648 649 // check if we have already passed the splitObject (some other markup might come before) 650 if ( el === rangeObject.splitObject ) { 651 passedSplitObject = true; 652 } 653 654 // if not passed splitObject, skip this markup 655 if ( !passedSplitObject ) { 656 continue; 657 } 658 659 // once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent 660 if ( Aloha.Selection.canTag1WrapTag2( jQuery( el ).parent()[0].nodeName, followUpContainer[0].nodeName ) ) { 661 return el; 662 } 663 } 664 665 666 return false; 667 }, 668 669 /** 670 * @fixme: Someone who knows what this function does, please refactor it. 671 * 1. splitObject arg is not used at all 672 * 2. Would be better to use ternary operation would be better than if else statement 673 * 674 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height 675 * @param splitObject split object (dom object) 676 * @return fillUpElement HTML Code 677 */ 678 getFillUpElement: function( splitObject ) { 679 if ( jQuery.browser.msie ) { 680 return false; 681 } else { 682 return jQuery( '<br class="aloha-cleanme"/>' ); 683 } 684 }, 685 686 /** 687 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags) 688 * @param domArray array of domObjects 689 * @return void 690 */ 691 removeElementContentWhitespaceObj: function( domArray ) { 692 var correction = 0, 693 removeLater = [], 694 i, 695 el, removeIndex; 696 697 for ( i = 0; i < domArray.length; ++i ) { 698 el = domArray[ i ]; 699 if ( el.isElementContentWhitespace ) { 700 removeLater[ removeLater.length ] = i; 701 } 702 } 703 704 for ( i = 0; i < removeLater.length; ++i ) { 705 removeIndex = removeLater[ i ]; 706 domArray.splice( removeIndex - correction, 1 ); 707 ++correction; 708 } 709 }, 710 711 /** 712 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other 713 * @param selectionTree tree to iterate over as contained in rangeObject. must be passed separately to allow recursion in the selection tree, but not in the rangeObject 714 715 * @param rangeObject Aloha.Selection.SelectionRange of the current selection 716 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object 717 * @param inBetweenMarkup jQuery object to be inserted between the two split parts. will be either a <br> (if no followUpContainer is passed) OR e.g. a table, which must be inserted between the splitobject AND the follow up 718 * @return void 719 */ 720 721 splitRangeObjectHelper: function( selectionTree, rangeObject, 722 followUpContainer, inBetweenMarkup ) { 723 if ( !followUpContainer ) { 724 Aloha.Log.warn( this, 'no followUpContainer, no inBetweenMarkup, nothing to do...' ); 725 } 726 727 var fillUpElement = this.getFillUpElement( rangeObject.splitObject ), 728 splitObject = jQuery( rangeObject.splitObject ), 729 startMoving = false, 730 el, 731 i, 732 completeText, 733 jqObj, 734 mirrorLevel, 735 parent, 736 treeLength; 737 738 if ( selectionTree.length > 0 ) { 739 mirrorLevel = followUpContainer.contents(); 740 741 // if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes 742 if ( mirrorLevel.length !== selectionTree.length ) { 743 this.removeElementContentWhitespaceObj( mirrorLevel ); 744 } 745 746 for ( i = 0, treeLength = selectionTree.length; i < treeLength; ++i ) { 747 el = selectionTree[ i ]; 748 749 // remove all objects in the mirrorLevel, which are BEFORE the cursor 750 // OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended) 751 if ( ( el.selection === 'none' && startMoving === false ) || 752 ( el.domobj && el.domobj.nodeType === 3 753 && el === selectionTree[ ( selectionTree.length - 1 ) ] 754 && el.startOffset === el.domobj.data.length ) ) { 755 // iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer 756 // however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead 757 // otherwise the followUpContainer is invalid and takes up no vertical space 758 759 if ( followUpContainer.textNodes().length > 1 760 || ( el.domobj.nodeType === 1 && el.children.length === 0 ) ) { 761 // note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition, 762 // where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be 763 // removed correctly otherwise 764 mirrorLevel.eq( i ).remove(); 765 766 } else if ( GENTICS.Utils.Dom.isSplitObject( followUpContainer[0] ) ) { 767 if ( fillUpElement ) { 768 followUpContainer.html( fillUpElement ); // for your zoological german knowhow: ephemera = Eintagsfliege 769 } else { 770 followUpContainer.empty(); 771 } 772 773 } else { 774 followUpContainer.empty(); 775 followUpContainer.addClass( 'preparedForRemoval' ); 776 } 777 778 continue; 779 780 } else { 781 // split objects, which are AT the cursor Position or directly above 782 if ( el.selection !== 'none' ) { // before cursor, leave this part inside the splitObject 783 // TODO better check for selection == 'partial' here? 784 if ( el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined ) { 785 completeText = el.domobj.data; 786 if ( el.startOffset > 0 ) {// first check, if there will be some text left in the splitObject 787 el.domobj.data = completeText.substr( 0, el.startOffset ); 788 } else if ( selectionTree.length > 1 ) { // if not, check if the splitObject contains more than one node, because then it can be removed. this happens, when ENTER is pressed inside of a textnode, but not at the borders 789 jQuery( el.domobj ).remove(); 790 } else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break 791 // if the parent is a blocklevel element, we insert the fillup element 792 parent = jQuery( el.domobj ).parent(); 793 if ( GENTICS.Utils.Dom.isSplitObject( parent[0] ) ) { 794 if ( fillUpElement ) { 795 parent.html( fillUpElement ); 796 } else { 797 parent.empty(); 798 } 799 800 } else { 801 // if the parent is no blocklevel element and would be empty now, we completely remove it 802 parent.remove(); 803 } 804 } 805 if ( completeText.length - el.startOffset > 0 ) { 806 // first check if there is text left to put in the followUpContainer's textnode. this happens, when ENTER is pressed inside of a textnode, but not at the borders 807 mirrorLevel[i].data = completeText.substr( el.startOffset, completeText.length ); 808 } else if ( mirrorLevel.length > 1 ) { 809 // if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed 810 mirrorLevel.eq( ( i ) ).remove(); 811 } else if ( GENTICS.Utils.Dom.isBlockLevelElement( followUpContainer[0] ) ) { 812 // if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break 813 if ( fillUpElement ) { 814 followUpContainer.html( fillUpElement ); 815 } else { 816 followUpContainer.empty(); 817 } 818 819 } else { 820 // if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal 821 followUpContainer.empty(); 822 followUpContainer.addClass( 'preparedForRemoval' ); 823 } 824 } 825 826 startMoving = true; 827 828 if ( el.children.length > 0 ) { 829 this.splitRangeObjectHelper( el.children, rangeObject, mirrorLevel.eq( i ), inBetweenMarkup ); 830 } 831 832 } else { 833 // remove all objects in the origin, which are AFTER the cursor 834 if ( el.selection === 'none' && startMoving === true ) { 835 // iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer 836 jqObj = jQuery( el.domobj ).remove(); 837 } 838 } 839 } 840 } 841 } else { 842 Aloha.Log.error( this, 'can not split splitObject due to an empty selection tree' ); 843 } 844 845 // and finally cleanup: remove all fillUps > 1 846 splitObject.find( 'br.aloha-ephemera:gt(0)' ).remove(); // remove all elements greater than (gt) 0, that also means: leave one 847 followUpContainer.find( 'br.aloha-ephemera:gt(0)' ).remove(); // remove all elements greater than (gt) 0, that also means: leave one 848 849 // remove objects prepared for removal 850 splitObject.find( '.preparedForRemoval' ).remove(); 851 followUpContainer.find( '.preparedForRemoval' ).remove(); 852 853 // if splitObject / followUp are empty, place a fillUp inside 854 if ( splitObject.contents().length === 0 855 && GENTICS.Utils.Dom.isSplitObject( splitObject[0] ) 856 && fillUpElement ) { 857 splitObject.html( fillUpElement ); 858 } 859 860 if ( followUpContainer.contents().length === 0 861 && GENTICS.Utils.Dom.isSplitObject( followUpContainer[0] ) 862 && fillUpElement ) { 863 followUpContainer.html( fillUpElement ); 864 } 865 }, 866 867 /** 868 * returns a jQuery object fitting the passed splitObject as follow up object 869 * examples, 870 * - when passed a p it will return an empty p (clone of the passed p) 871 * - when passed an h1, it will return either an h1 (clone of the passed one) or a new p (if the collapsed selection was at the end) 872 * @param rangeObject Aloha.RangeObject 873 * @return void 874 */ 875 getSplitFollowUpContainer: function( rangeObject ) { 876 var tagName = rangeObject.splitObject.nodeName.toLowerCase(), 877 returnObj, 878 inside, 879 lastObj; 880 881 switch ( tagName ) { 882 case 'h1': 883 case 'h2': 884 case 'h3': 885 case 'h4': 886 case 'h5': 887 case 'h6': 888 // get the last textnode in the splitobject, but don't consider aloha-cleanme elements 889 lastObj = jQuery( rangeObject.splitObject ).textNodes( ':not(.aloha-cleanme)' ).last()[0]; 890 // special case: when enter is hit at the end of a heading, the followUp should be a <p> 891 if ( lastObj && rangeObject.startContainer === lastObj 892 && rangeObject.startOffset === lastObj.length ) { 893 returnObj = jQuery( '<p></p>' ); 894 inside = jQuery( rangeObject.splitObject ).clone().contents(); 895 returnObj.append( inside ); 896 return returnObj; 897 } 898 break; 899 900 case 'li': 901 // TODO check whether the li is the last one 902 // special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list) 903 if ( rangeObject.startContainer.nodeName.toLowerCase() === 'br' 904 && jQuery( rangeObject.startContainer ).hasClass( 'aloha-ephemera' ) ) { 905 returnObj = jQuery( '<p></p>' ); 906 inside = jQuery( rangeObject.splitObject ).clone().contents(); 907 returnObj.append( inside ); 908 return returnObj; 909 } 910 // when the li is the last one and empty, we also just return a <p> 911 if ( !rangeObject.splitObject.nextSibling 912 && jQuery.trim( jQuery( rangeObject.splitObject ).text() ).length === 0 ) { 913 returnObj = jQuery( '<p></p>' ); 914 return returnObj; 915 } 916 } 917 918 return jQuery( rangeObject.splitObject ).clone(); 919 }, 920 921 /** 922 * Transform the given domobj into an object with the given new nodeName. 923 * Preserves the content and all attributes. If a range object is given, also the range will be preserved 924 * @param domobj dom object to transform 925 * @param nodeName new node name 926 * @param range range object 927 * @api 928 * @return new object as jQuery object 929 */ 930 transformDomObject: function( domobj, nodeName, range ) { 931 // first create the new element 932 var jqOldObj = jQuery( domobj ), 933 jqNewObj = jQuery( '<' + nodeName + '></' + nodeName + '>' ), 934 i; 935 936 // TODO what about events? css properties? 937 938 // copy attributes 939 if ( jqOldObj[0].attributes ) { 940 for ( i = 0; i < jqOldObj[0].attributes.length; ++i ) { 941 jqNewObj.attr( 942 jqOldObj[0].attributes[ i ].nodeName, 943 jqOldObj[0].attributes[ i ].nodeValue 944 ); 945 } 946 } 947 948 // copy inline CSS 949 if ( jqOldObj[0].style && jqOldObj[0].style.cssText ) { 950 jqNewObj[0].style.cssText = jqOldObj[0].style.cssText; 951 } 952 953 // now move the contents of the old dom object into the new dom object 954 jqOldObj.contents().appendTo( jqNewObj ); 955 956 // finally replace the old object with the new one 957 jqOldObj.replaceWith( jqNewObj ); 958 959 // preserve the range 960 if ( range ) { 961 if ( range.startContainer == domobj ) { 962 range.startContainer = jqNewObj.get( 0 ); 963 } 964 965 if ( range.endContainer == domobj ) { 966 range.endContainer = jqNewObj.get( 0 ); 967 } 968 } 969 970 return jqNewObj; 971 }, 972 973 /** 974 * String representation 975 * @return {String} 976 */ 977 toString: function() { 978 return 'Aloha.Markup'; 979 } 980 981 } ); 982 983 Aloha.Markup = new Aloha.Markup(); 984 985 return Aloha.Markup; 986 987 } ); 988