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('insertparagraph', false);
393 					return false;
394 				// if the shift key is pressed, or if the active editable is not allowed
395 				// to contain paragraphs, a linebreak is inserted instead
396 				} else {
397 					Aloha.execCommand('insertlinebreak', false);
398 					return false;
399 				}
400 			}
401 			return true;
402 		},
403 
404 		/**
405 		 * Processing of cursor keys.
406 		 * Detect blocks (elements with contenteditable=false) and will select them
407 		 * (normally the cursor would simply jump right past them).
408 		 *
409 		 * For each block that is selected, an 'aloha-block-selected' event will be
410 		 * triggered.
411 		 *
412 		 * TODO: the above is what should happen. Currently we just skip past blocks.
413 		 *
414 		 * @param {RangyRange} range A range object for the current selection.
415 		 * @param {number} keyCode Code of the currently pressed key.
416 		 * @return {boolean} False if a block was found, to prevent further events,
417 		 *                   true otherwise.
418 		 * @TODO move to block-jump.js
419 		 */
420 		processCursor: function (range, keyCode) {
421 			if (!range.collapsed) {
422 				return true;
423 			}
424 
425 			BlockJump.removeZeroWidthTextNodeFix();
426 
427 428 			var node = range.startContainer,
429 				selection = Aloha.getSelection();
430 
431 			if (!node) {
432 				return true;
433 			}
434 
435 			var sibling, offset;
436 
437 			// special handling for moving Cursor around zero-width whitespace in IE7
438 			if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) <= 7 && isTextNode(node)) {
439 				if (keyCode == 37) {
440 					// moving left -> skip zwsp to the left
441 					offset = range.startOffset;
442 					while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') {
443 						offset--;
444 					}
445 					if (offset != range.startOffset) {
446 						range.setStart(range.startContainer, offset);
447 						range.setEnd(range.startContainer, offset);
448 						selection = Aloha.getSelection();
449 						selection.removeAllRanges();
450 						selection.addRange(range);
451 					}
452 				} else if (keyCode == 39) {
453 					// moving right -> skip zwsp to the right
454 					offset = range.startOffset;
455 					while (offset < node.data.length && node.data.charAt(offset) === '\u200b') {
456 						offset++;
457 					}
458 					if (offset != range.startOffset) {
459 						range.setStart(range.startContainer, offset);
460 						range.setEnd(range.startContainer, offset);
461 						selection.removeAllRanges();
462 						selection.addRange(range);
463 					}
464 				}
465 			}
466 
467 			// Versions of Internet Explorer that are older that 9, will
468 			// erroneously allow you to enter and edit inside elements which have
469 			// their contenteditable attribute set to false...
470 			if (isOldIE && !jQuery(node).contentEditable()) {
471 				var $parentBlock = jQuery(node).parents('[contenteditable=false]');
472 				var isInsideBlock = $parentBlock.length > 0;
473 
474 				if (isInsideBlock) {
475 					if (isBlockInsideEditable($parentBlock)) {
476 						sibling = $parentBlock[0];
477 					} else {
478 						return true;
479 					}
480 				}
481 			}
482 
483 			var isLeft;
484 			if (!sibling) {
485 				// True if keyCode denotes LEFT or UP arrow key, otherwise they
486 				// keyCode is for RIGHT or DOWN in which this value will be false.
487 				isLeft = (37 === keyCode || 38 === keyCode);
488 				offset = range.startOffset;
489 
490 				if (isTextNode(node)) {
491 					if (isLeft) {
492 						var isApproachingFrontPosition = (1 === offset);
493 						if (!isApproachingFrontPosition && !isFrontPosition(node, offset)) {
494 							return true;
495 						}
496 					} else if (!isEndPosition(node, offset)) {
497 						return true;
498 					}
499 
500 				} else {
501 					node = node.childNodes[offset === nodeLength(node) ? offset - 1 : offset];
502 				}
503 
504 				sibling = isLeft ? prevVisibleNode(node) : nextVisibleNode(node);
505 			}
506 
507 			if (isBlock(sibling)) {
508 				jumpBlock(sibling, isLeft, range);
509 				return false;
510 			}
511 
512 			return true;
513 		},
514 
515 		/**
516 		 * method handling shiftEnter
517 		 * @param Aloha.Selection.SelectionRange of the current selection
518 		 * @return void
519 		 */
520 		processShiftEnter: function (rangeObject) {
521 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
522 		},
523 
524 		/**
525 		 * method handling Enter
526 		 * @param Aloha.Selection.SelectionRange of the current selection
527 		 * @return void
528 		 */
529 		processEnter: function (rangeObject) {
530 			if (rangeObject.splitObject) {
531 				// 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
532 				// if ( jQuery.browser.msie
533 				//      && GENTICS.Utils.Dom
534 				//           .isListElement( rangeObject.splitObject ) ) {
535 				//  jQuery( rangeObject.splitObject ).append(
536 				//          jQuery( document.createTextNode( '' ) ) );
537 				//  }
538 				this.splitRangeObject(rangeObject);
539 			} else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2)
540 				this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
541 			}
542 		},
543 
544 		/**
545 		 * Insert the given html markup at the current selection
546 		 * @param html html markup to be inserted
547 		 */
548 		insertHTMLCode: function (html) {
549 			var rangeObject = Aloha.Selection.rangeObject;
550 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html));
551 		},
552 
553 		/**
554 		 * insert an HTML Break <br /> into current selection
555 		 * @param Aloha.Selection.SelectionRange of the current selection
556 		 * @return void
557 		 */
558 		insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) {
559 			var i,
560 			    treeLength,
561 			    el,
562 			    jqEl,
563 			    jqElBefore,
564 			    jqElAfter,
565 			    tmpObject,
566 			    offset,
567 			    checkObj;
568 
569 			inBetweenMarkup = inBetweenMarkup || jQuery('<br/>');
570 
571 			for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
572 				el = selectionTree[i];
573 				jqEl = el.domobj ? jQuery(el.domobj) : undefined;
574 
575 				if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
576 					if (el.selection == 'collapsed') {
577 						// collapsed selection found (between nodes)
578 						if (i > 0) {
579 							// not at the start, so get the element to the left
580 							jqElBefore = jQuery(selectionTree[i - 1].domobj);
581 
582 							// and insert the break after it
583 							jqElBefore.after(inBetweenMarkup);
584 
585 						} else {
586 							// at the start, so get the element to the right
587 							jqElAfter = jQuery(selectionTree[1].domobj);
588 
589 							// and insert the break before it
590 							jqElAfter.before(inBetweenMarkup);
591 						}
592 
593 						// now set the range
594 						rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
595 						rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1;
596 						rangeObject.correctRange();
597 
598 					} else if (el.domobj && el.domobj.nodeType === 3) { // textNode
599 						// when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
600 						if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) {
601 							// TODO check whether this depends on the browser
602 							jqEl.after('<br/>');
603 						}
604 
605 						if (this.needEndingBreak()) {
606 							// when the textnode is the last inside a blocklevel element
607 							// (like p, h1, ...) we need to add an additional br as very
608 							// last object in the blocklevel element
609 							checkObj = el.domobj;
610 
611 							while (checkObj) {
612 								if (checkObj.nextSibling) {
613 									checkObj = false;
614 								} else {
615 									// go to the parent
616 									checkObj = checkObj.parentNode;
617 
618 									// found a blocklevel or list element, we are done
619 									if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) {
620 										break;
621 									}
622 
623 									// reached the limit object, we are done
624 									if (checkObj === rangeObject.limitObject) {
625 										checkObj = false;
626 									}
627 								}
628 							}
629 
630 							// when we found a blocklevel element, insert a break at the
631 							// end. Mark the break so that it is cleaned when the
632 							// content is fetched.
633 							if (checkObj) {
634 								jQuery(checkObj).append('<br class="aloha-cleanme" />');
635 							}
636 						}
637 
638 						// insert the break
639 						jqEl.between(inBetweenMarkup, el.startOffset);
640 
641 						// correct the range
642 						// count the number of previous siblings
643 						offset = 0;
644 						tmpObject = inBetweenMarkup[0];
645 						while (tmpObject) {
646 							tmpObject = tmpObject.previousSibling;
647 							++offset;
648 						}
649 
650 						rangeObject.startContainer = inBetweenMarkup[0].parentNode;
651 						rangeObject.endContainer = inBetweenMarkup[0].parentNode;
652 						rangeObject.startOffset = offset;
653 						rangeObject.endOffset = offset;
654 						rangeObject.correctRange();
655 
656 					} else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break
657 						if (jqEl.parent().find('br.aloha-ephemera').length === 0) {
658 							// but before putting it, remove all:
659 							jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove();
660 
661 							//  now put it:
662 							jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject));
663 						}
664 
665 						jqEl.after(inBetweenMarkup);
666 
667 						// now set the selection. Since we just added one break do the currect el
668 						// the new position must be el's position + 1. el's position is the index
669 						// of the el in the selection tree, which is i. then we must add
670 						// another +1 because we want to be AFTER the object, not before. therefor +2
671 						rangeObject.startContainer = rangeObject.commonAncestorContainer;
672 						rangeObject.endContainer = rangeObject.startContainer;
673 						rangeObject.startOffset = i + 2;
674 						rangeObject.endOffset = i + 2;
675 						rangeObject.update();
676 					}
677 				}
678 			}
679 			rangeObject.select();
680 		},
681 
682 		/**
683 		 * Check whether blocklevel elements need breaks at the end to visibly render a newline
684 		 * @return true if an ending break is necessary, false if not
685 		 */
686 		needEndingBreak: function () {
687 			// currently, all browser except IE need ending breaks
688 			return !jQuery.browser.msie;
689 		},
690 
691 		/**
692 		 * Get the currently selected text or false if nothing is selected (or the selection is collapsed)
693 		 * @return selected text
694 		 */
695 		getSelectedText: function () {
696 			var rangeObject = Aloha.Selection.rangeObject;
697 
698 			if (rangeObject.isCollapsed()) {
699 				return false;
700 			}
701 
702 			return this.getFromSelectionTree(rangeObject.getSelectionTree(), true);
703 		},
704 
705 		/**
706 		 * Recursive function to get the selected text from the selection tree starting at the given level
707 		 * @param selectionTree array of selectiontree elements
708 		 * @param astext true when the contents shall be fetched as text, false for getting as html markup
709 		 * @return selected text from that level (incluiding all sublevels)
710 		 */
711 		getFromSelectionTree: function (selectionTree, astext) {
712 			var text = '', i, treeLength, el, clone;
713 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
714 				el = selectionTree[i];
715 				if (el.selection == 'partial') {
716 					if (el.domobj.nodeType === 3) {
717 						// partial text node selected, get the selected part
718 						text += el.domobj.data.substring(el.startOffset, el.endOffset);
719 					} else if (el.domobj.nodeType === 1 && el.children) {
720 						// partial element node selected, do the recursion into the children
721 						if (astext) {
722 							text += this.getFromSelectionTree(el.children, astext);
723 						} else {
724 							// when the html shall be fetched, we create a clone of
725 							// the element and remove all the children
726 							clone = jQuery(el.domobj.outerHTML).empty();
727 							// then we do the recursion and add the selection into the clone
728 							clone.html(this.getFromSelectionTree(el.children, astext));
729 							// finally we get the html of the clone
730 							text += clone.outerHTML();
731 						}
732 					}
733 				} else if (el.selection == 'full') {
734 					if (el.domobj.nodeType === 3) {
735 						// full text node selected, get the text
736 						text += jQuery(el.domobj).text();
737 					} else if (el.domobj.nodeType === 1 && el.children) {
738 						// full element node selected, get the html of the node and all children
739 						text += astext ? jQuery(el.domobj).text() : jQuery(el.domobj).outerHTML();
740 					}
741 				}
742 			}
743 
744 			return text;
745 		},
746 
747 		/**
748 		 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed)
749 		 * @return {?String}
750 		 */
751 		getSelectedMarkup: function () {
752 			var rangeObject = Aloha.Selection.rangeObject;
753 			return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false);
754 		},
755 
756 		/**
757 		 * Remove the currently selected markup
758 		 */
759 		removeSelectedMarkup: function () {
760 			var rangeObject = Aloha.Selection.rangeObject,
761 				newRange;
762 
763 			if (rangeObject.isCollapsed()) {
764 				return;
765 			}
766 
767 			newRange = new Aloha.Selection.SelectionRange();
768 			// remove the selection
769 			this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange);
770 
771 			// do a cleanup now (starting with the commonancestorcontainer)
772 			newRange.update();
773 			GENTICS.Utils.Dom.doCleanup({
774 				'merge': true,
775 				'removeempty': true
776 			}, Aloha.Selection.rangeObject);
777 			Aloha.Selection.rangeObject = newRange;
778 
779 			// need to set the collapsed selection now
780 			newRange.correctRange();
781 			newRange.update();
782 			newRange.select();
783 			Aloha.Selection.updateSelection();
784 		},
785 
786 		/**
787 		 * Recursively remove the selected items, starting with the given level in the selectiontree
788 		 * @param selectionTree current level of the selectiontree
789 		 * @param newRange new collapsed range to be set after the removal
790 		 */
791 		removeFromSelectionTree: function (selectionTree, newRange) {
792 			// remember the first found partially selected element node (in case we need
793 			// to merge it with the last found partially selected element node)
794 			var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength;
795 
796 			// iterate through the selection tree
797 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
798 				el = selectionTree[i];
799 
800 				// check the type of selection
801 				if (el.selection == 'partial') {
802 					if (el.domobj.nodeType === 3) {
803 						// partial text node selected, so remove the selected portion
804 						newdata = '';
805 						if (el.startOffset > 0) {
806 							newdata += el.domobj.data.substring(0, el.startOffset);
807 						}
808 809 						if (el.endOffset < el.domobj.data.length) {
810 							newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length);
811 						}
812 						el.domobj.data = newdata;
813 
814 						// eventually set the new range (if not done before)
815 						if (!newRange.startContainer) {
816 							newRange.startContainer = newRange.endContainer = el.domobj;
817 							newRange.startOffset = newRange.endOffset = el.startOffset;
818 						}
819 					} else if (el.domobj.nodeType === 1 && el.children) {
820 						// partial element node selected, so do the recursion into the children
821 						this.removeFromSelectionTree(el.children, newRange);
822 
823 						if (firstPartialElement) {
824 							// when the first parially selected element is the same type
825 							// of element, we need to merge them
826 							if (firstPartialElement.nodeName == el.domobj.nodeName) {
827 								// merge the nodes
828 								jQuery(firstPartialElement).append(jQuery(el.domobj).contents());
829 
830 								// and remove the latter one
831 								jQuery(el.domobj).remove();
832 							}
833 
834 						} else {
835 							// remember this element as first partially selected element
836 							firstPartialElement = el.domobj;
837 						}
838 					}
839 
840 				} else if (el.selection == 'full') {
841 					// eventually set the new range (if not done before)
842 					if (!newRange.startContainer) {
843 						adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
844 							el.domobj.parentNode,
845 							GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1,
846 							false,
847 							{
848 								'blocklevel': false
849 							}
850 						);
851 
852 						if (adjacentTextNode) {
853 							newRange.startContainer = newRange.endContainer = adjacentTextNode;
854 							newRange.startOffset = newRange.endOffset = 0;
855 						} else {
856 							newRange.startContainer = newRange.endContainer = el.domobj.parentNode;
857 							newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1;
858 						}
859 					}
860 
861 					// full node selected, so just remove it (will also remove all children)
862 					jQuery(el.domobj).remove();
863 				}
864 			}
865 		},
866 
867 		/**
868 		 * split passed rangeObject without or with optional markup
869 		 * @param Aloha.Selection.SelectionRange of the current selection
870 		 * @param markup object (jQuery) to insert in between the split elements
871 		 * @return void
872 		 */
873 		splitRangeObject: function (rangeObject, markup) {
874 			// UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split
875 			// object which is split up
876 			var splitObject = jQuery(rangeObject.splitObject),
877 				selectionTree,
878 			    insertAfterObject,
879 			    followUpContainer;
880 
881 			// update the commonAncestor with the splitObject (so that the selectionTree is correct)
882 			rangeObject.update(rangeObject.splitObject); // set the splitObject as new commonAncestorContainer and update the selectionTree
883 
884 			// calculate the selection tree. NOTE: it is necessary to do this before
885 			// getting the followupcontainer, since getting the selection tree might
886 			// possibly merge text nodes, which would lead to differences in the followupcontainer
887 			selectionTree = rangeObject.getSelectionTree();
888 
889 			// object to be inserted after the splitObject
890 			followUpContainer = this.getSplitFollowUpContainer(rangeObject);
891 
892 			// now split up the splitObject into itself AND the followUpContainer
893 			this.splitRangeObjectHelper(selectionTree, rangeObject, followUpContainer); // split the current object into itself and the followUpContainer
894 
895 			// check whether the followupcontainer is still marked for removal
896 			if (followUpContainer.hasClass('preparedForRemoval')) {
897 				// TODO shall we just remove the class or shall we not use the followupcontainer?
898 				followUpContainer.removeClass('preparedForRemoval');
899 			}
900 
901 			// now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in
902 			// some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list)
903 			insertAfterObject = this.getInsertAfterObject(rangeObject, followUpContainer);
904 
905 			// now insert the followUpContainer
906 			jQuery(followUpContainer).insertAfter(insertAfterObject); // attach the followUpContainer right after the insertAfterObject
907 
908 			// in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice)
909 			if (rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator(rangeObject.splitObject, followUpContainer)) {
910 				jQuery(rangeObject.splitObject).remove();
911 			}
912 
913 			rangeObject.startContainer = null;
914 			// first check whether the followUpContainer starts with a <br/>
915 			// if so, place the cursor right before the <br/>
916 			var followContents = followUpContainer.contents();
917 			if (followContents.length > 0 && followContents.get(0).nodeType == 1 && followContents.get(0).nodeName.toLowerCase() === 'br') {
918 				rangeObject.startContainer = followUpContainer.get(0);
919 			}
920 
921 			if (!rangeObject.startContainer) {
922 				// find a possible text node in the followUpContainer and set the selection to it
923 				// if no textnode is available, set the selection to the followup container itself
924 				rangeObject.startContainer = followUpContainer.textNodes(true, true).first().get(0);
925 			}
926 			if (!rangeObject.startContainer) { // if no text node was found, select the parent object of <br class="aloha-ephemera" />
927 				rangeObject.startContainer = followUpContainer.textNodes(false).first().parent().get(0);
928 			}
929 			if (rangeObject.startContainer) {
930 				// the cursor is always at the beginning of the followUp
931 				rangeObject.endContainer = rangeObject.startContainer;
932 				rangeObject.startOffset = 0;
933 				rangeObject.endOffset = 0;
934 			} else {
935 				rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get(0);
936 				rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(followUpContainer.get(0));
937 			}
938 
939 			// finally update the range object again
940 			rangeObject.update();
941 
942 			// now set the selection
943 			rangeObject.select();
944 		},
945 
946 		/**
947 		 * method to get the object after which the followUpContainer can be inserted during splitup
948 		 * this is a helper method, not needed anywhere else
949 		 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
950 		 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
951 		 * @return object after which the followUpContainer can be inserted
952 		 */
953 		getInsertAfterObject: function (rangeObject, followUpContainer) {
954 			var passedSplitObject, i, el;
955 
956 			for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) {
957 				el = rangeObject.markupEffectiveAtStart[i];
958 
959 				// check if we have already passed the splitObject (some other markup might come before)
960 				if (el === rangeObject.splitObject) {
961 					passedSplitObject = true;
962 				}
963 
964 				// if not passed splitObject, skip this markup
965 				if (!passedSplitObject) {
966 					continue;
967 				}
968 
969 				// once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent
970 				if (Aloha.Selection.canTag1WrapTag2(jQuery(el).parent()[0].nodeName, followUpContainer[0].nodeName)) {
971 					return el;
972 				}
973 			}
974 
975 			return false;
976 		},
977 
978 		/**
979 		 * @fixme: Someone who knows what this function does, please refactor it.
980 		 *			1. splitObject arg is not used at all
981 		 *			2. Would be better to use ternary operation would be better than if else statement
982 		 *
983 		 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height
984 		 * @param splitObject split object (dom object)
985 		 * @return fillUpElement HTML Code
986 		 */
987 		getFillUpElement: function (splitObject) {
988 			if (jQuery.browser.msie) {
989 				return false;
990 			}
991 			return jQuery('<br class="aloha-cleanme"/>');
992 		},
993 
994 		/**
995 		 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags)
996 		 * @param domArray array of domObjects
997 		 * @return void
998 		 */
999 		removeElementContentWhitespaceObj: function (domArray) {
1000 			var correction = 0,
1001 				removeLater = [],
1002 				i,
1003 				el,
1004 			    removeIndex;
1005 
1006 			for (i = 0; i < domArray.length; ++i) {
1007 				el = domArray[i];
1008 				if (el.isElementContentWhitespace) {
1009 					removeLater[removeLater.length] = i;
1010 				}
1011 			}
1012 
1013 			for (i = 0; i < removeLater.length; ++i) {
1014 				removeIndex = removeLater[i];
1015 				domArray.splice(removeIndex - correction, 1);
1016 				++correction;
1017 			}
1018 		},
1019 
1020 		/**
1021 		 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other
1022 		 * @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
1023 		 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
1024 		 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
1025 		 * @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
1026 		 * @return void
1027 		 */
1028 		splitRangeObjectHelper: function (selectionTree, rangeObject, followUpContainer, inBetweenMarkup) {
1029 			if (!followUpContainer) {
1030 				Aloha.Log.warn(this, 'no followUpContainer, no inBetweenMarkup, nothing to do...');
1031 			}
1032 
1033 			var fillUpElement = this.getFillUpElement(rangeObject.splitObject),
1034 				splitObject = jQuery(rangeObject.splitObject),
1035 				startMoving = false,
1036 				el,
1037 				i,
1038 				completeText,
1039 				jqObj,
1040 				mirrorLevel,
1041 				parent,
1042 				treeLength;
1043 
1044 			if (selectionTree.length > 0) {
1045 				mirrorLevel = followUpContainer.contents();
1046 
1047 				// if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes
1048 				if (mirrorLevel.length !== selectionTree.length) {
1049 					this.removeElementContentWhitespaceObj(mirrorLevel);
1050 				}
1051 
1052 				for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
1053 					el = selectionTree[i];
1054 
1055 					// remove all objects in the mirrorLevel, which are BEFORE the cursor
1056 					// OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended)
1057 					if ((el.selection === 'none' && startMoving === false) || (el.domobj && el.domobj.nodeType === 3 && el === selectionTree[(selectionTree.length - 1)] && el.startOffset === el.domobj.data.length)) {
1058 						// iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer
1059 						// however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead
1060 						// otherwise the followUpContainer is invalid and takes up no vertical space
1061 
1062 						if (followUpContainer.textNodes().length > 1 || (el.domobj.nodeType === 1 && el.children.length === 0)) {
1063 							// note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition,
1064 							// where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be
1065 							// removed correctly otherwise
1066 							mirrorLevel.eq(i).remove();
1067 
1068 						} else if (GENTICS.Utils.Dom.isSplitObject(followUpContainer[0])) {
1069 							if (fillUpElement) {
1070 								followUpContainer.html(fillUpElement); // for your zoological german knowhow: ephemera = Eintagsfliege
1071 							} else {
1072 								followUpContainer.empty();
1073 							}
1074 
1075 						} else {
1076 							followUpContainer.empty();
1077 							followUpContainer.addClass('preparedForRemoval');
1078 						}
1079 
1080 						continue;
1081 
1082 					} else {
1083 						// split objects, which are AT the cursor Position or directly above
1084 						if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
1085 							// TODO better check for selection == 'partial' here?
1086 							if (el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined) {
1087 								completeText = el.domobj.data;
1088 								if (el.startOffset > 0) { // first check, if there will be some text left in the splitObject
1089 									el.domobj.data = completeText.substr(0, el.startOffset);
1090 								} 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
1091 									jQuery(el.domobj).remove();
1092 								} else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break
1093 									// if the parent is a blocklevel element, we insert the fillup element
1094 									parent = jQuery(el.domobj).parent();
1095 									if (GENTICS.Utils.Dom.isSplitObject(parent[0])) {
1096 										if (fillUpElement) {
1097 											parent.html(fillUpElement);
1098 										} else {
1099 											parent.empty();
1100 										}
1101 
1102 									} else {
1103 										// if the parent is no blocklevel element and would be empty now, we completely remove it
1104 										parent.remove();
1105 									}
1106 								}
1107 								if (completeText.length - el.startOffset > 0) {
1108 									// 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
1109 									mirrorLevel[i].data = completeText.substr(el.startOffset, completeText.length);
1110 								} else if (mirrorLevel.length > 1) {
1111 									// if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed
1112 									mirrorLevel.eq((i)).remove();
1113 								} else if (GENTICS.Utils.Dom.isBlockLevelElement(followUpContainer[0])) {
1114 									// if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break
1115 									if (fillUpElement) {
1116 										followUpContainer.html(fillUpElement);
1117 									} else {
1118 										followUpContainer.empty();
1119 									}
1120 
1121 								} else {
1122 									// if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal
1123 									followUpContainer.empty();
1124 									followUpContainer.addClass('preparedForRemoval');
1125 								}
1126 							}
1127 
1128 							startMoving = true;
1129 
1130 							if (el.children.length > 0) {
1131 								this.splitRangeObjectHelper(el.children, rangeObject, mirrorLevel.eq(i), inBetweenMarkup);
1132 							}
1133 
1134 						} else {
1135 							// remove all objects in the origin, which are AFTER the cursor
1136 							if (el.selection === 'none' && startMoving === true) {
1137 								// iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer
1138 								jqObj = jQuery(el.domobj).remove();
1139 							}
1140 						}
1141 					}
1142 				}
1143 			} else {
1144 				Aloha.Log.error(this, 'can not split splitObject due to an empty selection tree');
1145 			}
1146 
1147 			// and finally cleanup: remove all fillUps > 1
1148 			splitObject.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
1149 			followUpContainer.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
1150 
1151 			// remove objects prepared for removal
1152 			splitObject.find('.preparedForRemoval').remove();
1153 			followUpContainer.find('.preparedForRemoval').remove();
1154 
1155 			// if splitObject / followUp are empty, place a fillUp inside
1156 			if (splitObject.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(splitObject[0]) && fillUpElement) {
1157 				splitObject.html(fillUpElement);
1158 			}
1159 
1160 			if (followUpContainer.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(followUpContainer[0]) && fillUpElement) {
1161 				followUpContainer.html(fillUpElement);
1162 			}
1163 		},
1164 
1165 		/**
1166 		 * returns a jQuery object fitting the passed splitObject as follow up object
1167 		 * examples,
1168 		 * - when passed a p it will return an empty p (clone of the passed p)
1169 		 * - 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)
1170 		 * @param rangeObject Aloha.RangeObject
1171 		 * @return void
1172 		 */
1173 		getSplitFollowUpContainer: function (rangeObject) {
1174 			var tagName = rangeObject.splitObject.nodeName.toLowerCase(),
1175 				returnObj,
1176 				inside,
1177 				lastObj;
1178 
1179 			switch (tagName) {
1180 			case 'h1':
1181 			case 'h2':
1182 			case 'h3':
1183 			case 'h4':
1184 			case 'h5':
1185 			case 'h6':
1186 				// get the last textnode in the splitobject, but don't consider aloha-cleanme elements
1187 				lastObj = jQuery(rangeObject.splitObject).textNodes(':not(.aloha-cleanme)').last()[0];
1188 				// special case: when enter is hit at the end of a heading, the followUp should be a <p>
1189 				if (lastObj && rangeObject.startContainer === lastObj && rangeObject.startOffset === lastObj.length) {
1190 					returnObj = jQuery('<p></p>');
1191 					inside = jQuery(rangeObject.splitObject.outerHTML).contents();
1192 					returnObj.append(inside);
1193 					return returnObj;
1194 				}
1195 				break;
1196 
1197 			case 'li':
1198 				// TODO check whether the li is the last one
1199 				// special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list)
1200 				if (rangeObject.startContainer.nodeName.toLowerCase() === 'br' && jQuery(rangeObject.startContainer).hasClass('aloha-ephemera')) {
1201 					returnObj = jQuery('<p></p>');
1202 					inside = jQuery(rangeObject.splitObject.outerHTML).contents();
1203 					returnObj.append(inside);
1204 					return returnObj;
1205 				}
1206 				// when the li is the last one and empty, we also just return a <p>
1207 				if (!rangeObject.splitObject.nextSibling && jQuery.trim(jQuery(rangeObject.splitObject).text()).length === 0) {
1208 					returnObj = jQuery('<p></p>');
1209 					return returnObj;
1210 				}
1211 				break;
1212 			}
1213 
1214 			return jQuery(rangeObject.splitObject.outerHTML);
1215 		},
1216 
1217 		/**
1218 		 * Transform the given domobj into an object with the given new nodeName.
1219 		 * Preserves the content and all attributes. If a range object is given, also the range will be preserved
1220 		 * @param domobj dom object to transform
1221 		 * @param nodeName new node name
1222 		 * @param range range object
1223 		 * @api
1224 		 * @return new object as jQuery object
1225 		 */
1226 		transformDomObject: function (domobj, nodeName, range) {
1227 			// first create the new element
1228 			var jqOldObj = jQuery(domobj),
1229 				jqNewObj = jQuery('<' + nodeName + '>'),
1230 				i,
1231 				attributes = jqOldObj[0].cloneNode(false).attributes;
1232 
1233 			// TODO what about events?
1234 			// copy attributes
1235 			if (attributes) {
1236 				for (i = 0; i < attributes.length; ++i) {
1237 					if (typeof attributes[i].specified === 'undefined' || attributes[i].specified) {
1238 						jqNewObj.attr(attributes[i].nodeName, attributes[i].nodeValue);
1239 					}
1240 				}
1241 			}
1242 
1243 			// copy inline CSS
1244 			if (jqOldObj[0].style && jqOldObj[0].style.cssText) {
1245 				jqNewObj[0].style.cssText = jqOldObj[0].style.cssText;
1246 			}
1247 
1248 			// now move the contents of the old dom object into the new dom object
1249 			jqOldObj.contents().appendTo(jqNewObj);
1250 
1251 			// finally replace the old object with the new one
1252 			jqOldObj.replaceWith(jqNewObj);
1253 
1254 			// preserve the range
1255 			if (range) {
1256 				if (range.startContainer == domobj) {
1257 					range.startContainer = jqNewObj.get(0);
1258 				}
1259 
1260 				if (range.endContainer == domobj) {
1261 					range.endContainer = jqNewObj.get(0);
1262 				}
1263 			}
1264 
1265 			return jqNewObj;
1266 		},
1267 
1268 		/**
1269 		 * String representation
1270 		 * @return {String}
1271 		 */
1272 		toString: function () {
1273 			return 'Aloha.Markup';
1274 		}
1275 
1276 	});
1277 
1278 	Aloha.Markup = new Aloha.Markup();
1279 	return Aloha.Markup;
1280 });
1281