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