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