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