1 /* markup.js is part of Aloha Editor project http://aloha-editor.org 2 * 3 * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. 4 * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria. 5 * Contributors http://aloha-editor.org/contribution.php 6 * 7 * Aloha Editor is free software; you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License 9 * as published by the Free Software Foundation; either version 2 10 * of the License, or any later version. 11 * 12 * Aloha Editor is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with this program; if not, write to the Free Software 19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 * 21 * As an additional permission to the GNU GPL version 2, you may distribute 22 * non-source (e.g., minimized or compacted) forms of the Aloha-Editor 23 * source code without the copy of the GNU GPL normally required, 24 * provided you include this license notice and a URL through which 25 * recipients can access the Corresponding Source. 26 */ 27 define([ 28 'aloha/core', 29 'util/class', 30 'util/html', 31 'jquery', 32 'aloha/ecma5shims', 33 'aloha/console', 34 'aloha/block-jump' 35 ], function ( 36 Aloha, 37 Class, 38 Html, 39 jQuery, 40 shims, 41 console, 42 BlockJump 43 ) { 44 "use strict"; 45 46 var GENTICS = window.GENTICS; 47 48 var isOldIE = !!(jQuery.browser.msie && 9 > parseInt(jQuery.browser.version, 10)); 49 50 function isBR(node) { 51 return 'BR' === node.nodeName; 52 } 53 54 function isBlock(node) { 55 return 'false' === jQuery(node).attr('contenteditable'); 56 } 57 58 function isTextNode(node) { 59 return node && 3 === node.nodeType; // Node.TEXT_NODE 60 } 61 62 function nodeLength(node) { 63 return !node ? 0 : (isTextNode(node) ? node.length : node.childNodes.length); 64 } 65 66 /** 67 * Determines whether the given text node is visible to the the user, 68 * based on our understanding that browsers will not display 69 * superfluous white spaces. 70 * 71 * @param {HTMLEmenent} node The text node to be checked. 72 */ 73 function isVisibleTextNode(node) { 74 return 0 < node.data.replace(/\s+/g, '').length; 75 } 76 77 function nextVisibleNode(node) { 78 if (!node) { 79 return null; 80 } 81 82 if (node.nextSibling) { 83 // Skip over nodes that the user cannot see ... 84 if (isTextNode(node.nextSibling) && !isVisibleTextNode(node.nextSibling)) { 85 return nextVisibleNode(node.nextSibling); 86 } 87 88 // Skip over propping <br>s ... 89 if (isBR(node.nextSibling) && node.nextSibling === node.parentNode.lastChild) { 90 return nextVisibleNode(node.nextSibling); 91 } 92 93 // Skip over empty editable elements ... 94 if ('' === node.nextSibling.innerHTML && !isBlock(node.nextSibling)) { 95 return nextVisibleNode(node.nextSibling); 96 } 97 98 return node.nextSibling; 99 } 100 101 if (node.parentNode) { 102 return nextVisibleNode(node.parentNode); 103 } 104 105 return null; 106 } 107 108 function prevVisibleNode(node) { 109 if (!node) { 110 return null; 111 } 112 113 if (node.previousSibling) { 114 // Skip over nodes that the user cannot see... 115 if (isTextNode(node.previousSibling) && !isVisibleTextNode(node.previousSibling)) { 116 return prevVisibleNode(node.previousSibling); 117 } 118 119 // Skip over empty editable elements ... 120 if ('' === node.previousSibling.innerHTML && !isBlock(node.previousSibling)) { 121 return prevVisibleNode(node.previouSibling); 122 } 123 124 return node.previousSibling; 125 } 126 127 if (node.parentNode) { 128 return prevVisibleNode(node.parentNode); 129 } 130 131 return null; 132 } 133 134 function isFrontPosition(node, offset) { 135 return (0 === offset) || (offset <= node.data.length - node.data.replace(/^\s+/, '').length); 136 } 137 138 function isBlockInsideEditable($block) { 139 return $block.parent().hasClass('aloha-editable'); 140 } 141 142 function isEndPosition(node, offset) { 143 var length = nodeLength(node); 144 145 if (length === offset) { 146 return true; 147 } 148 149 var isText = isTextNode(node); 150 151 // If within a text node, then ignore superfluous white-spaces, 152 // since they are invisible to the user. 153 154 if (isText && node.data.replace(/\s+$/, '').length === offset) { 155 return true; 156 } 157 158 if (1 === length && !isText) { 159 return isBR(node.childNodes[0]); 160 } 161 162 return false; 163 } 164 165 function blink(node) { 166 jQuery(node).stop(true).css({ 167 opacity: 0 168 }).fadeIn(0).delay(100).fadeIn(function () { 169 jQuery(node).css({ 170 opacity: 1 171 }); 172 }); 173 174 return node; 175 } 176 177 function nodeContains(node1, node2) { 178 return isOldIE ? (shims.compareDocumentPosition(node1, node2) & 16) : 0 < jQuery(node1).find(node2).length; 179 } 180 181 function isInsidePlaceholder(range) { 182 var start = range.startContainer; 183 var end = range.endContainer; 184 var $placeholder = window.$_alohaPlaceholder; 185 186 return $placeholder.is(start) || $placeholder.is(end) || nodeContains($placeholder[0], start) || nodeContains($placeholder[0], end); 187 } 188 189 function cleanupPlaceholders(range) { 190 if (window.$_alohaPlaceholder && !isInsidePlaceholder(range)) { 191 if (0 === window.$_alohaPlaceholder.html().replace(/^( )*$/, '').length) { 192 window.$_alohaPlaceholder.remove(); 193 } 194 195 window.$_alohaPlaceholder = null; 196 } 197 } 198 199 /** 200 * @TODO(petro): We need to be more intelligent about whether we insert a 201 * block-level placeholder or a phrasing level element. 202 * @TODO(petro): test with <pre> 203 * @TODO: move to block-jump.js 204 */ 205 function jumpBlock(block, isGoingLeft, currentRange) { 206 var range = new GENTICS.Utils.RangeObject(); 207 var sibling = isGoingLeft ? prevVisibleNode(block) : nextVisibleNode(block); 208 209 if (!sibling || isBlock(sibling)) { 210 var $landing = jQuery('<div> </div>'); 211 212 if (isGoingLeft) { 213 jQuery(block).before($landing); 214 } else { 215 jQuery(block).after($landing); 216 } 217 218 range.startContainer = range.endContainer = $landing[0]; 219 range.startOffset = range.endOffset = 0; 220 221 // Clear out any old placeholder first ... 222 cleanupPlaceholders(range); 223 224 window.$_alohaPlaceholder = $landing; 225 } else { 226 227 // Don't jump the block yet if the cursor is moving to the 228 // beginning or end of a text node, or if it is about to leave 229 // an element node. Both these cases require a hack in some 230 // browsers. 231 var moveToBoundaryPositionInIE = ( // To the beginning or end of a text node? 232 (currentRange.startContainer.nodeType === 3 233 && currentRange.startContainer === currentRange.endContainer 234 && currentRange.startContainer.nodeValue !== "" 235 && (isGoingLeft ? currentRange.startOffset === 1 : currentRange.endOffset + 1 === currentRange.endContainer.length)) 236 // Leaving an element node? 237 || (currentRange.startContainer.nodeType === 1 238 && (!currentRange.startOffset 239 || (currentRange.startContainer.childNodes[currentRange.startOffset] && currentRange.startContainer.childNodes[currentRange.startOffset].nodeType === 1))) 240 ); 241 242 if (moveToBoundaryPositionInIE) { 243 // The cursor is moving to the beginning or end of a text 244 // node, or is leaving an element node, which requires a 245 // hack in some browsers. 246 var zeroWidthNode = BlockJump.insertZeroWidthTextNodeFix(block, isGoingLeft); 247 range.startContainer = range.endContainer = zeroWidthNode; 248 range.startOffset = range.endOffset = isGoingLeft ? 1 : 0; 249 } else { 250 // The selection is already at the boundary position - jump 251 // the block. 252 range.startContainer = range.endContainer = sibling; 253 range.startOffset = range.endOffset = isGoingLeft ? nodeLength(sibling) : 0; 254 if (!isGoingLeft) { 255 // Just as above, jumping to the first position right of 256 // a block requires a hack in some browsers. Jumping 257 // left seems to be fine. 258 BlockJump.insertZeroWidthTextNodeFix(block, true); 259 } 260 } 261 cleanupPlaceholders(range); 262 } 263 264 range.select(); 265 266 Aloha.trigger('aloha-block-selected', block); 267 Aloha.Selection.preventSelectionChanged(); 268 } 269 270 /** 271 * recursively search through parent nodes to find if 272 * node is child of a specific node. 273 * 274 * @param {DOMNode} starting node 275 * @param {Array[String]} Array of UPPERCASE (!) node names to search for, eg. ["TD"] or ["TD", "TH"]. 276 * @return true if node is child of a node of nodeName, false otherwise 277 */ 278 function isChildOf(node, nodeNames) { 279 var i; 280 if (node.parentNode) { 281 for (i = 0; i < nodeNames.length; i++) { 282 if (nodeNames[i] === node.parentNode.nodeName) { 283 return true; 284 } 285 } 286 return isChildOf(node.parentNode, nodeNames); 287 } else { 288 return false; 289 } 290 } 291 292 /** 293 * Will recursively check if the current node is the first node in 294 * it's hierarchy up it's ancestor tree until the stopNode is reached. 295 * Useful to find eg. if you're in the first td within a table. 296 * Will stop if stopNodeName is encountered or the root node is reached. 297 * 298 * @param {DOMnode} node to start from 299 * @param {String} UPPERCASE node name to stop search at 300 * @return true if node is the first node, false otherwise 301 */ 302 function isFirstNode(node, stopNodeName) { 303 if (!node.parentNode) { 304 return true; 305 } 306 307 // firstChild will also find textNodes while children[0] will only return non-text nodes 308 var isTextNode = ((node.nodeType === 3 && node.parentNode.firstChild === node) || (node.parentNode.children[0] === node)); 309 310 // unfortunately we need to take care of the aloha-table-selectrow and aloha-table-selectcolumn 311 var isTableSelectRow = node.nodeName === 'TR' && node.parentNode.children[0].className.indexOf('aloha-table-selectcolumn') !== -1; 312 var isTableSelectColumn = node.nodeName === 'TD' && node.parentNode.children[0].className.indexOf('aloha-table-selectrow') !== -1; 313 var isFirstNodeOfTable = ((isTableSelectColumn || isTableSelectRow) && node.parentNode.children[1] === node); 314 315 if (isTextNode || isFirstNodeOfTable) { 316 if (node.parentNode.nodeName === stopNodeName) { 317 return true; 318 } else { 319 return isFirstNode(node.parentNode, stopNodeName); 320 } 321 } else { 322 return false; 323 } 324 } 325 326 /** 327 * Will recurseively check if the current node is the last node in 328 * it's hierarchy up it's ancestor tree until the stopNode is reached. 329 * Useful to find eg. if you're in the last td within a table. 330 * Will stop if stopNodeName is encountered or the root node is reached. 331 * Will ignore whitespace text nodes and caption nodes 332 * 333 * @param {DOMnode} node to start from 334 * @param {String} UPPERCASE node name to stop search at 335 * @return true if node is the last node, false otherwise 336 */ 337 // implemented as an IIFE because the local helper getLast() should only be defined once 338 var isLastNode = (function () { 339 // get the last node that is not empty text or a table caption 340 function getLast(node) { 341 var last, i; 342 343 for (i = node.childNodes.length - 1; i > -1; i--) { 344 last = node.childNodes[i]; 345 if (last.nodeName !== 'CAPTION' && !(last.nodeType === 3 && /^[\t\n\r ]+$/.test(last.data))) { 346 return last; 347 } 348 } 349 350 return node.lastChild; 351 } 352 353 return function (node, stopNodeName) { 354 if (!node.parentNode) { 355 return true; 356 } 357 358 if (getLast(node.parentNode) === node) { 359 if (node.parentNode.nodeName === stopNodeName) { 360 return true; 361 } else { 362 return isLastNode(node.parentNode, stopNodeName); 363 } 364 } else { 365 return false; 366 } 367 }; 368 }()); 369 370 /** 371 * Markup object 372 */ 373 Aloha.Markup = Class.extend({ 374 375 /** 376 * Key handlers for special key codes 377 */ 378 keyHandlers: {}, 379 380 /** 381 * Add a key handler for the given key code 382 * @param keyCode key code 383 * @param handler handler function 384 */ 385 addKeyHandler: function (keyCode, handler) { 386 if (!this.keyHandlers[keyCode]) { 387 this.keyHandlers[keyCode] = []; 388 } 389 390 this.keyHandlers[keyCode].push(handler); 391 }, 392 393 /** 394 * Removes a key handler for the given key code 395 * @param keyCode key code 396 */ 397 removeKeyHandler: function (keyCode) { 398 if (this.keyHandlers[keyCode]) { 399 this.keyHandlers[keyCode] = null; 400 } 401 }, 402 403 insertBreak: function () { 404 var range = Aloha.Selection.rangeObject, 405 nonWSIndex, 406 nextTextNode, 407 newBreak; 408 409 if (!range.isCollapsed()) { 410 this.removeSelectedMarkup(); 411 } 412 413 newBreak = jQuery('<br/>'); 414 GENTICS.Utils.Dom.insertIntoDOM(newBreak, range, Aloha.activeEditable.obj); 415 416 nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode( 417 newBreak.parent().get(0), 418 GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1, 419 false 420 ); 421 422 if (nextTextNode) { 423 // trim leading whitespace 424 nonWSIndex = nextTextNode.data.search(/\S/); 425 if (nonWSIndex > 0) { 426 nextTextNode.data = nextTextNode.data.substring(nonWSIndex); 427 } 428 } 429 430 range.startContainer = range.endContainer = newBreak.get(0).parentNode; 431 range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1; 432 range.correctRange(); 433 range.clearCaches(); 434 range.select(); 435 }, 436 437 /** 438 * first method to handle key strokes 439 * @param event DOM event 440 * @param rangeObject as provided by Aloha.Selection.getRangeObject(); 441 * @return "Aloha.Selection" 442 */ 443 preProcessKeyStrokes: function (event) { 444 if (event.type !== 'keydown') { 445 return false; 446 } 447 448 var rangeObject, 449 handlers, 450 i; 451 452 if (this.keyHandlers[event.keyCode]) { 453 handlers = this.keyHandlers[event.keyCode]; 454 for (i = 0; i < handlers.length; ++i) { 455 if (!handlers[i](event)) { 456 return false; 457 } 458 } 459 } 460 461 // LEFT (37), RIGHT (39) keys for block detection 462 if (event.keyCode === 37 || event.keyCode === 39) { 463 if (Aloha.getSelection().getRangeCount()) { 464 rangeObject = Aloha.getSelection().getRangeAt(0); 465 466 if (this.processCursor(rangeObject, event.keyCode)) { 467 cleanupPlaceholders(Aloha.Selection.rangeObject); 468 return true; 469 } 470 } 471 472 return false; 473 } 474 475 // UP (38), DOWN (40) keys for table navigation 476 if (event.keyCode === 38 || event.keyCode === 40) { 477 if (Aloha.getSelection().getRangeCount()) { 478 rangeObject = Aloha.getSelection().getRangeAt(0); 479 if (this.processCursorUpDown(rangeObject, event.keyCode)) { 480 return false; 481 } 482 } 483 return true; 484 } 485 486 // BACKSPACE 487 if (event.keyCode === 8) { 488 event.preventDefault(); // prevent history.back() even on exception 489 Aloha.execCommand('delete', false); 490 return false; 491 } 492 493 // DELETE 494 if (event.keyCode === 46) { 495 Aloha.execCommand('forwarddelete', false); 496 return false; 497 } 498 499 // ENTER 500 if (event.keyCode === 13) { 501 if (!event.shiftKey && Html.allowNestedParagraph(Aloha.activeEditable)) { 502 Aloha.execCommand('insertparagraph', false); 503 return false; 504 // if the shift key is pressed, or if the active editable is not allowed 505 // to contain paragraphs, a linebreak is inserted instead 506 } else { 507 Aloha.execCommand('insertlinebreak', false); 508 return false; 509 } 510 } 511 return true; 512 }, 513 514 /** 515 * processing up and down cursor keys inside tables 516 * will only try to figure out if cursor is at first 517 * or last position in table and exit to the next 518 * editable node from there 519 * 520 * won't do anything if range is not collapsed 521 * 522 * @param {RangyRange} range A range object for the current selection. 523 * @param {number} keyCode Code of the currently pressed key. 524 * @return {boolean} true if something was done, false if browser should 525 * continue handling the event 526 */ 527 processCursorUpDown: function (range, keyCode) { 528 if (!range.collapsed) { 529 return false; 530 } 531 532 var node = range.startContainer, 533 tableWrapper, 534 cursorNode; 535 536 // UP 537 if (keyCode === 38 && 538 isFrontPosition(node, range.startOffset) && 539 isChildOf(node, ['TD', 'TH']) && 540 isFirstNode(node, 'TABLE')) { 541 542 // we want to position the cursor now in the first 543 // element before the table, so we need to find the 544 // table wrapper first ... 545 tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0); 546 if (!tableWrapper) { 547 return false; 548 } 549 550 // ... and then find it's previousSibling 551 // which we will descend down to its deepest 552 // nested child node, where we will put the 553 // cursor 554 // prefer previousElemntSibling because Firefox will land you in a 555 // whitespace text node between a preceding <p> and the table otherwise 556 if (tableWrapper.previousElementSibling) { 557 cursorNode = tableWrapper.previousElementSibling; 558 } else { 559 cursorNode = tableWrapper.previousSibling; 560 } 561 while (cursorNode.nodeType !== 3) { 562 cursorNode = cursorNode.lastChild; 563 if (cursorNode === null) { 564 // stop if there is no element to be entered before the table 565 return false; 566 } 567 } 568 569 Aloha.Selection.rangeObject.startContainer = cursorNode; 570 Aloha.Selection.rangeObject.endContainer = cursorNode; 571 Aloha.Selection.rangeObject.startOffset = cursorNode.length; 572 Aloha.Selection.rangeObject.endOffset = cursorNode.length; 573 Aloha.Selection.rangeObject.select(); 574 575 // Mozilla needs this fix or else the selection will not work 576 if (Aloha.activeEditable && jQuery.browser.mozilla) { 577 Aloha.activeEditable.obj.focus(); 578 } 579 580 return true; 581 582 // DOWN 583 } else if (keyCode === 40 && 584 isEndPosition(node, range.startOffset) && 585 isChildOf(node, ['TD', 'TH']) && 586 isLastNode(node, 'TABLE')) { 587 588 // we want to put the cursor in the first element right 589 // after the table so we need to find the table wrapper first 590 tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0); 591 if (!tableWrapper) { 592 return false; 593 } 594 595 // and now find its following sibling where we will put 596 // the cursor in the first position 597 // the next elementSibling is preffered over the nextSibling 598 // because Mozilla will sometimes have an empty text node 599 // right next to the table - but we most likely want to put 600 // the cursor into the next paragraph 601 if (tableWrapper.nextElementSibling) { 602 cursorNode = tableWrapper.nextElementSibling; 603 } else { 604 cursorNode = tableWrapper.nextSibling; 605 } 606 607 while (cursorNode.nodeType !== 3) { 608 cursorNode = cursorNode.firstChild; 609 if (cursorNode === null) { 610 return false; 611 } 612 } 613 614 Aloha.Selection.rangeObject.startContainer = cursorNode; 615 Aloha.Selection.rangeObject.endContainer = cursorNode; 616 Aloha.Selection.rangeObject.startOffset = 0; 617 Aloha.Selection.rangeObject.endOffset = 0; 618 Aloha.Selection.rangeObject.select(); 619 620 // Mozilla needs this fix or else the selection will not work 621 if (Aloha.activeEditable && jQuery.browser.mozilla) { 622 Aloha.activeEditable.obj.focus(); 623 } 624 625 return true; 626 627 } else { 628 return false; 629 } 630 }, 631 632 /** 633 * Processing of cursor keys. 634 * Detect blocks (elements with contenteditable=false) and will select them 635 * (normally the cursor would simply jump right past them). 636 * 637 * For each block that is selected, an 'aloha-block-selected' event will be 638 * triggered. 639 * 640 * TODO: the above is what should happen. Currently we just skip past blocks. 641 * 642 * @param {RangyRange} range A range object for the current selection. 643 * @param {number} keyCode Code of the currently pressed key. 644 * @return {boolean} False if a block was found, to prevent further events, 645 * true otherwise. 646 * @TODO move to block-jump.js 647 */ 648 processCursor: function (range, keyCode) { 649 if (!range.collapsed) { 650 return true; 651 } 652 653 BlockJump.removeZeroWidthTextNodeFix(); 654 655 var node = range.startContainer, 656 selection = Aloha.getSelection(); 657 658 if (!node) { 659 return true; 660 } 661 662 var sibling, offset; 663 664 // special handling for moving Cursor around zero-width whitespace in IE7 665 if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) <= 7 && isTextNode(node)) { 666 if (keyCode == 37) { 667 // moving left -> skip zwsp to the left 668 offset = range.startOffset; 669 while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') { 670 offset--; 671 } 672 if (offset != range.startOffset) { 673 range.setStart(range.startContainer, offset); 674 range.setEnd(range.startContainer, offset); 675 selection = Aloha.getSelection(); 676 selection.removeAllRanges(); 677 selection.addRange(range); 678 } 679 } else if (keyCode == 39) { 680 // moving right -> skip zwsp to the right 681 offset = range.startOffset; 682 while (offset < node.data.length && node.data.charAt(offset) === '\u200b') { 683 offset++; 684 } 685 if (offset != range.startOffset) { 686 range.setStart(range.startContainer, offset); 687 range.setEnd(range.startContainer, offset); 688 selection.removeAllRanges(); 689 selection.addRange(range); 690 } 691 } 692 } 693 694 // Versions of Internet Explorer that are older that 9, will 695 // erroneously allow you to enter and edit inside elements which have 696 // their contenteditable attribute set to false... 697 if (isOldIE && !jQuery(node).contentEditable()) { 698 var $parentBlock = jQuery(node).parents('[contenteditable=false]'); 699 var isInsideBlock = $parentBlock.length > 0; 700 701 if (isInsideBlock) { 702 if (isBlockInsideEditable($parentBlock)) { 703 sibling = $parentBlock[0]; 704 } else { 705 return true; 706 } 707 } 708 } 709 710 var isLeft; 711 if (!sibling) { 712 // True if keyCode denotes LEFT or UP arrow key, otherwise they 713 // keyCode is for RIGHT or DOWN in which this value will be false. 714 isLeft = (37 === keyCode || 38 === keyCode); 715 offset = range.startOffset; 716 717 if (isTextNode(node)) { 718 if (isLeft) { 719 var isApproachingFrontPosition = (1 === offset); 720 if (!isApproachingFrontPosition && !isFrontPosition(node, offset)) { 721 return true; 722 } 723 } else if (!isEndPosition(node, offset)) { 724 return true; 725 } 726 727 } else { 728 node = node.childNodes[offset === nodeLength(node) ? offset - 1 : offset]; 729 } 730 731 sibling = isLeft ? prevVisibleNode(node) : nextVisibleNode(node); 732 } 733 734 if (isBlock(sibling)) { 735 jumpBlock(sibling, isLeft, range); 736 return false; 737 } 738 739 return true; 740 }, 741 742 /** 743 * method handling shiftEnter 744 * @param Aloha.Selection.SelectionRange of the current selection 745 * @return void 746 */ 747 processShiftEnter: function (rangeObject) { 748 this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject); 749 }, 750 751 /** 752 753 * method handling Enter 754 * @param Aloha.Selection.SelectionRange of the current selection 755 * @return void 756 */ 757 processEnter: function (rangeObject) { 758 if (rangeObject.splitObject) { 759 // 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 760 // if ( jQuery.browser.msie 761 // && GENTICS.Utils.Dom 762 // .isListElement( rangeObject.splitObject ) ) { 763 // jQuery( rangeObject.splitObject ).append( 764 // jQuery( document.createTextNode( '' ) ) ); 765 // } 766 this.splitRangeObject(rangeObject); 767 } else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2) 768 this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject); 769 } 770 }, 771 772 /** 773 * Insert the given html markup at the current selection 774 * @param html html markup to be inserted 775 */ 776 insertHTMLCode: function (html) { 777 var rangeObject = Aloha.Selection.rangeObject; 778 this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html)); 779 }, 780 781 /** 782 * insert an HTML Break <br /> into current selection 783 * @param Aloha.Selection.SelectionRange of the current selection 784 * @return void 785 */ 786 insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) { 787 var i, 788 treeLength, 789 el, 790 jqEl, 791 jqElBefore, 792 jqElAfter, 793 tmpObject, 794 offset, 795 checkObj; 796 797 inBetweenMarkup = inBetweenMarkup || jQuery('<br/>'); 798 799 for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) { 800 el = selectionTree[i]; 801 jqEl = el.domobj ? jQuery(el.domobj) : undefined; 802 803 if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject 804 if (el.selection == 'collapsed') { 805 // collapsed selection found (between nodes) 806 if (i > 0) { 807 // not at the start, so get the element to the left 808 jqElBefore = jQuery(selectionTree[i - 1].domobj); 809 810 // and insert the break after it 811 jqElBefore.after(inBetweenMarkup); 812 813 } else { 814 // at the start, so get the element to the right 815 jqElAfter = jQuery(selectionTree[1].domobj); 816 817 // and insert the break before it 818 jqElAfter.before(inBetweenMarkup); 819 } 820 821 // now set the range 822 rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode; 823 rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1; 824 rangeObject.correctRange(); 825 826 } else if (el.domobj && el.domobj.nodeType === 3) { // textNode 827 // when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between 828 if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) { 829 // TODO check whether this depends on the browser 830 jqEl.after('<br/>'); 831 } 832 833 if (this.needEndingBreak()) { 834 // when the textnode is the last inside a blocklevel element 835 // (like p, h1, ...) we need to add an additional br as very 836 // last object in the blocklevel element 837 checkObj = el.domobj; 838 839 while (checkObj) { 840 if (checkObj.nextSibling) { 841 checkObj = false; 842 } else { 843 // go to the parent 844 checkObj = checkObj.parentNode; 845 846 // found a blocklevel or list element, we are done 847 if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) { 848 break; 849 } 850 851 // reached the limit object, we are done 852 if (checkObj === rangeObject.limitObject) { 853 checkObj = false; 854 } 855 } 856 } 857 858 // when we found a blocklevel element, insert a break at the 859 // end. Mark the break so that it is cleaned when the 860 // content is fetched. 861 if (checkObj) { 862 jQuery(checkObj).append('<br class="aloha-cleanme" />'); 863 } 864 } 865 866 // insert the break 867 jqEl.between(inBetweenMarkup, el.startOffset); 868 869 // correct the range 870 // count the number of previous siblings 871 offset = 0; 872 tmpObject = inBetweenMarkup[0]; 873 while (tmpObject) { 874 tmpObject = tmpObject.previousSibling; 875 ++offset; 876 } 877 878 rangeObject.startContainer = inBetweenMarkup[0].parentNode; 879 rangeObject.endContainer = inBetweenMarkup[0].parentNode; 880 rangeObject.startOffset = offset; 881 rangeObject.endOffset = offset; 882 rangeObject.correctRange(); 883 884 } else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break 885 if (jqEl.parent().find('br.aloha-ephemera').length === 0) { 886 // but before putting it, remove all: 887 jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove(); 888 889 // now put it: 890 jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject)); 891 } 892 893 jqEl.after(inBetweenMarkup); 894 895 // now set the selection. Since we just added one break do the currect el 896 // the new position must be el's position + 1. el's position is the index 897 // of the el in the selection tree, which is i. then we must add 898 // another +1 because we want to be AFTER the object, not before. therefor +2 899 rangeObject.startContainer = rangeObject.commonAncestorContainer; 900 rangeObject.endContainer = rangeObject.startContainer; 901 rangeObject.startOffset = i + 2; 902 rangeObject.endOffset = i + 2; 903 rangeObject.update(); 904 } 905 } 906 } 907 rangeObject.select(); 908 }, 909 910 /** 911 * Check whether blocklevel elements need breaks at the end to visibly render a newline 912 * @return true if an ending break is necessary, false if not 913 */ 914 needEndingBreak: function () { 915 // currently, all browser except IE need ending breaks 916 return !jQuery.browser.msie; 917 }, 918 919 /** 920 * Get the currently selected text or false if nothing is selected (or the selection is collapsed) 921 * @return selected text 922 */ 923 getSelectedText: function () { 924 var rangeObject = Aloha.Selection.rangeObject; 925 926 if (rangeObject.isCollapsed()) { 927 return false; 928 } 929 930 return this.getFromSelectionTree(rangeObject.getSelectionTree(), true); 931 }, 932 933 /** 934 * Recursive function to get the selected text from the selection tree starting at the given level 935 * @param selectionTree array of selectiontree elements 936 * @param astext true when the contents shall be fetched as text, false for getting as html markup 937 * @return selected text from that level (incluiding all sublevels) 938 */ 939 getFromSelectionTree: function (selectionTree, astext) { 940 var text = '', i, treeLength, el, clone; 941 for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) { 942 el = selectionTree[i]; 943 if (el.selection == 'partial') { 944 if (el.domobj.nodeType === 3) { 945 // partial text node selected, get the selected part 946 text += el.domobj.data.substring(el.startOffset, el.endOffset); 947 } else if (el.domobj.nodeType === 1 && el.children) { 948 // partial element node selected, do the recursion into the children 949 if (astext) { 950 951 text += this.getFromSelectionTree(el.children, astext); 952 } else { 953 // when the html shall be fetched, we create a clone of 954 // the element and remove all the children 955 clone = jQuery(el.domobj.outerHTML).empty(); 956 // then we do the recursion and add the selection into the clone 957 clone.html(this.getFromSelectionTree(el.children, astext)); 958 // finally we get the html of the clone 959 text += clone.outerHTML(); 960 } 961 } 962 } else if (el.selection == 'full') { 963 if (el.domobj.nodeType === 3) { 964 // full text node selected, get the text 965 text += jQuery(el.domobj).text(); 966 } else if (el.domobj.nodeType === 1 && el.children) { 967 // full element node selected, get the html of the node and all children 968 text += astext ? jQuery(el.domobj).text() : jQuery(el.domobj).outerHTML(); 969 } 970 } 971 } 972 973 return text; 974 }, 975 976 /** 977 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed) 978 * @return {?String} 979 */ 980 getSelectedMarkup: function () { 981 var rangeObject = Aloha.Selection.rangeObject; 982 return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false); 983 }, 984 985 /** 986 * Remove the currently selected markup 987 */ 988 removeSelectedMarkup: function () { 989 var rangeObject = Aloha.Selection.rangeObject, 990 newRange; 991 992 if (rangeObject.isCollapsed()) { 993 return; 994 } 995 996 newRange = new Aloha.Selection.SelectionRange(); 997 // remove the selection 998 this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange); 999 1000 // do a cleanup now (starting with the commonancestorcontainer) 1001 newRange.update(); 1002 GENTICS.Utils.Dom.doCleanup({ 1003 'merge': true, 1004 'removeempty': true 1005 }, Aloha.Selection.rangeObject); 1006 Aloha.Selection.rangeObject = newRange; 1007 1008 // need to set the collapsed selection now 1009 newRange.correctRange(); 1010 newRange.update(); 1011 newRange.select(); 1012 Aloha.Selection.updateSelection(); 1013 }, 1014 1015 /** 1016 * Recursively remove the selected items, starting with the given level in the selectiontree 1017 * @param selectionTree current level of the selectiontree 1018 * @param newRange new collapsed range to be set after the removal 1019 */ 1020 removeFromSelectionTree: function (selectionTree, newRange) { 1021 // remember the first found partially selected element node (in case we need 1022 // to merge it with the last found partially selected element node) 1023 var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength; 1024 1025 // iterate through the selection tree 1026 for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) { 1027 el = selectionTree[i]; 1028 1029 // check the type of selection 1030 if (el.selection == 'partial') { 1031 if (el.domobj.nodeType === 3) { 1032 // partial text node selected, so remove the selected portion 1033 newdata = ''; 1034 if (el.startOffset > 0) { 1035 newdata += el.domobj.data.substring(0, el.startOffset); 1036 } 1037 if (el.endOffset < el.domobj.data.length) { 1038 newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length); 1039 } 1040 el.domobj.data = newdata; 1041 1042 // eventually set the new range (if not done before) 1043 if (!newRange.startContainer) { 1044 newRange.startContainer = newRange.endContainer = el.domobj; 1045 newRange.startOffset = newRange.endOffset = el.startOffset; 1046 } 1047 } else if (el.domobj.nodeType === 1 && el.children) { 1048 // partial element node selected, so do the recursion into the children 1049 this.removeFromSelectionTree(el.children, newRange); 1050 1051 if (firstPartialElement) { 1052 // when the first parially selected element is the same type 1053 // of element, we need to merge them 1054 if (firstPartialElement.nodeName == el.domobj.nodeName) { 1055 // merge the nodes 1056 jQuery(firstPartialElement).append(jQuery(el.domobj).contents()); 1057 1058 // and remove the latter one 1059 jQuery(el.domobj).remove(); 1060 } 1061 1062 } else { 1063 // remember this element as first partially selected element 1064 firstPartialElement = el.domobj; 1065 } 1066 } 1067 1068 } else if (el.selection == 'full') { 1069 // eventually set the new range (if not done before) 1070 if (!newRange.startContainer) { 1071 adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode( 1072 el.domobj.parentNode, 1073 GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1, 1074 false, 1075 { 1076 'blocklevel': false 1077 } 1078 ); 1079 1080 if (adjacentTextNode) { 1081 newRange.startContainer = newRange.endContainer = adjacentTextNode; 1082 newRange.startOffset = newRange.endOffset = 0; 1083 } else { 1084 newRange.startContainer = newRange.endContainer = el.domobj.parentNode; 1085 newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1; 1086 } 1087 } 1088 1089 // full node selected, so just remove it (will also remove all children) 1090 jQuery(el.domobj).remove(); 1091 } 1092 } 1093 }, 1094 1095 /** 1096 * split passed rangeObject without or with optional markup 1097 * @param Aloha.Selection.SelectionRange of the current selection 1098 * @param markup object (jQuery) to insert in between the split elements 1099 1100 * @return void 1101 */ 1102 splitRangeObject: function (rangeObject, markup) { 1103 // UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split 1104 // object which is split up 1105 var splitObject = jQuery(rangeObject.splitObject), 1106 selectionTree, 1107 insertAfterObject, 1108 followUpContainer; 1109 1110 // update the commonAncestor with the splitObject (so that the selectionTree is correct) 1111 rangeObject.update(rangeObject.splitObject); // set the splitObject as new commonAncestorContainer and update the selectionTree 1112 1113 // calculate the selection tree. NOTE: it is necessary to do this before 1114 // getting the followupcontainer, since getting the selection tree might 1115 // possibly merge text nodes, which would lead to differences in the followupcontainer 1116 selectionTree = rangeObject.getSelectionTree(); 1117 1118 // object to be inserted after the splitObject 1119 followUpContainer = this.getSplitFollowUpContainer(rangeObject); 1120 1121 // now split up the splitObject into itself AND the followUpContainer 1122 this.splitRangeObjectHelper(selectionTree, rangeObject, followUpContainer); // split the current object into itself and the followUpContainer 1123 1124 // check whether the followupcontainer is still marked for removal 1125 if (followUpContainer.hasClass('preparedForRemoval')) { 1126 // TODO shall we just remove the class or shall we not use the followupcontainer? 1127 followUpContainer.removeClass('preparedForRemoval'); 1128 } 1129 1130 // now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in 1131 // some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list) 1132 insertAfterObject = this.getInsertAfterObject(rangeObject, followUpContainer); 1133 1134 // now insert the followUpContainer 1135 jQuery(followUpContainer).insertAfter(insertAfterObject); // attach the followUpContainer right after the insertAfterObject 1136 1137 // in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice) 1138 if (rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator(rangeObject.splitObject, followUpContainer)) { 1139 jQuery(rangeObject.splitObject).remove(); 1140 } 1141 1142 rangeObject.startContainer = null; 1143 // first check whether the followUpContainer starts with a <br/> 1144 // if so, place the cursor right before the <br/> 1145 var followContents = followUpContainer.contents(); 1146 if (followContents.length > 0 && followContents.get(0).nodeType == 1 && followContents.get(0).nodeName.toLowerCase() === 'br') { 1147 rangeObject.startContainer = followUpContainer.get(0); 1148 } 1149 1150 if (!rangeObject.startContainer) { 1151 // find a possible text node in the followUpContainer and set the selection to it 1152 // if no textnode is available, set the selection to the followup container itself 1153 rangeObject.startContainer = followUpContainer.textNodes(true, true).first().get(0); 1154 } 1155 if (!rangeObject.startContainer) { // if no text node was found, select the parent object of <br class="aloha-ephemera" /> 1156 rangeObject.startContainer = followUpContainer.textNodes(false).first().parent().get(0); 1157 } 1158 if (rangeObject.startContainer) { 1159 // the cursor is always at the beginning of the followUp 1160 rangeObject.endContainer = rangeObject.startContainer; 1161 rangeObject.startOffset = 0; 1162 rangeObject.endOffset = 0; 1163 } else { 1164 rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get(0); 1165 rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(followUpContainer.get(0)); 1166 } 1167 1168 // finally update the range object again 1169 rangeObject.update(); 1170 1171 // now set the selection 1172 rangeObject.select(); 1173 }, 1174 1175 /** 1176 * method to get the object after which the followUpContainer can be inserted during splitup 1177 * this is a helper method, not needed anywhere else 1178 * @param rangeObject Aloha.Selection.SelectionRange of the current selection 1179 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object 1180 * @return object after which the followUpContainer can be inserted 1181 */ 1182 getInsertAfterObject: function (rangeObject, followUpContainer) { 1183 var passedSplitObject, i, el; 1184 1185 for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) { 1186 el = rangeObject.markupEffectiveAtStart[i]; 1187 1188 // check if we have already passed the splitObject (some other markup might come before) 1189 if (el === rangeObject.splitObject) { 1190 passedSplitObject = true; 1191 } 1192 1193 // if not passed splitObject, skip this markup 1194 if (!passedSplitObject) { 1195 continue; 1196 } 1197 1198 // once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent 1199 if (Aloha.Selection.canTag1WrapTag2(jQuery(el).parent()[0].nodeName, followUpContainer[0].nodeName)) { 1200 return el; 1201 } 1202 } 1203 1204 return false; 1205 }, 1206 1207 /** 1208 * @fixme: Someone who knows what this function does, please refactor it. 1209 * 1. splitObject arg is not used at all 1210 * 2. Would be better to use ternary operation would be better than if else statement 1211 * 1212 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height 1213 * @param splitObject split object (dom object) 1214 * @return fillUpElement HTML Code 1215 */ 1216 getFillUpElement: function (splitObject) { 1217 if (jQuery.browser.msie) { 1218 return false; 1219 } 1220 return jQuery('<br class="aloha-cleanme"/>'); 1221 }, 1222 1223 /** 1224 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags) 1225 * @param domArray array of domObjects 1226 * @return void 1227 */ 1228 removeElementContentWhitespaceObj: function (domArray) { 1229 var correction = 0, 1230 removeLater = [], 1231 i, 1232 el, 1233 removeIndex; 1234 1235 for (i = 0; i < domArray.length; ++i) { 1236 el = domArray[i]; 1237 if (el.isElementContentWhitespace) { 1238 removeLater[removeLater.length] = i; 1239 } 1240 } 1241 1242 for (i = 0; i < removeLater.length; ++i) { 1243 removeIndex = removeLater[i]; 1244 domArray.splice(removeIndex - correction, 1); 1245 ++correction; 1246 } 1247 }, 1248 1249 /** 1250 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other 1251 * @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 1252 * @param rangeObject Aloha.Selection.SelectionRange of the current selection 1253 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object 1254 * @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 1255 * @return void 1256 */ 1257 splitRangeObjectHelper: function (selectionTree, rangeObject, followUpContainer, inBetweenMarkup) { 1258 if (!followUpContainer) { 1259 Aloha.Log.warn(this, 'no followUpContainer, no inBetweenMarkup, nothing to do...'); 1260 } 1261 1262 var fillUpElement = this.getFillUpElement(rangeObject.splitObject), 1263 splitObject = jQuery(rangeObject.splitObject), 1264 startMoving = false, 1265 el, 1266 i, 1267 completeText, 1268 jqObj, 1269 mirrorLevel, 1270 parent, 1271 treeLength; 1272 1273 if (selectionTree.length > 0) { 1274 1275 mirrorLevel = followUpContainer.contents(); 1276 1277 // if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes 1278 if (mirrorLevel.length !== selectionTree.length) { 1279 this.removeElementContentWhitespaceObj(mirrorLevel); 1280 } 1281 1282 for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) { 1283 el = selectionTree[i]; 1284 1285 // remove all objects in the mirrorLevel, which are BEFORE the cursor 1286 // OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended) 1287 if ((el.selection === 'none' && startMoving === false) || (el.domobj && el.domobj.nodeType === 3 && el === selectionTree[(selectionTree.length - 1)] && el.startOffset === el.domobj.data.length)) { 1288 // iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer 1289 // however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead 1290 // otherwise the followUpContainer is invalid and takes up no vertical space 1291 1292 if (followUpContainer.textNodes().length > 1 || (el.domobj.nodeType === 1 && el.children.length === 0)) { 1293 // note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition, 1294 // where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be 1295 // removed correctly otherwise 1296 mirrorLevel.eq(i).remove(); 1297 1298 } else if (GENTICS.Utils.Dom.isSplitObject(followUpContainer[0])) { 1299 if (fillUpElement) { 1300 followUpContainer.html(fillUpElement); // for your zoological german knowhow: ephemera = Eintagsfliege 1301 } else { 1302 followUpContainer.empty(); 1303 } 1304 1305 1306 } else { 1307 followUpContainer.empty(); 1308 followUpContainer.addClass('preparedForRemoval'); 1309 } 1310 1311 continue; 1312 1313 } else { 1314 // split objects, which are AT the cursor Position or directly above 1315 if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject 1316 // TODO better check for selection == 'partial' here? 1317 if (el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined) { 1318 completeText = el.domobj.data; 1319 if (el.startOffset > 0) { // first check, if there will be some text left in the splitObject 1320 el.domobj.data = completeText.substr(0, el.startOffset); 1321 } 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 1322 jQuery(el.domobj).remove(); 1323 } else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break 1324 // if the parent is a blocklevel element, we insert the fillup element 1325 parent = jQuery(el.domobj).parent(); 1326 if (GENTICS.Utils.Dom.isSplitObject(parent[0])) { 1327 if (fillUpElement) { 1328 parent.html(fillUpElement); 1329 } else { 1330 parent.empty(); 1331 } 1332 1333 } else { 1334 // if the parent is no blocklevel element and would be empty now, we completely remove it 1335 parent.remove(); 1336 } 1337 } 1338 if (completeText.length - el.startOffset > 0) { 1339 // 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 1340 mirrorLevel[i].data = completeText.substr(el.startOffset, completeText.length); 1341 } else if (mirrorLevel.length > 1) { 1342 // if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed 1343 mirrorLevel.eq((i)).remove(); 1344 } else if (GENTICS.Utils.Dom.isBlockLevelElement(followUpContainer[0])) { 1345 // if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break 1346 if (fillUpElement) { 1347 followUpContainer.html(fillUpElement); 1348 } else { 1349 followUpContainer.empty(); 1350 } 1351 1352 } else { 1353 // if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal 1354 followUpContainer.empty(); 1355 followUpContainer.addClass('preparedForRemoval'); 1356 } 1357 } 1358 1359 startMoving = true; 1360 1361 if (el.children.length > 0) { 1362 this.splitRangeObjectHelper(el.children, rangeObject, mirrorLevel.eq(i), inBetweenMarkup); 1363 } 1364 1365 } else { 1366 // remove all objects in the origin, which are AFTER the cursor 1367 if (el.selection === 'none' && startMoving === true) { 1368 // iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer 1369 jqObj = jQuery(el.domobj).remove(); 1370 } 1371 } 1372 } 1373 } 1374 } else { 1375 Aloha.Log.error(this, 'can not split splitObject due to an empty selection tree'); 1376 } 1377 1378 // and finally cleanup: remove all fillUps > 1 1379 splitObject.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one 1380 followUpContainer.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one 1381 1382 // remove objects prepared for removal 1383 splitObject.find('.preparedForRemoval').remove(); 1384 followUpContainer.find('.preparedForRemoval').remove(); 1385 1386 // if splitObject / followUp are empty, place a fillUp inside 1387 if (splitObject.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(splitObject[0]) && fillUpElement) { 1388 splitObject.html(fillUpElement); 1389 } 1390 1391 if (followUpContainer.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(followUpContainer[0]) && fillUpElement) { 1392 followUpContainer.html(fillUpElement); 1393 } 1394 }, 1395 1396 /** 1397 * returns a jQuery object fitting the passed splitObject as follow up object 1398 * examples, 1399 * - when passed a p it will return an empty p (clone of the passed p) 1400 * - 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) 1401 * @param rangeObject Aloha.RangeObject 1402 * @return void 1403 */ 1404 getSplitFollowUpContainer: function (rangeObject) { 1405 var tagName = rangeObject.splitObject.nodeName.toLowerCase(), 1406 returnObj, 1407 inside, 1408 lastObj; 1409 1410 switch (tagName) { 1411 case 'h1': 1412 case 'h2': 1413 case 'h3': 1414 case 'h4': 1415 case 'h5': 1416 case 'h6': 1417 // get the last textnode in the splitobject, but don't consider aloha-cleanme elements 1418 lastObj = jQuery(rangeObject.splitObject).textNodes(':not(.aloha-cleanme)').last()[0]; 1419 // special case: when enter is hit at the end of a heading, the followUp should be a <p> 1420 if (lastObj && rangeObject.startContainer === lastObj && rangeObject.startOffset === lastObj.length) { 1421 returnObj = jQuery('<p></p>'); 1422 inside = jQuery(rangeObject.splitObject.outerHTML).contents(); 1423 returnObj.append(inside); 1424 return returnObj; 1425 } 1426 break; 1427 1428 case 'li': 1429 // TODO check whether the li is the last one 1430 // special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list) 1431 if (rangeObject.startContainer.nodeName.toLowerCase() === 'br' && jQuery(rangeObject.startContainer).hasClass('aloha-ephemera')) { 1432 returnObj = jQuery('<p></p>'); 1433 inside = jQuery(rangeObject.splitObject.outerHTML).contents(); 1434 returnObj.append(inside); 1435 return returnObj; 1436 1437 } 1438 // when the li is the last one and empty, we also just return a <p> 1439 if (!rangeObject.splitObject.nextSibling && jQuery.trim(jQuery(rangeObject.splitObject).text()).length === 0) { 1440 returnObj = jQuery('<p></p>'); 1441 return returnObj; 1442 } 1443 break; 1444 } 1445 1446 return jQuery(rangeObject.splitObject.outerHTML); 1447 }, 1448 1449 /** 1450 * Transform the given domobj into an object with the given new nodeName. 1451 * Preserves the content and all attributes. If a range object is given, also the range will be preserved 1452 * @param domobj dom object to transform 1453 * @param nodeName new node name 1454 * @param range range object 1455 * @api 1456 * @return new object as jQuery object 1457 */ 1458 transformDomObject: function (domobj, nodeName, range) { 1459 // first create the new element 1460 var jqOldObj = jQuery(domobj), 1461 jqNewObj = jQuery('<' + nodeName + '>'), 1462 i, 1463 attributes = jqOldObj[0].cloneNode(false).attributes; 1464 1465 // TODO what about events? 1466 // copy attributes 1467 if (attributes) { 1468 for (i = 0; i < attributes.length; ++i) { 1469 if (typeof attributes[i].specified === 'undefined' || attributes[i].specified) { 1470 jqNewObj.attr(attributes[i].nodeName, attributes[i].nodeValue); 1471 } 1472 } 1473 } 1474 1475 // copy inline CSS 1476 if (jqOldObj[0].style && jqOldObj[0].style.cssText) { 1477 jqNewObj[0].style.cssText = jqOldObj[0].style.cssText; 1478 } 1479 1480 // now move the contents of the old dom object into the new dom object 1481 jqOldObj.contents().appendTo(jqNewObj); 1482 1483 // finally replace the old object with the new one 1484 jqOldObj.replaceWith(jqNewObj); 1485 1486 // preserve the range 1487 if (range) { 1488 if (range.startContainer == domobj) { 1489 range.startContainer = jqNewObj.get(0); 1490 } 1491 1492 if (range.endContainer == domobj) { 1493 range.endContainer = jqNewObj.get(0); 1494 } 1495 } 1496 1497 return jqNewObj; 1498 }, 1499 1500 /** 1501 * String representation 1502 * @return {String} 1503 */ 1504 toString: function () { 1505 return 'Aloha.Markup'; 1506 } 1507 1508 }); 1509 1510 Aloha.Markup = new Aloha.Markup(); 1511 return Aloha.Markup; 1512 }); 1513