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