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