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 /** 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 return true; 160 } 161 162 var isText = isTextNode(node); 163 164 // If within a text node, then ignore superfluous white-spaces, 165 // since they are invisible to the user. 166 if (isText && node.data.replace(/\s+$/, '').length === offset) { 167 return true; 168 } 169 170 if (1 === length && !isText) { 171 return isBR(node.childNodes[0]); 172 } 173 174 return false; 175 } 176 177 function blink(node) { 178 jQuery(node).stop(true).css({ 179 opacity: 0 180 }).fadeIn(0).delay(100).fadeIn(function () { 181 jQuery(node).css({ 182 opacity: 1 183 }); 184 }); 185 186 return node; 187 } 188 189 function nodeContains(node1, node2) { 190 return isOldIE ? (shims.compareDocumentPosition(node1, node2) & 16) : 0 < jQuery(node1).find(node2).length; 191 } 192 193 function isInsidePlaceholder(range) { 194 var start = range.startContainer; 195 var end = range.endContainer; 196 var $placeholder = window.$_alohaPlaceholder; 197 198 return $placeholder.is(start) || $placeholder.is(end) || nodeContains($placeholder[0], start) || nodeContains($placeholder[0], end); 199 } 200 201 function cleanupPlaceholders(range) { 202 if (window.$_alohaPlaceholder && !isInsidePlaceholder(range)) { 203 if (0 === window.$_alohaPlaceholder.html().replace(/^( )*$/, '').length) { 204 window.$_alohaPlaceholder.remove(); 205 } 206 207 window.$_alohaPlaceholder = null; 208 } 209 } 210 211 /** 212 * @TODO(petro): We need to be more intelligent about whether we insert a 213 * block-level placeholder or a phrasing level element. 214 * @TODO(petro): test with <pre> 215 * @TODO: move to block-jump.js 216 */ 217 function jumpBlock(block, isGoingLeft, currentRange) { 218 var range = new GENTICS.Utils.RangeObject(); 219 var sibling = isGoingLeft ? prevVisibleNode(block) : nextVisibleNode(block); 220 221 if (!sibling || isBlock(sibling)) { 222 var $landing = jQuery('<div> </div>'); 223 224 if (isGoingLeft) { 225 jQuery(block).before($landing); 226 } else { 227 jQuery(block).after($landing); 228 } 229 230 range.startContainer = range.endContainer = $landing[0]; 231 range.startOffset = range.endOffset = 0; 232 233 // Clear out any old placeholder first ... 234 cleanupPlaceholders(range); 235 236 window.$_alohaPlaceholder = $landing; 237 } else { 238 239 // Don't jump the block yet if the cursor is moving to the 240 // beginning or end of a text node, or if it is about to leave 241 // an element node. Both these cases require a hack in some 242 // browsers. 243 var moveToBoundaryPositionInIE = ( // To the beginning or end of a text node? 244 (currentRange.startContainer.nodeType === 3 245 && currentRange.startContainer === currentRange.endContainer 246 && currentRange.startContainer.nodeValue !== "" 247 && (isGoingLeft ? currentRange.startOffset === 1 : currentRange.endOffset + 1 === currentRange.endContainer.length)) 248 // Leaving an element node? 249 || (currentRange.startContainer.nodeType === 1 250 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 return true; 525 }, 526 527 /** 528 * processing up and down cursor keys inside tables 529 * will only try to figure out if cursor is at first 530 * or last position in table and exit to the next 531 * editable node from there 532 * 533 * won't do anything if range is not collapsed 534 * 535 * @param {RangyRange} range A range object for the current selection. 536 * @param {number} keyCode Code of the currently pressed key. 537 * @return {boolean} true if something was done, false if browser should 538 * continue handling the event 539 */ 540 processCursorUpDown: function (range, keyCode) { 541 if (!range.collapsed) { 542 return false; 543 } 544 545 var node = range.startContainer, 546 tableWrapper, 547 cursorNode; 548 549 // UP 550 if (keyCode === 38 && 551 isFrontPosition(node, range.startOffset) && 552 isChildOf(node, ['TD', 'TH']) && 553 isFirstNode(node, 'TABLE')) { 554 555 // we want to position the cursor now in the first 556 // element before the table, so we need to find the 557 // table wrapper first ... 558 tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0); 559 if (!tableWrapper) { 560 return false; 561 } 562 563 // ... and then find it's previousSibling 564 // which we will descend down to its deepest 565 // nested child node, where we will put the 566 // cursor 567 // prefer previousElemntSibling because Firefox will land you in a 568 // whitespace text node between a preceding <p> and the table otherwise 569 if (tableWrapper.previousElementSibling) { 570 cursorNode = tableWrapper.previousElementSibling; 571 } else { 572 cursorNode = tableWrapper.previousSibling; 573 } 574 while (cursorNode.nodeType !== 3) { 575 cursorNode = cursorNode.lastChild; 576 if (cursorNode === null) { 577 // stop if there is no element to be entered before the table 578 return false; 579 } 580 } 581 582 Aloha.Selection.rangeObject.startContainer = cursorNode; 583 Aloha.Selection.rangeObject.endContainer = cursorNode; 584 Aloha.Selection.rangeObject.startOffset = cursorNode.length; 585 Aloha.Selection.rangeObject.endOffset = cursorNode.length; 586 Aloha.Selection.rangeObject.select(); 587 588 // Mozilla needs this fix or else the selection will not work 589 if (Aloha.activeEditable && jQuery.browser.mozilla) { 590 Aloha.activeEditable.obj.focus(); 591 } 592 593 return true; 594 595 // DOWN 596 } else if (keyCode === 40 && 597 isEndPosition(node, range.startOffset) && 598 isChildOf(node, ['TD', 'TH']) && 599 isLastNode(node, 'TABLE')) { 600 601 // we want to put the cursor in the first element right 602 // after the table so we need to find the table wrapper first 603 tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0); 604 if (!tableWrapper) { 605 return false; 606 } 607 608 // and now find its following sibling where we will put 609 // the cursor in the first position 610 // the next elementSibling is preffered over the nextSibling 611 // because Mozilla will sometimes have an empty text node 612 // right next to the table - but we most likely want to put 613 // the cursor into the next paragraph 614 if (tableWrapper.nextElementSibling) { 615 cursorNode = tableWrapper.nextElementSibling; 616 } else { 617 cursorNode = tableWrapper.nextSibling; 618 } 619 620 while (cursorNode.nodeType !== 3) { 621 cursorNode = cursorNode.firstChild; 622 if (cursorNode === null) { 623 return false; 624 } 625 } 626 627 Aloha.Selection.rangeObject.startContainer = cursorNode; 628 Aloha.Selection.rangeObject.endContainer = cursorNode; 629 Aloha.Selection.rangeObject.startOffset = 0; 630 Aloha.Selection.rangeObject.endOffset = 0; 631 Aloha.Selection.rangeObject.select(); 632 633 // Mozilla needs this fix or else the selection will not work 634 if (Aloha.activeEditable && jQuery.browser.mozilla) { 635 Aloha.activeEditable.obj.focus(); 636 } 637 638 return true; 639 640 } else { 641 return false; 642 643 } 644 }, 645 646 /** 647 * Processing of cursor keys. 648 * Detect blocks (elements with contenteditable=false) and will select them 649 * (normally the cursor would simply jump right past them). 650 * 651 * For each block that is selected, an 'aloha-block-selected' event will be 652 * triggered. 653 * 654 * TODO: the above is what should happen. Currently we just skip past blocks. 655 * 656 * @param {RangyRange} range A range object for the current selection. 657 * @param {number} keyCode Code of the currently pressed key. 658 * @return {boolean} False if a block was found, to prevent further events, 659 * true otherwise. 660 * @TODO move to block-jump.js 661 */ 662 processCursor: function (range, keyCode) { 663 if (!range.collapsed) { 664 return true; 665 } 666 667 BlockJump.removeZeroWidthTextNodeFix(); 668 669 var node = range.startContainer, 670 selection = Aloha.getSelection(); 671 672 if (!node) { 673 return true; 674 } 675 676 var sibling, offset; 677 678 // special handling for moving Cursor around zero-width whitespace in IE7 679 if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) <= 7 && isTextNode(node)) { 680 if (keyCode == 37) { 681 // moving left -> skip zwsp to the left 682 offset = range.startOffset; 683 while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') { 684 offset--; 685 } 686 if (offset != range.startOffset) { 687 range.setStart(range.startContainer, offset); 688 range.setEnd(range.startContainer, offset); 689 selection = Aloha.getSelection(); 690 selection.removeAllRanges(); 691 selection.addRange(range); 692 } 693 } else if (keyCode == 39) { 694 // moving right -> skip zwsp to the right 695 offset = range.startOffset; 696 while (offset < node.data.length && node.data.charAt(offset) === '\u200b') { 697 offset++; 698 } 699 if (offset != range.startOffset) { 700 range.setStart(range.startContainer, offset); 701 range.setEnd(range.startContainer, offset); 702 selection.removeAllRanges(); 703 selection.addRange(range); 704 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 ( jQuery.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 789 * @param html html markup to be inserted 790 */ 791 insertHTMLCode: function (html) { 792 var rangeObject = Aloha.Selection.rangeObject; 793 this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html)); 794 }, 795 796 /** 797 * insert an HTML Break <br /> into current selection 798 * @param Aloha.Selection.SelectionRange of the current selection 799 * @return void 800 */ 801 insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) { 802 var i, 803 treeLength, 804 el, 805 jqEl, 806 jqElBefore, 807 jqElAfter, 808 tmpObject, 809 offset, 810 checkObj; 811 812 inBetweenMarkup = inBetweenMarkup || jQuery('<br/>'); 813 814 for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) { 815 el = selectionTree[i]; 816 jqEl = el.domobj ? jQuery(el.domobj) : undefined; 817 818 if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject 819 if (el.selection == 'collapsed') { 820 // collapsed selection found (between nodes) 821 if (i > 0) { 822 // not at the start, so get the element to the left 823 jqElBefore = jQuery(selectionTree[i - 1].domobj); 824 825 // and insert the break after it 826 jqElBefore.after(inBetweenMarkup); 827 828 } else { 829 // at the start, so get the element to the right 830 jqElAfter = jQuery(selectionTree[1].domobj); 831 832 // and insert the break before it 833 jqElAfter.before(inBetweenMarkup); 834 } 835 836 // now set the range 837 rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode; 838 rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1; 839 rangeObject.correctRange(); 840 841 } else if (el.domobj && el.domobj.nodeType === 3) { // textNode 842 // when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between 843 if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) { 844 // TODO check whether this depends on the browser 845 jqEl.after('<br/>'); 846 } 847 848 if (this.needEndingBreak()) { 849 // when the textnode is the last inside a blocklevel element 850 // (like p, h1, ...) we need to add an additional br as very 851 // last object in the blocklevel element 852 checkObj = el.domobj; 853 854 while (checkObj) { 855 if (checkObj.nextSibling) { 856 checkObj = false; 857 } else { 858 // go to the parent 859 checkObj = checkObj.parentNode; 860 861 // found a blocklevel or list element, we are done 862 if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) { 863 break; 864 } 865 866 // reached the limit object, we are done 867 if (checkObj === rangeObject.limitObject) { 868 checkObj = false; 869 } 870 871 } 872 } 873 874 // when we found a blocklevel element, insert a break at the 875 // end. Mark the break so that it is cleaned when the 876 // content is fetched. 877 if (checkObj) { 878 jQuery(checkObj).append('<br class="aloha-cleanme" />'); 879 } 880 } 881 882 // insert the break 883 jqEl.between(inBetweenMarkup, el.startOffset); 884 885 // correct the range 886 // count the number of previous siblings 887 offset = 0; 888 tmpObject = inBetweenMarkup[0]; 889 while (tmpObject) { 890 tmpObject = tmpObject.previousSibling; 891 ++offset; 892 } 893 894 rangeObject.startContainer = inBetweenMarkup[0].parentNode; 895 rangeObject.endContainer = inBetweenMarkup[0].parentNode; 896 rangeObject.startOffset = offset; 897 rangeObject.endOffset = offset; 898 rangeObject.correctRange(); 899 900 } else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break 901 if (jqEl.parent().find('br.aloha-ephemera').length === 0) { 902 // but before putting it, remove all: 903 jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove(); 904 905 // now put it: 906 jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject)); 907 } 908 909 jqEl.after(inBetweenMarkup); 910 911 // now set the selection. Since we just added one break do the currect el 912 // the new position must be el's position + 1. el's position is the index 913 // of the el in the selection tree, which is i. then we must add 914 // another +1 because we want to be AFTER the object, not before. therefor +2 915 rangeObject.startContainer = rangeObject.commonAncestorContainer; 916 rangeObject.endContainer = rangeObject.startContainer; 917 rangeObject.startOffset = i + 2; 918 rangeObject.endOffset = i + 2; 919 rangeObject.update(); 920 } 921 } 922 } 923 rangeObject.select(); 924 }, 925 926 /** 927 * Check whether blocklevel elements need breaks at the end to visibly render a newline 928 * @return true if an ending break is necessary, false if not 929 */ 930 needEndingBreak: function () { 931 // currently, all browser except IE need ending breaks 932 return !jQuery.browser.msie; 933 }, 934 935 /** 936 * Get the currently selected text or false if nothing is selected (or the selection is collapsed) 937 * @return selected text 938 */ 939 getSelectedText: function () { 940 var rangeObject = Aloha.Selection.rangeObject; 941 942 if (rangeObject.isCollapsed()) { 943 return false; 944 } 945 946 return this.getFromSelectionTree(rangeObject.getSelectionTree(), true); 947 }, 948 949 /** 950 * Recursive function to get the selected text from the selection tree starting at the given level 951 * @param selectionTree array of selectiontree elements 952 * @param astext true when the contents shall be fetched as text, false for getting as html markup 953 * @return selected text from that level (incluiding all sublevels) 954 */ 955 getFromSelectionTree: function (selectionTree, astext) { 956 var text = '', i, treeLength, el, clone; 957 for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) { 958 el = selectionTree[i]; 959 if (el.selection == 'partial') { 960 if (el.domobj.nodeType === 3) { 961 // partial text node selected, get the selected part 962 text += el.domobj.data.substring(el.startOffset, el.endOffset); 963 } else if (el.domobj.nodeType === 1 && el.children) { 964 // partial element node selected, do the recursion into the children 965 if (astext) { 966 text += this.getFromSelectionTree(el.children, astext); 967 } else { 968 // when the html shall be fetched, we create a clone of 969 // the element and remove all the children 970 clone = jQuery(el.domobj.outerHTML).empty(); 971 // then we do the recursion and add the selection into the clone 972 clone.html(this.getFromSelectionTree(el.children, astext)); 973 // finally we get the html of the clone 974 text += clone.outerHTML(); 975 } 976 } 977 } else if (el.selection == 'full') { 978 if (el.domobj.nodeType === 3) { 979 // full text node selected, get the text 980 text += jQuery(el.domobj).text(); 981 } else if (el.domobj.nodeType === 1 && el.children) { 982 // full element node selected, get the html of the node and all children 983 text += astext ? jQuery(el.domobj).text() : jQuery(el.domobj).outerHTML(); 984 } 985 } 986 } 987 988 return text; 989 }, 990 991 /** 992 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed) 993 * @return {?String} 994 */ 995 getSelectedMarkup: function () { 996 var rangeObject = Aloha.Selection.rangeObject; 997 return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false); 998 }, 999 1000 /** 1001 * Remove the currently selected markup 1002 */ 1003 removeSelectedMarkup: function () { 1004 var rangeObject = Aloha.Selection.rangeObject, 1005 newRange; 1006 1007 if (rangeObject.isCollapsed()) { 1008 return; 1009 } 1010 1011 newRange = new Aloha.Selection.SelectionRange(); 1012 // remove the selection 1013 this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange); 1014 1015 // do a cleanup now (starting with the commonancestorcontainer) 1016 newRange.update(); 1017 GENTICS.Utils.Dom.doCleanup({ 1018 'merge': true, 1019 'removeempty': true 1020 }, Aloha.Selection.rangeObject); 1021 Aloha.Selection.rangeObject = newRange; 1022 1023 // need to set the collapsed selection now 1024 newRange.correctRange(); 1025 newRange.update(); 1026 newRange.select(); 1027 Aloha.Selection.updateSelection(); 1028 }, 1029 1030 /** 1031 * Recursively remove the selected items, starting with the given level in the selectiontree 1032 * @param selectionTree current level of the selectiontree 1033 * @param newRange new collapsed range to be set after the removal 1034 */ 1035 removeFromSelectionTree: function (selectionTree, newRange) { 1036 // remember the first found partially selected element node (in case we need 1037 // to merge it with the last found partially selected element node) 1038 var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength; 1039 1040 // iterate through the selection tree 1041 for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) { 1042 el = selectionTree[i]; 1043 1044 // check the type of selection 1045 if (el.selection == 'partial') { 1046 if (el.domobj.nodeType === 3) { 1047 // partial text node selected, so remove the selected portion 1048 newdata = ''; 1049 if (el.startOffset > 0) { 1050 newdata += el.domobj.data.substring(0, el.startOffset); 1051 } 1052 if (el.endOffset < el.domobj.data.length) { 1053 newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length); 1054 } 1055 el.domobj.data = newdata; 1056 1057 // eventually set the new range (if not done before) 1058 if (!newRange.startContainer) { 1059 newRange.startContainer = newRange.endContainer = el.domobj; 1060 newRange.startOffset = newRange.endOffset = el.startOffset; 1061 } 1062 } else if (el.domobj.nodeType === 1 && el.children) { 1063 // partial element node selected, so do the recursion into the children 1064 this.removeFromSelectionTree(el.children, newRange); 1065 1066 if (firstPartialElement) { 1067 // when the first parially selected element is the same type 1068 // of element, we need to merge them 1069 if (firstPartialElement.nodeName == el.domobj.nodeName) { 1070 // merge the nodes 1071 jQuery(firstPartialElement).append(jQuery(el.domobj).contents()); 1072 1073 // and remove the latter one 1074 jQuery(el.domobj).remove(); 1075 } 1076 1077 } else { 1078 // remember this element as first partially selected element 1079 firstPartialElement = el.domobj; 1080 } 1081 } 1082 1083 } else if (el.selection == 'full') { 1084 // eventually set the new range (if not done before) 1085 if (!newRange.startContainer) { 1086 adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode( 1087 el.domobj.parentNode, 1088 GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1, 1089 false, 1090 { 1091 'blocklevel': false 1092 } 1093 ); 1094 1095 if (adjacentTextNode) { 1096 newRange.startContainer = newRange.endContainer = adjacentTextNode; 1097 newRange.startOffset = newRange.endOffset = 0; 1098 } else { 1099 1100 newRange.startContainer = newRange.endContainer = el.domobj.parentNode; 1101 newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1; 1102 } 1103 } 1104 1105 // full node selected, so just remove it (will also remove all children) 1106 jQuery(el.domobj).remove(); 1107 } 1108 } 1109 }, 1110 1111 /** 1112 * split passed rangeObject without or with optional markup 1113 * @param Aloha.Selection.SelectionRange of the current selection 1114 * @param markup object (jQuery) to insert in between the split elements 1115 * @return void 1116 */ 1117 splitRangeObject: function (rangeObject, markup) { 1118 // UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split 1119 // object which is split up 1120 var splitObject = jQuery(rangeObject.splitObject), 1121 selectionTree, 1122 insertAfterObject, 1123 followUpContainer; 1124 1125 // update the commonAncestor with the splitObject (so that the selectionTree is correct) 1126 rangeObject.update(rangeObject.splitObject); // set the splitObject as new commonAncestorContainer and update the selectionTree 1127 1128 // calculate the selection tree. NOTE: it is necessary to do this before 1129 // getting the followupcontainer, since getting the selection tree might 1130 // possibly merge text nodes, which would lead to differences in the followupcontainer 1131 selectionTree = rangeObject.getSelectionTree(); 1132 1133 // object to be inserted after the splitObject 1134 followUpContainer = this.getSplitFollowUpContainer(rangeObject); 1135 1136 // now split up the splitObject into itself AND the followUpContainer 1137 this.splitRangeObjectHelper(selectionTree, rangeObject, followUpContainer); // split the current object into itself and the followUpContainer 1138 1139 // check whether the followupcontainer is still marked for removal 1140 if (followUpContainer.hasClass('preparedForRemoval')) { 1141 // TODO shall we just remove the class or shall we not use the followupcontainer? 1142 followUpContainer.removeClass('preparedForRemoval'); 1143 } 1144 1145 // now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in 1146 // some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list) 1147 insertAfterObject = this.getInsertAfterObject(rangeObject, followUpContainer); 1148 1149 // now insert the followUpContainer 1150 jQuery(followUpContainer).insertAfter(insertAfterObject); // attach the followUpContainer right after the insertAfterObject 1151 1152 // in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice) 1153 if (rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator(rangeObject.splitObject, followUpContainer)) { 1154 jQuery(rangeObject.splitObject).remove(); 1155 } 1156 1157 rangeObject.startContainer = null; 1158 // first check whether the followUpContainer starts with a <br/> 1159 // if so, place the cursor right before the <br/> 1160 var followContents = followUpContainer.contents(); 1161 1162 if (followContents.length > 0 && followContents.get(0).nodeType == 1 && followContents.get(0).nodeName.toLowerCase() === 'br') { 1163 rangeObject.startContainer = followUpContainer.get(0); 1164 } 1165 1166 if (!rangeObject.startContainer) { 1167 // find a possible text node in the followUpContainer and set the selection to it 1168 // if no textnode is available, set the selection to the followup container itself 1169 rangeObject.startContainer = followUpContainer.textNodes(true, true).first().get(0); 1170 } 1171 if (!rangeObject.startContainer) { // if no text node was found, select the parent object of <br class="aloha-ephemera" /> 1172 rangeObject.startContainer = followUpContainer.textNodes(false).first().parent().get(0); 1173 } 1174 if (rangeObject.startContainer) { 1175 // the cursor is always at the beginning of the followUp 1176 rangeObject.endContainer = rangeObject.startContainer; 1177 rangeObject.startOffset = 0; 1178 rangeObject.endOffset = 0; 1179 } else { 1180 rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get(0); 1181 rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(followUpContainer.get(0)); 1182 } 1183 1184 // finally update the range object again 1185 rangeObject.update(); 1186 1187 // now set the selection 1188 rangeObject.select(); 1189 }, 1190 1191 /** 1192 * method to get the object after which the followUpContainer can be inserted during splitup 1193 * this is a helper method, not needed anywhere else 1194 * @param rangeObject Aloha.Selection.SelectionRange of the current selection 1195 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object 1196 * @return object after which the followUpContainer can be inserted 1197 */ 1198 getInsertAfterObject: function (rangeObject, followUpContainer) { 1199 var passedSplitObject, i, el; 1200 1201 for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) { 1202 el = rangeObject.markupEffectiveAtStart[i]; 1203 1204 // check if we have already passed the splitObject (some other markup might come before) 1205 if (el === rangeObject.splitObject) { 1206 passedSplitObject = true; 1207 } 1208 1209 // if not passed splitObject, skip this markup 1210 if (!passedSplitObject) { 1211 continue; 1212 } 1213 1214 // once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent 1215 if (Aloha.Selection.canTag1WrapTag2(jQuery(el).parent()[0].nodeName, followUpContainer[0].nodeName)) { 1216 return el; 1217 } 1218 } 1219 1220 return false; 1221 }, 1222 1223 /** 1224 * @fixme: Someone who knows what this function does, please refactor it. 1225 * 1. splitObject arg is not used at all 1226 * 2. Would be better to use ternary operation would be better than if else statement 1227 * 1228 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height 1229 * @param splitObject split object (dom object) 1230 * @return fillUpElement HTML Code 1231 */ 1232 getFillUpElement: function (splitObject) { 1233 if (jQuery.browser.msie) { 1234 return false; 1235 } 1236 return jQuery('<br class="aloha-cleanme"/>'); 1237 }, 1238 1239 /** 1240 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags) 1241 * @param domArray array of domObjects 1242 * @return void 1243 */ 1244 removeElementContentWhitespaceObj: function (domArray) { 1245 var correction = 0, 1246 removeLater = [], 1247 i, 1248 el, 1249 removeIndex; 1250 1251 for (i = 0; i < domArray.length; ++i) { 1252 el = domArray[i]; 1253 if (el.isElementContentWhitespace) { 1254 removeLater[removeLater.length] = i; 1255 } 1256 } 1257 1258 for (i = 0; i < removeLater.length; ++i) { 1259 removeIndex = removeLater[i]; 1260 domArray.splice(removeIndex - correction, 1); 1261 ++correction; 1262 } 1263 }, 1264 1265 /** 1266 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other 1267 1268 * @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 1269 * @param rangeObject Aloha.Selection.SelectionRange of the current selection 1270 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object 1271 * @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 1272 * @return void 1273 */ 1274 splitRangeObjectHelper: function (selectionTree, rangeObject, followUpContainer, inBetweenMarkup) { 1275 if (!followUpContainer) { 1276 Aloha.Log.warn(this, 'no followUpContainer, no inBetweenMarkup, nothing to do...'); 1277 } 1278 1279 var fillUpElement = this.getFillUpElement(rangeObject.splitObject), 1280 splitObject = jQuery(rangeObject.splitObject), 1281 startMoving = false, 1282 el, 1283 i, 1284 completeText, 1285 jqObj, 1286 mirrorLevel, 1287 parent, 1288 treeLength; 1289 1290 if (selectionTree.length > 0) { 1291 mirrorLevel = followUpContainer.contents(); 1292 1293 // if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes 1294 if (mirrorLevel.length !== selectionTree.length) { 1295 this.removeElementContentWhitespaceObj(mirrorLevel); 1296 } 1297 1298 for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) { 1299 el = selectionTree[i]; 1300 1301 // remove all objects in the mirrorLevel, which are BEFORE the cursor 1302 // OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended) 1303 if ((el.selection === 'none' && startMoving === false) || (el.domobj && el.domobj.nodeType === 3 && el === selectionTree[(selectionTree.length - 1)] && el.startOffset === el.domobj.data.length)) { 1304 // iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer 1305 // however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead 1306 // otherwise the followUpContainer is invalid and takes up no vertical space 1307 1308 if (followUpContainer.textNodes().length > 1 || (el.domobj.nodeType === 1 && el.children.length === 0)) { 1309 // note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition, 1310 // where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be 1311 // removed correctly otherwise 1312 mirrorLevel.eq(i).remove(); 1313 1314 } else if (GENTICS.Utils.Dom.isSplitObject(followUpContainer[0])) { 1315 if (fillUpElement) { 1316 followUpContainer.html(fillUpElement); // for your zoological german knowhow: ephemera = Eintagsfliege 1317 } else { 1318 followUpContainer.empty(); 1319 } 1320 1321 } else { 1322 followUpContainer.empty(); 1323 followUpContainer.addClass('preparedForRemoval'); 1324 } 1325 1326 continue; 1327 1328 } else { 1329 // split objects, which are AT the cursor Position or directly above 1330 if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject 1331 // TODO better check for selection == 'partial' here? 1332 if (el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined) { 1333 completeText = el.domobj.data; 1334 if (el.startOffset > 0) { // first check, if there will be some text left in the splitObject 1335 el.domobj.data = completeText.substr(0, el.startOffset); 1336 } 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 1337 jQuery(el.domobj).remove(); 1338 } else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break 1339 // if the parent is a blocklevel element, we insert the fillup element 1340 parent = jQuery(el.domobj).parent(); 1341 if (GENTICS.Utils.Dom.isSplitObject(parent[0])) { 1342 if (fillUpElement) { 1343 parent.html(fillUpElement); 1344 } else { 1345 parent.empty(); 1346 } 1347 1348 } else { 1349 // if the parent is no blocklevel element and would be empty now, we completely remove it 1350 parent.remove(); 1351 } 1352 } 1353 if (completeText.length - el.startOffset > 0) { 1354 // 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 1355 mirrorLevel[i].data = completeText.substr(el.startOffset, completeText.length); 1356 } else if (mirrorLevel.length > 1) { 1357 // if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed 1358 mirrorLevel.eq((i)).remove(); 1359 } else if (GENTICS.Utils.Dom.isBlockLevelElement(followUpContainer[0])) { 1360 // if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break 1361 if (fillUpElement) { 1362 followUpContainer.html(fillUpElement); 1363 } else { 1364 followUpContainer.empty(); 1365 } 1366 1367 } else { 1368 // if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal 1369 followUpContainer.empty(); 1370 followUpContainer.addClass('preparedForRemoval'); 1371 } 1372 } 1373 1374 startMoving = true; 1375 1376 if (el.children.length > 0) { 1377 this.splitRangeObjectHelper(el.children, rangeObject, mirrorLevel.eq(i), inBetweenMarkup); 1378 } 1379 1380 } else { 1381 // remove all objects in the origin, which are AFTER the cursor 1382 if (el.selection === 'none' && startMoving === true) { 1383 // iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer 1384 jqObj = jQuery(el.domobj).remove(); 1385 } 1386 } 1387 } 1388 } 1389 } else { 1390 Aloha.Log.error(this, 'can not split splitObject due to an empty selection tree'); 1391 } 1392 1393 // and finally cleanup: remove all fillUps > 1 1394 splitObject.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one 1395 followUpContainer.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one 1396 1397 // remove objects prepared for removal 1398 splitObject.find('.preparedForRemoval').remove(); 1399 followUpContainer.find('.preparedForRemoval').remove(); 1400 1401 // if splitObject / followUp are empty, place a fillUp inside 1402 if (splitObject.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(splitObject[0]) && fillUpElement) { 1403 splitObject.html(fillUpElement); 1404 } 1405 1406 if (followUpContainer.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(followUpContainer[0]) && fillUpElement) { 1407 followUpContainer.html(fillUpElement); 1408 } 1409 }, 1410 1411 /** 1412 * returns a jQuery object fitting the passed splitObject as follow up object 1413 * examples, 1414 * - when passed a p it will return an empty p (clone of the passed p) 1415 * - 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) 1416 * @param rangeObject Aloha.RangeObject 1417 * @return void 1418 */ 1419 getSplitFollowUpContainer: function (rangeObject) { 1420 var tagName = rangeObject.splitObject.nodeName.toLowerCase(), 1421 returnObj, 1422 inside, 1423 lastObj; 1424 1425 switch (tagName) { 1426 case 'h1': 1427 case 'h2': 1428 case 'h3': 1429 case 'h4': 1430 case 'h5': 1431 case 'h6': 1432 // get the last textnode in the splitobject, but don't consider aloha-cleanme elements 1433 lastObj = jQuery(rangeObject.splitObject).textNodes(':not(.aloha-cleanme)').last()[0]; 1434 // special case: when enter is hit at the end of a heading, the followUp should be a <p> 1435 if (lastObj && rangeObject.startContainer === lastObj && rangeObject.startOffset === lastObj.length) { 1436 returnObj = jQuery('<p></p>'); 1437 inside = jQuery(rangeObject.splitObject.outerHTML).contents(); 1438 returnObj.append(inside); 1439 return returnObj; 1440 } 1441 break; 1442 1443 case 'li': 1444 // TODO check whether the li is the last one 1445 // special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list) 1446 if (rangeObject.startContainer.nodeName.toLowerCase() === 'br' && jQuery(rangeObject.startContainer).hasClass('aloha-ephemera')) { 1447 returnObj = jQuery('<p></p>'); 1448 inside = jQuery(rangeObject.splitObject.outerHTML).contents(); 1449 returnObj.append(inside); 1450 return returnObj; 1451 } 1452 // when the li is the last one and empty, we also just return a <p> 1453 if (!rangeObject.splitObject.nextSibling && jQuery.trim(jQuery(rangeObject.splitObject).text()).length === 0) { 1454 returnObj = jQuery('<p></p>'); 1455 return returnObj; 1456 } 1457 break; 1458 } 1459 1460 return jQuery(rangeObject.splitObject.outerHTML); 1461 }, 1462 1463 /** 1464 * Transform the given domobj into an object with the given new nodeName. 1465 * Preserves the content and all attributes. If a range object is given, also the range will be preserved 1466 * @param domobj dom object to transform 1467 * @param nodeName new node name 1468 * @param range range object 1469 * @api 1470 * @return new object as jQuery object 1471 */ 1472 transformDomObject: function (domobj, nodeName, range) { 1473 // first create the new element 1474 var jqOldObj = jQuery(domobj), 1475 jqNewObj = jQuery('<' + nodeName + '>'), 1476 i, 1477 attributes = jqOldObj[0].cloneNode(false).attributes; 1478 1479 // TODO what about events? 1480 // copy attributes 1481 if (attributes) { 1482 for (i = 0; i < attributes.length; ++i) { 1483 if (typeof attributes[i].specified === 'undefined' || attributes[i].specified) { 1484 jqNewObj.attr(attributes[i].nodeName, attributes[i].nodeValue); 1485 } 1486 } 1487 } 1488 1489 // copy inline CSS 1490 if (jqOldObj[0].style && jqOldObj[0].style.cssText) { 1491 jqNewObj[0].style.cssText = jqOldObj[0].style.cssText; 1492 } 1493 1494 // now move the contents of the old dom object into the new dom object 1495 jqOldObj.contents().appendTo(jqNewObj); 1496 1497 // finally replace the old object with the new one 1498 jqOldObj.replaceWith(jqNewObj); 1499 1500 // preserve the range 1501 if (range) { 1502 if (range.startContainer == domobj) { 1503 range.startContainer = jqNewObj.get(0); 1504 } 1505 1506 if (range.endContainer == domobj) { 1507 range.endContainer = jqNewObj.get(0); 1508 } 1509 } 1510 1511 return jqNewObj; 1512 }, 1513 1514 /** 1515 * String representation 1516 * @return {String} 1517 */ 1518 toString: function () { 1519 return 'Aloha.Markup'; 1520 } 1521 1522 }); 1523 1524 Aloha.Markup = new Aloha.Markup(); 1525 return Aloha.Markup; 1526 }); 1527