1 /* markup.js is part of Aloha Editor project http://aloha-editor.org
  2  *
  3  * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. 
  4  * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria.
  5  * Contributors http://aloha-editor.org/contribution.php 
  6  * 
  7  * Aloha Editor is free software; you can redistribute it and/or
  8  * modify it under the terms of the GNU General Public License
  9  * as published by the Free Software Foundation; either version 2
 10  * of the License, or any later version.
 11  *
 12  * Aloha Editor is distributed in the hope that it will be useful,
 13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU General Public License
 18  * along with this program; if not, write to the Free Software
 19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 20  * 
 21  * As an additional permission to the GNU GPL version 2, you may distribute
 22  * non-source (e.g., minimized or compacted) forms of the Aloha-Editor
 23  * source code without the copy of the GNU GPL normally required,
 24  * provided you include this license notice and a URL through which
 25  * recipients can access the Corresponding Source.
 26  */
 27 define([
 28 	'aloha/core',
 29 	'util/class',
 30 	'jquery',
 31 	'aloha/ecma5shims',
 32 	'aloha/console',
 33 	'aloha/block-jump'
 34 ], function (
 35 	Aloha,
 36 	Class,
 37 	jQuery,
 38 	shims,
 39 	console,
 40 	BlockJump
 41 ) {
 42 	"use strict";
 43 
 44 	var GENTICS = window.GENTICS;
 45 
 46 	var isOldIE = !!(jQuery.browser.msie && 9 > parseInt(jQuery.browser.version, 10));
 47 
 48 	function isBR(node) {
 49 		return 'BR' === node.nodeName;
 50 	}
 51 
 52 	function isBlock(node) {
 53 		return 'false' === jQuery(node).attr('contenteditable');
 54 	}
 55 
 56 	function isTextNode(node) {
 57 		return node && 3 === node.nodeType; // Node.TEXT_NODE
 58 	}
 59 
 60 	function nodeLength(node) {
 61 		return !node ? 0 : (isTextNode(node) ? node.length : node.childNodes.length);
 62 	}
 63 
 64 	/**
 65 	 * Determines whether the given text node is visible to the the user,
 66 	 * based on our understanding that browsers will not display
 67 	 * superfluous white spaces.
 68 	 *
 69 	 * @param {HTMLEmenent} node The text node to be checked.
 70 	 */
 71 	function isVisibleTextNode(node) {
 72 		return 0 < node.data.replace(/\s+/g, '').length;
 73 	}
 74 
 75 	function nextVisibleNode(node) {
 76 		if (!node) {
 77 			return null;
 78 		}
 79 
 80 		if (node.nextSibling) {
 81 			// Skip over nodes that the user cannot see ...
 82 			if (isTextNode(node.nextSibling) && !isVisibleTextNode(node.nextSibling)) {
 83 				return nextVisibleNode(node.nextSibling);
 84 			}
 85 
 86 			// Skip over propping <br>s ...
 87 			if (isBR(node.nextSibling) && node.nextSibling === node.parentNode.lastChild) {
 88 				return nextVisibleNode(node.nextSibling);
 89 			}
 90 
 91 			// Skip over empty editable elements ...
 92 			if ('' === node.nextSibling.innerHTML && !isBlock(node.nextSibling)) {
 93 				return nextVisibleNode(node.nextSibling);
 94 			}
 95 
 96 			return node.nextSibling;
 97 		}
 98 
 99 		if (node.parentNode) {
100 			return nextVisibleNode(node.parentNode);
101 		}
102 
103 		return null;
104 	}
105 
106 	function prevVisibleNode(node) {
107 		if (!node) {
108 			return null;
109 		}
110 
111 		if (node.previousSibling) {
112 			// Skip over nodes that the user cannot see...
113 			if (isTextNode(node.previousSibling) && !isVisibleTextNode(node.previousSibling)) {
114 				return prevVisibleNode(node.previousSibling);
115 			}
116 
117 			// Skip over empty editable elements ...
118 			if ('' === node.previousSibling.innerHTML && !isBlock(node.previousSibling)) {
119 				return prevVisibleNode(node.previouSibling);
120 			}
121 
122 			return node.previousSibling;
123 		}
124 
125 		if (node.parentNode) {
126 			return prevVisibleNode(node.parentNode);
127 		}
128 
129 		return null;
130 	}
131 
132 	function isFrontPosition(node, offset) {
133 		return (0 === offset) || (offset <= node.data.length - node.data.replace(/^\s+/, '').length);
134 	}
135 
136 	function isBlockInsideEditable($block) {
137 		return $block.parent().hasClass('aloha-editable');
138 	}
139 
140 	function isEndPosition(node, offset) {
141 		var length = nodeLength(node);
142 
143 		if (length === offset) {
144 			return true;
145 		}
146 
147 		var isText = isTextNode(node);
148 
149 		// If within a text node, then ignore superfluous white-spaces,
150 		// since they are invisible to the user.
151 		if (isText && node.data.replace(/\s+$/, '').length === offset) {
152 			return true;
153 		}
154 
155 		if (1 === length && !isText) {
156 			return isBR(node.childNodes[0]);
157 		}
158 
159 		return false;
160 	}
161 
162 	function blink(node) {
163 		jQuery(node).stop(true).css({
164 			opacity: 0
165 		}).fadeIn(0).delay(100).fadeIn(function () {
166 			jQuery(node).css({
167 				opacity: 1
168 			});
169 		});
170 
171 		return node;
172 	}
173 
174 	function nodeContains(node1, node2) {
175 		return isOldIE ? (shims.compareDocumentPosition(node1, node2) & 16) : 0 < jQuery(node1).find(node2).length;
176 	}
177 
178 	function isInsidePlaceholder(range) {
179 		var start = range.startContainer;
180 		var end = range.endContainer;
181 		var $placeholder = window.$_alohaPlaceholder;
182 
183 		return $placeholder.is(start) || $placeholder.is(end) || nodeContains($placeholder[0], start) || nodeContains($placeholder[0], end);
184 	}
185 
186 	function cleanupPlaceholders(range) {
187 		if (window.$_alohaPlaceholder && !isInsidePlaceholder(range)) {
188 			if (0 === window.$_alohaPlaceholder.html().replace(/^( )*$/, '').length) {
189 				window.$_alohaPlaceholder.remove();
190 			}
191 
192 			window.$_alohaPlaceholder = null;
193 		}
194 	}
195 
196 	/**
197 	 * @TODO(petro): We need to be more intelligent about whether we insert a
198 	 *               block-level placeholder or a phrasing level element.
199 	 * @TODO(petro): test with <pre>
200 	 * @TODO: move to block-jump.js
201 	 */
202 	function jumpBlock(block, isGoingLeft, currentRange) {
203 		var range = new GENTICS.Utils.RangeObject();
204 		var sibling = isGoingLeft ? prevVisibleNode(block) : nextVisibleNode(block);
205 
206 		if (!sibling || isBlock(sibling)) {
207 			var $landing = jQuery('<div> </div>');
208 
209 			if (isGoingLeft) {
210 				jQuery(block).before($landing);
211 			} else {
212 				jQuery(block).after($landing);
213 			}
214 
215 			range.startContainer = range.endContainer = $landing[0];
216 			range.startOffset = range.endOffset = 0;
217 
218 			// Clear out any old placeholder first ...
219 			cleanupPlaceholders(range);
220 
221 			window.$_alohaPlaceholder = $landing;
222 		} else {
223 
224 			// Don't jump the block yet if the cursor is moving to the
225 			// beginning or end of a text node, or if it is about to leave
226 227 			// an element node. Both these cases require a hack in some
228 			// browsers.
229 			var moveToBoundaryPositionInIE = ( // To the beginning or end of a text node?
230 				(currentRange.startContainer.nodeType === 3
231 				 && currentRange.startContainer === currentRange.endContainer
232 				 && currentRange.startContainer.nodeValue !== ""
233 				 && (isGoingLeft ? currentRange.startOffset === 1 : currentRange.endOffset + 1 === currentRange.endContainer.length))
234 				// Leaving an element node?
235 					|| (currentRange.startContainer.nodeType === 1
236 						&& (!currentRange.startOffset
237 							|| (currentRange.startContainer.childNodes[currentRange.startOffset] && currentRange.startContainer.childNodes[currentRange.startOffset].nodeType === 1)))
238 			);
239 
240 			if (moveToBoundaryPositionInIE) {
241 				// The cursor is moving to the beginning or end of a text
242 				// node, or is leaving an element node, which requires a
243 				// hack in some browsers.
244 				var zeroWidthNode = BlockJump.insertZeroWidthTextNodeFix(block, isGoingLeft);
245 				range.startContainer = range.endContainer = zeroWidthNode;
246 				range.startOffset = range.endOffset = isGoingLeft ? 1 : 0;
247 			} else {
248 				// The selection is already at the boundary position - jump
249 				// the block.
250 				range.startContainer = range.endContainer = sibling;
251 				range.startOffset = range.endOffset = isGoingLeft ? nodeLength(sibling) : 0;
252 				if (!isGoingLeft) {
253 					// Just as above, jumping to the first position right of
254 					// a block requires a hack in some browsers. Jumping
255 					// left seems to be fine.
256 					BlockJump.insertZeroWidthTextNodeFix(block, true);
257 				}
258 			}
259 			cleanupPlaceholders(range);
260 		}
261 
262 		range.select();
263 
264 		Aloha.trigger('aloha-block-selected', block);
265 		Aloha.Selection.preventSelectionChanged();
266 	}
267 
268 	/**
269 	 * Markup object
270 	 */
271 	Aloha.Markup = Class.extend({
272 
273 		/**
274 		 * Key handlers for special key codes
275 		 */
276 		keyHandlers: {},
277 
278 		/**
279 		 * Add a key handler for the given key code
280 		 * @param keyCode key code
281 		 * @param handler handler function
282 		 */
283 		addKeyHandler: function (keyCode, handler) {
284 			if (!this.keyHandlers[keyCode]) {
285 				this.keyHandlers[keyCode] = [];
286 			}
287 
288 			this.keyHandlers[keyCode].push(handler);
289 		},
290 
291 		/**
292 		 * Removes a key handler for the given key code
293 		 * @param keyCode key code
294 		 */
295 		removeKeyHandler: function (keyCode) {
296 			if (this.keyHandlers[keyCode]) {
297 				this.keyHandlers[keyCode] = null;
298 			}
299 		},
300 
301 		insertBreak: function () {
302 			var range = Aloha.Selection.rangeObject,
303 				nonWSIndex,
304 				nextTextNode,
305 				newBreak;
306 
307 			if (!range.isCollapsed()) {
308 				this.removeSelectedMarkup();
309 			}
310 
311 			newBreak = jQuery('<br/>');
312 			GENTICS.Utils.Dom.insertIntoDOM(newBreak, range, Aloha.activeEditable.obj);
313 
314 			nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
315 				newBreak.parent().get(0),
316 				GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1,
317 				false
318 			);
319 
320 			if (nextTextNode) {
321 				// trim leading whitespace
322 				nonWSIndex = nextTextNode.data.search(/\S/);
323 				if (nonWSIndex > 0) {
324 					nextTextNode.data = nextTextNode.data.substring(nonWSIndex);
325 				}
326 			}
327 
328 			range.startContainer = range.endContainer = newBreak.get(0).parentNode;
329 			range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1;
330 			range.correctRange();
331 			range.clearCaches();
332 			range.select();
333 		},
334 
335 		/**
336 		 * first method to handle key strokes
337 		 * @param event DOM event
338 		 * @param rangeObject as provided by Aloha.Selection.getRangeObject();
339 		 * @return "Aloha.Selection"
340 		 */
341 		preProcessKeyStrokes: function (event) {
342 			if (event.type !== 'keydown') {
343 				return false;
344 			}
345 
346 			var rangeObject,
347 			    handlers,
348 			    i;
349 
350 			if (this.keyHandlers[event.keyCode]) {
351 				handlers = this.keyHandlers[event.keyCode];
352 				for (i = 0; i < handlers.length; ++i) {
353 					if (!handlers[i](event)) {
354 						return false;
355 					}
356 				}
357 			}
358 
359 			// LEFT (37), RIGHT (39) keys for block detection
360 			if (event.keyCode === 37 || event.keyCode === 39) {
361 				if (Aloha.getSelection().getRangeCount()) {
362 					rangeObject = Aloha.getSelection().getRangeAt(0);
363 
364 					if (this.processCursor(rangeObject, event.keyCode)) {
365 						cleanupPlaceholders(Aloha.Selection.rangeObject);
366 						return true;
367 					}
368 				}
369 
370 				return false;
371 			}
372 
373 			// BACKSPACE
374 			if (event.keyCode === 8) {
375 				event.preventDefault(); // prevent history.back() even on exception
376 				Aloha.execCommand('delete', false);
377 				return false;
378 			}
379 
380 			// DELETE
381 			if (event.keyCode === 46) {
382 				Aloha.execCommand('forwarddelete', false);
383 				return false;
384 			}
385 
386 			// ENTER
387 			if (event.keyCode === 13) {
388 				if (event.shiftKey) {
389 					Aloha.execCommand('insertlinebreak', false);
390 					return false;
391 				}
392 				Aloha.execCommand('insertparagraph', false);
393 				return false;
394 			}
395 
396 			return true;
397 		},
398 
399 		/**
400 		 * Processing of cursor keys.
401 		 * Detect blocks (elements with contenteditable=false) and will select them
402 		 * (normally the cursor would simply jump right past them).
403 		 *
404 		 * For each block that is selected, an 'aloha-block-selected' event will be
405 		 * triggered.
406 		 *
407 		 * TODO: the above is what should happen. Currently we just skip past blocks.
408 		 *
409 		 * @param {RangyRange} range A range object for the current selection.
410 		 * @param {number} keyCode Code of the currently pressed key.
411 		 * @return {boolean} False if a block was found, to prevent further events,
412 		 *                   true otherwise.
413 		 * @TODO move to block-jump.js
414 		 */
415 		processCursor: function (range, keyCode) {
416 			if (!range.collapsed) {
417 				return true;
418 			}
419 
420 			BlockJump.removeZeroWidthTextNodeFix();
421 
422 			var node = range.startContainer,
423 				selection = Aloha.getSelection();
424 
425 			if (!node) {
426 				return true;
427 			}
428 
429 			var sibling, offset;
430 
431 			// special handling for moving Cursor around zero-width whitespace in IE7
432 			if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) <= 7 && isTextNode(node)) {
433 				if (keyCode == 37) {
434 					// moving left -> skip zwsp to the left
435 					offset = range.startOffset;
436 					while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') {
437 						offset--;
438 					}
439 					if (offset != range.startOffset) {
440 						range.setStart(range.startContainer, offset);
441 						range.setEnd(range.startContainer, offset);
442 						selection = Aloha.getSelection();
443 						selection.removeAllRanges();
444 						selection.addRange(range);
445 					}
446 				} else if (keyCode == 39) {
447 					// moving right -> skip zwsp to the right
448 					offset = range.startOffset;
449 					while (offset < node.data.length && node.data.charAt(offset) === '\u200b') {
450 						offset++;
451 					}
452 					if (offset != range.startOffset) {
453 						range.setStart(range.startContainer, offset);
454 						range.setEnd(range.startContainer, offset);
455 						selection.removeAllRanges();
456 						selection.addRange(range);
457 					}
458 				}
459 			}
460 
461 			// Versions of Internet Explorer that are older that 9, will
462 			// erroneously allow you to enter and edit inside elements which have
463 			// their contenteditable attribute set to false...
464 			if (isOldIE && !jQuery(node).contentEditable()) {
465 				var $parentBlock = jQuery(node).parents('[contenteditable=false]');
466 				var isInsideBlock = $parentBlock.length > 0;
467 
468 				if (isInsideBlock) {
469 					if (isBlockInsideEditable($parentBlock)) {
470 						sibling = $parentBlock[0];
471 					} else {
472 						return true;
473 					}
474 				}
475 			}
476 
477 			var isLeft;
478 			if (!sibling) {
479 				// True if keyCode denotes LEFT or UP arrow key, otherwise they
480 				// keyCode is for RIGHT or DOWN in which this value will be false.
481 				isLeft = (37 === keyCode || 38 === keyCode);
482 				offset = range.startOffset;
483 
484 				if (isTextNode(node)) {
485 					if (isLeft) {
486 						var isApproachingFrontPosition = (1 === offset);
487 						if (!isApproachingFrontPosition && !isFrontPosition(node, offset)) {
488 							return true;
489 						}
490 					} else if (!isEndPosition(node, offset)) {
491 						return true;
492 					}
493 
494 				} else {
495 					node = node.childNodes[offset === nodeLength(node) ? offset - 1 : offset];
496 				}
497 
498 				sibling = isLeft ? prevVisibleNode(node) : nextVisibleNode(node);
499 			}
500 
501 			if (isBlock(sibling)) {
502 				jumpBlock(sibling, isLeft, range);
503 				return false;
504 			}
505 
506 			return true;
507 		},
508 
509 		/**
510 		 * method handling shiftEnter
511 		 * @param Aloha.Selection.SelectionRange of the current selection
512 		 * @return void
513 		 */
514 		processShiftEnter: function (rangeObject) {
515 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
516 		},
517 
518 		/**
519 		 * method handling Enter
520 		 * @param Aloha.Selection.SelectionRange of the current selection
521 		 * @return void
522 		 */
523 		processEnter: function (rangeObject) {
524 			if (rangeObject.splitObject) {
525 				// 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
526 				// if ( jQuery.browser.msie
527 				//      && GENTICS.Utils.Dom
528 				//           .isListElement( rangeObject.splitObject ) ) {
529 				//  jQuery( rangeObject.splitObject ).append(
530 				//          jQuery( document.createTextNode( '' ) ) );
531 				//  }
532 				this.splitRangeObject(rangeObject);
533 			} else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2)
534 				this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
535 			}
536 		},
537 
538 		/**
539 		 * Insert the given html markup at the current selection
540 		 * @param html html markup to be inserted
541 		 */
542 		insertHTMLCode: function (html) {
543 			var rangeObject = Aloha.Selection.rangeObject;
544 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html));
545 		},
546 
547 		/**
548 		 * insert an HTML Break <br /> into current selection
549 		 * @param Aloha.Selection.SelectionRange of the current selection
550 		 * @return void
551 		 */
552 		insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) {
553 			var i,
554 			    treeLength,
555 			    el,
556 			    jqEl,
557 			    jqElBefore,
558 			    jqElAfter,
559 			    tmpObject,
560 			    offset,
561 			    checkObj;
562 
563 			inBetweenMarkup = inBetweenMarkup || jQuery('<br/>');
564 
565 			for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
566 				el = selectionTree[i];
567 				jqEl = el.domobj ? jQuery(el.domobj) : undefined;
568 
569 				if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
570 					if (el.selection == 'collapsed') {
571 						// collapsed selection found (between nodes)
572 						if (i > 0) {
573 							// not at the start, so get the element to the left
574 							jqElBefore = jQuery(selectionTree[i - 1].domobj);
575 
576 							// and insert the break after it
577 							jqElBefore.after(inBetweenMarkup);
578 
579 						} else {
580 							// at the start, so get the element to the right
581 							jqElAfter = jQuery(selectionTree[1].domobj);
582 
583 							// and insert the break before it
584 							jqElAfter.before(inBetweenMarkup);
585 						}
586 
587 						// now set the range
588 						rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
589 						rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1;
590 						rangeObject.correctRange();
591 
592 					} else if (el.domobj && el.domobj.nodeType === 3) { // textNode
593 						// when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
594 						if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) {
595 							// TODO check whether this depends on the browser
596 							jqEl.after('<br/>');
597 						}
598 
599 						if (this.needEndingBreak()) {
600 							// when the textnode is the last inside a blocklevel element
601 							// (like p, h1, ...) we need to add an additional br as very
602 							// last object in the blocklevel element
603 							checkObj = el.domobj;
604 
605 							while (checkObj) {
606 								if (checkObj.nextSibling) {
607 									checkObj = false;
608 								} else {
609 									// go to the parent
610 									checkObj = checkObj.parentNode;
611 
612 									// found a blocklevel or list element, we are done
613 									if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) {
614 										break;
615 									}
616 
617 									// reached the limit object, we are done
618 									if (checkObj === rangeObject.limitObject) {
619 										checkObj = false;
620 									}
621 								}
622 							}
623 
624 							// when we found a blocklevel element, insert a break at the
625 							// end. Mark the break so that it is cleaned when the
626 							// content is fetched.
627 							if (checkObj) {
628 								jQuery(checkObj).append('<br class="aloha-cleanme" />');
629 							}
630 						}
631 
632 						// insert the break
633 						jqEl.between(inBetweenMarkup, el.startOffset);
634 
635 						// correct the range
636 						// count the number of previous siblings
637 						offset = 0;
638 						tmpObject = inBetweenMarkup[0];
639 640 						while (tmpObject) {
641 							tmpObject = tmpObject.previousSibling;
642 							++offset;
643 						}
644 
645 						rangeObject.startContainer = inBetweenMarkup[0].parentNode;
646 						rangeObject.endContainer = inBetweenMarkup[0].parentNode;
647 						rangeObject.startOffset = offset;
648 						rangeObject.endOffset = offset;
649 650 						rangeObject.correctRange();
651 
652 					} else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break
653 						if (jqEl.parent().find('br.aloha-ephemera').length === 0) {
654 							// but before putting it, remove all:
655 							jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove();
656 
657 							//  now put it:
658 							jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject));
659 						}
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 		 * @return {?String}
747 		 */
748 		getSelectedMarkup: function () {
749 			var rangeObject = Aloha.Selection.rangeObject;
750 			return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false);
751 		},
752 
753 		/**
754 		 * Remove the currently selected markup
755 		 */
756 		removeSelectedMarkup: function () {
757 			var rangeObject = Aloha.Selection.rangeObject,
758 				newRange;
759 
760 			if (rangeObject.isCollapsed()) {
761 				return;
762 			}
763 
764 			newRange = new Aloha.Selection.SelectionRange();
765 			// remove the selection
766 			this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange);
767 
768 			// do a cleanup now (starting with the commonancestorcontainer)
769 			newRange.update();
770 			GENTICS.Utils.Dom.doCleanup({
771 				'merge': true,
772 				'removeempty': true
773 			}, Aloha.Selection.rangeObject);
774 			Aloha.Selection.rangeObject = newRange;
775 
776 			// need to set the collapsed selection now
777 			newRange.correctRange();
778 			newRange.update();
779 			newRange.select();
780 			Aloha.Selection.updateSelection();
781 		},
782 
783 		/**
784 		 * Recursively remove the selected items, starting with the given level in the selectiontree
785 		 * @param selectionTree current level of the selectiontree
786 		 * @param newRange new collapsed range to be set after the removal
787 		 */
788 		removeFromSelectionTree: function (selectionTree, newRange) {
789 			// remember the first found partially selected element node (in case we need
790 			// to merge it with the last found partially selected element node)
791 			var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength;
792 
793 			// iterate through the selection tree
794 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
795 				el = selectionTree[i];
796 
797 				// check the type of selection
798 				if (el.selection == 'partial') {
799 					if (el.domobj.nodeType === 3) {
800 						// partial text node selected, so remove the selected portion
801 						newdata = '';
802 						if (el.startOffset > 0) {
803 							newdata += el.domobj.data.substring(0, el.startOffset);
804 						}
805 						if (el.endOffset < el.domobj.data.length) {
806 							newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length);
807 						}
808 						el.domobj.data = newdata;
809 
810 						// eventually set the new range (if not done before)
811 						if (!newRange.startContainer) {
812 							newRange.startContainer = newRange.endContainer = el.domobj;
813 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