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 	 * recursively search through parent nodes to find if
272 	 * node is child of a specific node.
273 	 *
274 	 * @param {DOMNode} starting node
275 	 * @param {Array[String]} Array of UPPERCASE (!) node names to search for, eg. ["TD"] or ["TD", "TH"].
276 	 * @return true if node is child of a node of nodeName, false otherwise
277 	 */
278 	function isChildOf(node, nodeNames) {
279 		var i;
280 		if (node.parentNode) {
281 			for (i = 0; i < nodeNames.length; i++) {
282 				if (nodeNames[i] === node.parentNode.nodeName) {
283 					return true;
284 				}
285 			}
286 			return isChildOf(node.parentNode, nodeNames);
287 		} else {
288 			return false;
289 		}
290 	}
291 
292 	/**
293 	 * Will recursively check if the current node is the first node in
294 	 * it's hierarchy up it's ancestor tree until the stopNode is reached.
295 	 * Useful to find eg. if you're in the first td within a table.
296 	 * Will stop if stopNodeName is encountered or the root node is reached.
297 	 *
298 	 * @param {DOMnode} node to start from
299 	 * @param {String} UPPERCASE node name to stop search at
300 	 * @return true if node is the first node, false otherwise
301 	 */
302 	function isFirstNode(node, stopNodeName) {
303 		if (!node.parentNode) {
304 			return true;
305 		}
306 
307 		// firstChild will also find textNodes while children[0] will only return non-text nodes
308 		var isTextNode = ((node.nodeType === 3 && node.parentNode.firstChild === node) || (node.parentNode.children[0] === node));
309 
310 		// unfortunately we need to take care of the aloha-table-selectrow and aloha-table-selectcolumn
311 		var isTableSelectRow = node.nodeName === 'TR' && node.parentNode.children[0].className.indexOf('aloha-table-selectcolumn') !== -1;
312 		var isTableSelectColumn = node.nodeName === 'TD' && node.parentNode.children[0].className.indexOf('aloha-table-selectrow') !== -1;
313 		var isFirstNodeOfTable = ((isTableSelectColumn || isTableSelectRow) && node.parentNode.children[1] === node);
314 
315 		if (isTextNode || isFirstNodeOfTable) {
316 			if (node.parentNode.nodeName === stopNodeName) {
317 				return true;
318 			} else {
319 				return isFirstNode(node.parentNode, stopNodeName);
320 			}
321 		} else {
322 			return false;
323 		}
324 	}
325 
326 	/**
327 	 * Will recurseively check if the current node is the last node in
328 	 * it's hierarchy up it's ancestor tree until the stopNode is reached.
329 	 * Useful to find eg. if you're in the last td within a table.
330 	 * Will stop if stopNodeName is encountered or the root node is reached.
331 	 * Will ignore whitespace text nodes and caption nodes
332 	 *
333 	 * @param {DOMnode} node to start from
334 	 * @param {String} UPPERCASE node name to stop search at
335 	 * @return true if node is the last node, false otherwise
336 	 */
337 	// implemented as an IIFE because the local helper getLast() should only be defined once
338 	var isLastNode = (function () {
339 		// get the last node that is not empty text or a table caption
340 		function getLast(node) {
341 			var last, i;
342 
343 			for (i = node.childNodes.length - 1; i > -1; i--) {
344 				last = node.childNodes[i];
345 				if (last.nodeName !== 'CAPTION' && !(last.nodeType === 3 && /^[\t\n\r ]+$/.test(last.data))) {
346 					return last;
347 				}
348 			}
349 
350 			return node.lastChild;
351 		}
352 
353 		return function (node, stopNodeName) {
354 			if (!node.parentNode) {
355 				return true;
356 			}
357 
358 			if (getLast(node.parentNode) === node) {
359 				if (node.parentNode.nodeName === stopNodeName) {
360 					return true;
361 				} else {
362 					return isLastNode(node.parentNode, stopNodeName);
363 				}
364 			} else {
365 				return false;
366 			}
367 		};
368 	}());
369 
370 	/**
371 	 * Markup object
372 	 */
373 	Aloha.Markup = Class.extend({
374 
375 		/**
376 		 * Key handlers for special key codes
377 		 */
378 		keyHandlers: {},
379 
380 		/**
381 		 * Add a key handler for the given key code
382 		 * @param keyCode key code
383 		 * @param handler handler function
384 		 */
385 		addKeyHandler: function (keyCode, handler) {
386 			if (!this.keyHandlers[keyCode]) {
387 				this.keyHandlers[keyCode] = [];
388 			}
389 
390 			this.keyHandlers[keyCode].push(handler);
391 		},
392 
393 		/**
394 		 * Removes a key handler for the given key code
395 		 * @param keyCode key code
396 		 */
397 		removeKeyHandler: function (keyCode) {
398 			if (this.keyHandlers[keyCode]) {
399 				this.keyHandlers[keyCode] = null;
400 			}
401 		},
402 
403 		insertBreak: function () {
404 			var range = Aloha.Selection.rangeObject,
405 				nonWSIndex,
406 				nextTextNode,
407 				newBreak;
408 
409 			if (!range.isCollapsed()) {
410 				this.removeSelectedMarkup();
411 			}
412 
413 			newBreak = jQuery('<br/>');
414 			GENTICS.Utils.Dom.insertIntoDOM(newBreak, range, Aloha.activeEditable.obj);
415 
416 			nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
417 				newBreak.parent().get(0),
418 				GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1,
419 				false
420 			);
421 
422 			if (nextTextNode) {
423 				// trim leading whitespace
424 				nonWSIndex = nextTextNode.data.search(/\S/);
425 				if (nonWSIndex > 0) {
426 					nextTextNode.data = nextTextNode.data.substring(nonWSIndex);
427 				}
428 			}
429 
430 			range.startContainer = range.endContainer = newBreak.get(0).parentNode;
431 			range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent(newBreak.get(0)) + 1;
432 			range.correctRange();
433 			range.clearCaches();
434 			range.select();
435 		},
436 
437 		/**
438 		 * first method to handle key strokes
439 		 * @param event DOM event
440 		 * @param rangeObject as provided by Aloha.Selection.getRangeObject();
441 		 * @return "Aloha.Selection"
442 		 */
443 		preProcessKeyStrokes: function (event) {
444 			if (event.type !== 'keydown') {
445 				return false;
446 			}
447 
448 			var rangeObject,
449 			    handlers,
450 			    i;
451 
452 			if (this.keyHandlers[event.keyCode]) {
453 				handlers = this.keyHandlers[event.keyCode];
454 				for (i = 0; i < handlers.length; ++i) {
455 					if (!handlers[i](event)) {
456 						return false;
457 					}
458 				}
459 			}
460 
461 			// LEFT (37), RIGHT (39) keys for block detection
462 			if (event.keyCode === 37 || event.keyCode === 39) {
463 				if (Aloha.getSelection().getRangeCount()) {
464 					rangeObject = Aloha.getSelection().getRangeAt(0);
465 
466 					if (this.processCursor(rangeObject, event.keyCode)) {
467 						cleanupPlaceholders(Aloha.Selection.rangeObject);
468 						return true;
469 					}
470 				}
471 
472 				return false;
473 			}
474 
475 			// UP (38), DOWN (40) keys for table navigation
476 			if (event.keyCode === 38 || event.keyCode === 40) {
477 				if (Aloha.getSelection().getRangeCount()) {
478 					rangeObject = Aloha.getSelection().getRangeAt(0);
479 					if (this.processCursorUpDown(rangeObject, event.keyCode)) {
480 						return false;
481 					}
482 				}
483 				return true;
484 			}
485 
486 			// BACKSPACE
487 			if (event.keyCode === 8) {
488 				event.preventDefault(); // prevent history.back() even on exception
489 				Aloha.execCommand('delete', false);
490 				return false;
491 			}
492 
493 			// DELETE
494 			if (event.keyCode === 46) {
495 				Aloha.execCommand('forwarddelete', false);
496 				return false;
497 			}
498 
499 			// ENTER
500 			if (event.keyCode === 13) {
501 				if (!event.shiftKey && Html.allowNestedParagraph(Aloha.activeEditable)) {
502 					Aloha.execCommand('insertparagraph', false);
503 					return false;
504 				// if the shift key is pressed, or if the active editable is not allowed
505 				// to contain paragraphs, a linebreak is inserted instead
506 				} else {
507 					Aloha.execCommand('insertlinebreak', false);
508 					return false;
509 				}
510 			}
511 			return true;
512 		},
513 
514 		/**
515 		 * processing up and down cursor keys inside tables
516 		 * will only try to figure out if cursor is at first
517 		 * or last position in table and exit to the next
518 		 * editable node from there
519 		 *
520 		 * won't do anything if range is not collapsed
521 		 * 
522 		 * @param {RangyRange} range A range object for the current selection.
523 		 * @param {number} keyCode Code of the currently pressed key.
524 		 * @return {boolean} true if something was done, false if browser should 
525 		 * continue handling the event
526 		 */
527 		processCursorUpDown: function (range, keyCode) {
528 			if (!range.collapsed) {
529 				return false;
530 			}
531 
532 			var node = range.startContainer,
533 				tableWrapper,
534 				cursorNode;
535 
536 			// UP
537 			if (keyCode === 38 &&
538 					isFrontPosition(node, range.startOffset) &&
539 					isChildOf(node, ['TD', 'TH']) &&
540 					isFirstNode(node, 'TABLE')) {
541 
542 				// we want to position the cursor now in the first 
543 				// element before the table, so we need to find the
544 				// table wrapper first ...
545 				tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0);
546 				if (!tableWrapper) {
547 					return false;
548 				}
549 
550 				// ... and then find it's previousSibling
551 				// which we will descend down to its deepest
552 				// nested child node, where we will put the
553 				// cursor
554 				// prefer previousElemntSibling because Firefox will land you in a
555 				// whitespace text node between a preceding <p> and the table otherwise
556 				if (tableWrapper.previousElementSibling) {
557 					cursorNode = tableWrapper.previousElementSibling;
558 				} else {
559 					cursorNode = tableWrapper.previousSibling;
560 				}
561 				while (cursorNode.nodeType !== 3) {
562 					cursorNode = cursorNode.lastChild;
563 					if (cursorNode === null) {
564 						// stop if there is no element to be entered before the table
565 						return false;
566 					}
567 				}
568 
569 				Aloha.Selection.rangeObject.startContainer = cursorNode;
570 				Aloha.Selection.rangeObject.endContainer = cursorNode;
571 				Aloha.Selection.rangeObject.startOffset = cursorNode.length;
572 				Aloha.Selection.rangeObject.endOffset = cursorNode.length;
573 				Aloha.Selection.rangeObject.select();
574 
575 				// Mozilla needs this fix or else the selection will not work
576 				if (Aloha.activeEditable && jQuery.browser.mozilla) {
577 					Aloha.activeEditable.obj.focus();
578 				}
579 
580 				return true;
581 
582 			// DOWN
583 			} else if (keyCode === 40 &&
584 					isEndPosition(node, range.startOffset) &&
585 					isChildOf(node, ['TD', 'TH']) &&
586 					isLastNode(node, 'TABLE')) {
587 
588 				// we want to put the cursor in the first element right 
589 				// after the table so we need to find the table wrapper first
590 				tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0);
591 				if (!tableWrapper) {
592 					return false;
593 				}
594 
595 				// and now find its following sibling where we will put
596 				// the cursor in the first position
597 				// the next elementSibling is preffered over the nextSibling
598 				// because Mozilla will sometimes have an empty text node
599 				// right next to the table - but we most likely want to put 
600 				// the cursor into the next paragraph
601 				if (tableWrapper.nextElementSibling) {
602 					cursorNode = tableWrapper.nextElementSibling;
603 				} else {
604 					cursorNode = tableWrapper.nextSibling;
605 				}
606 
607 				while (cursorNode.nodeType !== 3) {
608 					cursorNode = cursorNode.firstChild;
609 					if (cursorNode === null) {
610 						return false;
611 					}
612 				}
613 
614 				Aloha.Selection.rangeObject.startContainer = cursorNode;
615 				Aloha.Selection.rangeObject.endContainer = cursorNode;
616 				Aloha.Selection.rangeObject.startOffset = 0;
617 				Aloha.Selection.rangeObject.endOffset = 0;
618 				Aloha.Selection.rangeObject.select();
619 
620 				// Mozilla needs this fix or else the selection will not work
621 				if (Aloha.activeEditable && jQuery.browser.mozilla) {
622 					Aloha.activeEditable.obj.focus();
623 				}
624 
625 				return true;
626 
627 			} else {
628 				return false;
629 			}
630 		},
631 
632 		/**
633 		 * Processing of cursor keys.
634 		 * Detect blocks (elements with contenteditable=false) and will select them
635 		 * (normally the cursor would simply jump right past them).
636 		 *
637 		 * For each block that is selected, an 'aloha-block-selected' event will be
638 		 * triggered.
639 		 *
640 		 * TODO: the above is what should happen. Currently we just skip past blocks.
641 		 *
642 		 * @param {RangyRange} range A range object for the current selection.
643 		 * @param {number} keyCode Code of the currently pressed key.
644 		 * @return {boolean} False if a block was found, to prevent further events,
645 		 *                   true otherwise.
646 		 * @TODO move to block-jump.js
647 		 */
648 		processCursor: function (range, keyCode) {
649 			if (!range.collapsed) {
650 				return true;
651 			}
652 
653 			BlockJump.removeZeroWidthTextNodeFix();
654 
655 			var node = range.startContainer,
656 				selection = Aloha.getSelection();
657 
658 			if (!node) {
659 				return true;
660 			}
661 
662 			var sibling, offset;
663 
664 			// special handling for moving Cursor around zero-width whitespace in IE7
665 			if (jQuery.browser.msie && parseInt(jQuery.browser.version, 10) <= 7 && isTextNode(node)) {
666 				if (keyCode == 37) {
667 					// moving left -> skip zwsp to the left
668 					offset = range.startOffset;
669 					while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') {
670 						offset--;
671 					}
672 					if (offset != range.startOffset) {
673 						range.setStart(range.startContainer, offset);
674 						range.setEnd(range.startContainer, offset);
675 						selection = Aloha.getSelection();
676 						selection.removeAllRanges();
677 						selection.addRange(range);
678 					}
679 				} else if (keyCode == 39) {
680 					// moving right -> skip zwsp to the right
681 					offset = range.startOffset;
682 					while (offset < node.data.length && node.data.charAt(offset) === '\u200b') {
683 						offset++;
684 					}
685 					if (offset != range.startOffset) {
686 						range.setStart(range.startContainer, offset);
687 						range.setEnd(range.startContainer, offset);
688 						selection.removeAllRanges();
689 						selection.addRange(range);
690 					}
691 				}
692 			}
693 
694 			// Versions of Internet Explorer that are older that 9, will
695 			// erroneously allow you to enter and edit inside elements which have
696 			// their contenteditable attribute set to false...
697 			if (isOldIE && !jQuery(node).contentEditable()) {
698 				var $parentBlock = jQuery(node).parents('[contenteditable=false]');
699 				var isInsideBlock = $parentBlock.length > 0;
700 
701 				if (isInsideBlock) {
702 					if (isBlockInsideEditable($parentBlock)) {
703 						sibling = $parentBlock[0];
704 					} else {
705 						return true;
706 					}
707 				}
708 			}
709 
710 			var isLeft;
711 			if (!sibling) {
712 				// True if keyCode denotes LEFT or UP arrow key, otherwise they
713 				// keyCode is for RIGHT or DOWN in which this value will be false.
714 				isLeft = (37 === keyCode || 38 === keyCode);
715 				offset = range.startOffset;
716 
717 				if (isTextNode(node)) {
718 					if (isLeft) {
719 						var isApproachingFrontPosition = (1 === offset);
720 						if (!isApproachingFrontPosition && !isFrontPosition(node, offset)) {
721 							return true;
722 						}
723 					} else if (!isEndPosition(node, offset)) {
724 						return true;
725 					}
726 
727 				} else {
728 					node = node.childNodes[offset === nodeLength(node) ? offset - 1 : offset];
729 				}
730 
731 				sibling = isLeft ? prevVisibleNode(node) : nextVisibleNode(node);
732 			}
733 
734 			if (isBlock(sibling)) {
735 				jumpBlock(sibling, isLeft, range);
736 				return false;
737 			}
738 
739 			return true;
740 		},
741 
742 		/**
743 		 * method handling shiftEnter
744 		 * @param Aloha.Selection.SelectionRange of the current selection
745 		 * @return void
746 		 */
747 		processShiftEnter: function (rangeObject) {
748 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
749 		},
750 
751 		/**
752 753 		 * method handling Enter
754 		 * @param Aloha.Selection.SelectionRange of the current selection
755 		 * @return void
756 		 */
757 		processEnter: function (rangeObject) {
758 			if (rangeObject.splitObject) {
759 				// 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
760 				// if ( jQuery.browser.msie
761 				//      && GENTICS.Utils.Dom
762 				//           .isListElement( rangeObject.splitObject ) ) {
763 				//  jQuery( rangeObject.splitObject ).append(
764 				//          jQuery( document.createTextNode( '' ) ) );
765 				//  }
766 				this.splitRangeObject(rangeObject);
767 			} else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2)
768 				this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject);
769 			}
770 		},
771 
772 		/**
773 		 * Insert the given html markup at the current selection
774 		 * @param html html markup to be inserted
775 		 */
776 		insertHTMLCode: function (html) {
777 			var rangeObject = Aloha.Selection.rangeObject;
778 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html));
779 		},
780 
781 		/**
782 		 * insert an HTML Break <br /> into current selection
783 		 * @param Aloha.Selection.SelectionRange of the current selection
784 		 * @return void
785 		 */
786 		insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) {
787 			var i,
788 			    treeLength,
789 			    el,
790 			    jqEl,
791 			    jqElBefore,
792 			    jqElAfter,
793 			    tmpObject,
794 			    offset,
795 			    checkObj;
796 
797 			inBetweenMarkup = inBetweenMarkup || jQuery('<br/>');
798 
799 			for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
800 				el = selectionTree[i];
801 				jqEl = el.domobj ? jQuery(el.domobj) : undefined;
802 
803 				if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
804 					if (el.selection == 'collapsed') {
805 						// collapsed selection found (between nodes)
806 						if (i > 0) {
807 							// not at the start, so get the element to the left
808 							jqElBefore = jQuery(selectionTree[i - 1].domobj);
809 
810 							// and insert the break after it
811 							jqElBefore.after(inBetweenMarkup);
812 
813 						} else {
814 							// at the start, so get the element to the right
815 							jqElAfter = jQuery(selectionTree[1].domobj);
816 
817 							// and insert the break before it
818 							jqElAfter.before(inBetweenMarkup);
819 						}
820 
821 						// now set the range
822 						rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
823 						rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1;
824 						rangeObject.correctRange();
825 
826 					} else if (el.domobj && el.domobj.nodeType === 3) { // textNode
827 						// when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
828 						if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) {
829 							// TODO check whether this depends on the browser
830 							jqEl.after('<br/>');
831 						}
832 
833 						if (this.needEndingBreak()) {
834 							// when the textnode is the last inside a blocklevel element
835 							// (like p, h1, ...) we need to add an additional br as very
836 							// last object in the blocklevel element
837 							checkObj = el.domobj;
838 
839 							while (checkObj) {
840 								if (checkObj.nextSibling) {
841 									checkObj = false;
842 								} else {
843 									// go to the parent
844 									checkObj = checkObj.parentNode;
845 
846 									// found a blocklevel or list element, we are done
847 									if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) {
848 										break;
849 									}
850 
851 									// reached the limit object, we are done
852 									if (checkObj === rangeObject.limitObject) {
853 										checkObj = false;
854 									}
855 								}
856 							}
857 
858 							// when we found a blocklevel element, insert a break at the
859 							// end. Mark the break so that it is cleaned when the
860 							// content is fetched.
861 							if (checkObj) {
862 								jQuery(checkObj).append('<br class="aloha-cleanme" />');
863 							}
864 						}
865 
866 						// insert the break
867 						jqEl.between(inBetweenMarkup, el.startOffset);
868 
869 						// correct the range
870 						// count the number of previous siblings
871 						offset = 0;
872 						tmpObject = inBetweenMarkup[0];
873 						while (tmpObject) {
874 							tmpObject = tmpObject.previousSibling;
875 							++offset;
876 						}
877 
878 						rangeObject.startContainer = inBetweenMarkup[0].parentNode;
879 						rangeObject.endContainer = inBetweenMarkup[0].parentNode;
880 						rangeObject.startOffset = offset;
881 						rangeObject.endOffset = offset;
882 						rangeObject.correctRange();
883 
884 					} else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break
885 						if (jqEl.parent().find('br.aloha-ephemera').length === 0) {
886 							// but before putting it, remove all:
887 							jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove();
888 
889 							//  now put it:
890 							jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject));
891 						}
892 
893 						jqEl.after(inBetweenMarkup);
894 
895 						// now set the selection. Since we just added one break do the currect el
896 						// the new position must be el's position + 1. el's position is the index
897 						// of the el in the selection tree, which is i. then we must add
898 						// another +1 because we want to be AFTER the object, not before. therefor +2
899 						rangeObject.startContainer = rangeObject.commonAncestorContainer;
900 						rangeObject.endContainer = rangeObject.startContainer;
901 						rangeObject.startOffset = i + 2;
902 						rangeObject.endOffset = i + 2;
903 						rangeObject.update();
904 					}
905 				}
906 			}
907 			rangeObject.select();
908 		},
909 
910 		/**
911 		 * Check whether blocklevel elements need breaks at the end to visibly render a newline
912 		 * @return true if an ending break is necessary, false if not
913 		 */
914 		needEndingBreak: function () {
915 			// currently, all browser except IE need ending breaks
916 			return !jQuery.browser.msie;
917 		},
918 
919 		/**
920 		 * Get the currently selected text or false if nothing is selected (or the selection is collapsed)
921 		 * @return selected text
922 		 */
923 		getSelectedText: function () {
924 			var rangeObject = Aloha.Selection.rangeObject;
925 
926 			if (rangeObject.isCollapsed()) {
927 				return false;
928 			}
929 
930 			return this.getFromSelectionTree(rangeObject.getSelectionTree(), true);
931 		},
932 
933 		/**
934 		 * Recursive function to get the selected text from the selection tree starting at the given level
935 		 * @param selectionTree array of selectiontree elements
936 		 * @param astext true when the contents shall be fetched as text, false for getting as html markup
937 		 * @return selected text from that level (incluiding all sublevels)
938 		 */
939 		getFromSelectionTree: function (selectionTree, astext) {
940 			var text = '', i, treeLength, el, clone;
941 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
942 				el = selectionTree[i];
943 				if (el.selection == 'partial') {
944 					if (el.domobj.nodeType === 3) {
945 						// partial text node selected, get the selected part
946 						text += el.domobj.data.substring(el.startOffset, el.endOffset);
947 					} else if (el.domobj.nodeType === 1 && el.children) {
948 						// partial element node selected, do the recursion into the children
949 						if (astext) {
950 951 							text += this.getFromSelectionTree(el.children, astext);
952 						} else {
953 							// when the html shall be fetched, we create a clone of
954 							// the element and remove all the children
955 							clone = jQuery(el.domobj.outerHTML).empty();
956 							// then we do the recursion and add the selection into the clone
957 							clone.html(this.getFromSelectionTree(el.children, astext));
958 							// finally we get the html of the clone
959 							text += clone.outerHTML();
960 						}
961 					}
962 				} else if (el.selection == 'full') {
963 					if (el.domobj.nodeType === 3) {
964 						// full text node selected, get the text
965 						text += jQuery(el.domobj).text();
966 					} else if (el.domobj.nodeType === 1 && el.children) {
967 						// full element node selected, get the html of the node and all children
968 						text += astext ? jQuery(el.domobj).text() : jQuery(el.domobj).outerHTML();
969 					}
970 				}
971 			}
972 
973 			return text;
974 		},
975 
976 		/**
977 		 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed)
978 		 * @return {?String}
979 		 */
980 		getSelectedMarkup: function () {
981 			var rangeObject = Aloha.Selection.rangeObject;
982 			return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false);
983 		},
984 
985 		/**
986 		 * Remove the currently selected markup
987 		 */
988 		removeSelectedMarkup: function () {
989 			var rangeObject = Aloha.Selection.rangeObject,
990 				newRange;
991 
992 			if (rangeObject.isCollapsed()) {
993 				return;
994 			}
995 
996 			newRange = new Aloha.Selection.SelectionRange();
997 			// remove the selection
998 			this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange);
999 
1000 			// do a cleanup now (starting with the commonancestorcontainer)
1001 			newRange.update();
1002 			GENTICS.Utils.Dom.doCleanup({
1003 				'merge': true,
1004 				'removeempty': true
1005 			}, Aloha.Selection.rangeObject);
1006 			Aloha.Selection.rangeObject = newRange;
1007 
1008 			// need to set the collapsed selection now
1009 			newRange.correctRange();
1010 			newRange.update();
1011 			newRange.select();
1012 			Aloha.Selection.updateSelection();
1013 		},
1014 
1015 		/**
1016 		 * Recursively remove the selected items, starting with the given level in the selectiontree
1017 		 * @param selectionTree current level of the selectiontree
1018 		 * @param newRange new collapsed range to be set after the removal
1019 		 */
1020 		removeFromSelectionTree: function (selectionTree, newRange) {
1021 			// remember the first found partially selected element node (in case we need
1022 			// to merge it with the last found partially selected element node)
1023 			var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength;
1024 
1025 			// iterate through the selection tree
1026 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
1027 				el = selectionTree[i];
1028 
1029 				// check the type of selection
1030 				if (el.selection == 'partial') {
1031 					if (el.domobj.nodeType === 3) {
1032 						// partial text node selected, so remove the selected portion
1033 						newdata = '';
1034 						if (el.startOffset > 0) {
1035 							newdata += el.domobj.data.substring(0, el.startOffset);
1036 						}
1037 						if (el.endOffset < el.domobj.data.length) {
1038 							newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length);
1039 						}
1040 						el.domobj.data = newdata;
1041 
1042 						// eventually set the new range (if not done before)
1043 						if (!newRange.startContainer) {
1044 							newRange.startContainer = newRange.endContainer = el.domobj;
1045 							newRange.startOffset = newRange.endOffset = el.startOffset;
1046 						}
1047 					} else if (el.domobj.nodeType === 1 && el.children) {
1048 						// partial element node selected, so do the recursion into the children
1049 						this.removeFromSelectionTree(el.children, newRange);
1050 
1051 						if (firstPartialElement) {
1052 							// when the first parially selected element is the same type
1053 							// of element, we need to merge them
1054 							if (firstPartialElement.nodeName == el.domobj.nodeName) {
1055 								// merge the nodes
1056 								jQuery(firstPartialElement).append(jQuery(el.domobj).contents());
1057 
1058 								// and remove the latter one
1059 								jQuery(el.domobj).remove();
1060 							}
1061 
1062 						} else {
1063 							// remember this element as first partially selected element
1064 							firstPartialElement = el.domobj;
1065 						}
1066 					}
1067 
1068 				} else if (el.selection == 'full') {
1069 					// eventually set the new range (if not done before)
1070 					if (!newRange.startContainer) {
1071 						adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
1072 							el.domobj.parentNode,
1073 							GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1,
1074 							false,
1075 							{
1076 								'blocklevel': false
1077 							}
1078 						);
1079 
1080 						if (adjacentTextNode) {
1081 							newRange.startContainer = newRange.endContainer = adjacentTextNode;
1082 							newRange.startOffset = newRange.endOffset = 0;
1083 						} else {
1084 							newRange.startContainer = newRange.endContainer = el.domobj.parentNode;
1085 							newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1;
1086 						}
1087 					}
1088 
1089 					// full node selected, so just remove it (will also remove all children)
1090 					jQuery(el.domobj).remove();
1091 				}
1092 			}
1093 		},
1094 
1095 		/**
1096 		 * split passed rangeObject without or with optional markup
1097 		 * @param Aloha.Selection.SelectionRange of the current selection
1098 		 * @param markup object (jQuery) to insert in between the split elements
1099 1100 		 * @return void
1101 		 */
1102 		splitRangeObject: function (rangeObject, markup) {
1103 			// UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split
1104 			// object which is split up
1105 			var splitObject = jQuery(rangeObject.splitObject),
1106 				selectionTree,
1107 			    insertAfterObject,
1108 			    followUpContainer;
1109 
1110 			// update the commonAncestor with the splitObject (so that the selectionTree is correct)
1111 			rangeObject.update(rangeObject.splitObject); // set the splitObject as new commonAncestorContainer and update the selectionTree
1112 
1113 			// calculate the selection tree. NOTE: it is necessary to do this before
1114 			// getting the followupcontainer, since getting the selection tree might
1115 			// possibly merge text nodes, which would lead to differences in the followupcontainer
1116 			selectionTree = rangeObject.getSelectionTree();
1117 
1118 			// object to be inserted after the splitObject
1119 			followUpContainer = this.getSplitFollowUpContainer(rangeObject);
1120 
1121 			// now split up the splitObject into itself AND the followUpContainer
1122 			this.splitRangeObjectHelper(selectionTree, rangeObject, followUpContainer); // split the current object into itself and the followUpContainer
1123 
1124 			// check whether the followupcontainer is still marked for removal
1125 			if (followUpContainer.hasClass('preparedForRemoval')) {
1126 				// TODO shall we just remove the class or shall we not use the followupcontainer?
1127 				followUpContainer.removeClass('preparedForRemoval');
1128 			}
1129 
1130 			// now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in
1131 			// some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list)
1132 			insertAfterObject = this.getInsertAfterObject(rangeObject, followUpContainer);
1133 
1134 			// now insert the followUpContainer
1135 			jQuery(followUpContainer).insertAfter(insertAfterObject); // attach the followUpContainer right after the insertAfterObject
1136 
1137 			// in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice)
1138 			if (rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator(rangeObject.splitObject, followUpContainer)) {
1139 				jQuery(rangeObject.splitObject).remove();
1140 			}
1141 
1142 			rangeObject.startContainer = null;
1143 			// first check whether the followUpContainer starts with a <br/>
1144 			// if so, place the cursor right before the <br/>
1145 			var followContents = followUpContainer.contents();
1146 			if (followContents.length > 0 && followContents.get(0).nodeType == 1 && followContents.get(0).nodeName.toLowerCase() === 'br') {
1147 				rangeObject.startContainer = followUpContainer.get(0);
1148 			}
1149 
1150 			if (!rangeObject.startContainer) {
1151 				// find a possible text node in the followUpContainer and set the selection to it
1152 				// if no textnode is available, set the selection to the followup container itself
1153 				rangeObject.startContainer = followUpContainer.textNodes(true, true).first().get(0);
1154 			}
1155 			if (!rangeObject.startContainer) { // if no text node was found, select the parent object of <br class="aloha-ephemera" />
1156 				rangeObject.startContainer = followUpContainer.textNodes(false).first().parent().get(0);
1157 			}
1158 			if (rangeObject.startContainer) {
1159 				// the cursor is always at the beginning of the followUp
1160 				rangeObject.endContainer = rangeObject.startContainer;
1161 				rangeObject.startOffset = 0;
1162 				rangeObject.endOffset = 0;
1163 			} else {
1164 				rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get(0);
1165 				rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(followUpContainer.get(0));
1166 			}
1167 
1168 			// finally update the range object again
1169 			rangeObject.update();
1170 
1171 			// now set the selection
1172 			rangeObject.select();
1173 		},
1174 
1175 		/**
1176 		 * method to get the object after which the followUpContainer can be inserted during splitup
1177 		 * this is a helper method, not needed anywhere else
1178 		 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
1179 		 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
1180 		 * @return object after which the followUpContainer can be inserted
1181 		 */
1182 		getInsertAfterObject: function (rangeObject, followUpContainer) {
1183 			var passedSplitObject, i, el;
1184 
1185 			for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) {
1186 				el = rangeObject.markupEffectiveAtStart[i];
1187 
1188 				// check if we have already passed the splitObject (some other markup might come before)
1189 				if (el === rangeObject.splitObject) {
1190 					passedSplitObject = true;
1191 				}
1192 
1193 				// if not passed splitObject, skip this markup
1194 				if (!passedSplitObject) {
1195 					continue;
1196 				}
1197 
1198 				// once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent
1199 				if (Aloha.Selection.canTag1WrapTag2(jQuery(el).parent()[0].nodeName, followUpContainer[0].nodeName)) {
1200 					return el;
1201 				}
1202 			}
1203 
1204 			return false;
1205 		},
1206 
1207 		/**
1208 		 * @fixme: Someone who knows what this function does, please refactor it.
1209 		 *			1. splitObject arg is not used at all
1210 		 *			2. Would be better to use ternary operation would be better than if else statement
1211 		 *
1212 		 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height
1213 		 * @param splitObject split object (dom object)
1214 		 * @return fillUpElement HTML Code
1215 		 */
1216 		getFillUpElement: function (splitObject) {
1217 			if (jQuery.browser.msie) {
1218 				return false;
1219 			}
1220 			return jQuery('<br class="aloha-cleanme"/>');
1221 		},
1222 
1223 		/**
1224 		 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags)
1225 		 * @param domArray array of domObjects
1226 		 * @return void
1227 		 */
1228 		removeElementContentWhitespaceObj: function (domArray) {
1229 			var correction = 0,
1230 				removeLater = [],
1231 				i,
1232 				el,
1233 			    removeIndex;
1234 
1235 			for (i = 0; i < domArray.length; ++i) {
1236 				el = domArray[i];
1237 				if (el.isElementContentWhitespace) {
1238 					removeLater[removeLater.length] = i;
1239 				}
1240 			}
1241 
1242 			for (i = 0; i < removeLater.length; ++i) {
1243 				removeIndex = removeLater[i];
1244 				domArray.splice(removeIndex - correction, 1);
1245 				++correction;
1246 			}
1247 		},
1248 
1249 		/**
1250 		 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other
1251 		 * @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
1252 		 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
1253 		 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
1254 		 * @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
1255 		 * @return void
1256 		 */
1257 		splitRangeObjectHelper: function (selectionTree, rangeObject, followUpContainer, inBetweenMarkup) {
1258 			if (!followUpContainer) {
1259 				Aloha.Log.warn(this, 'no followUpContainer, no inBetweenMarkup, nothing to do...');
1260 			}
1261 
1262 			var fillUpElement = this.getFillUpElement(rangeObject.splitObject),
1263 				splitObject = jQuery(rangeObject.splitObject),
1264 				startMoving = false,
1265 				el,
1266 				i,
1267 				completeText,
1268 				jqObj,
1269 				mirrorLevel,
1270 				parent,
1271 				treeLength;
1272 
1273 			if (selectionTree.length > 0) {
1274 1275 				mirrorLevel = followUpContainer.contents();
1276 
1277 				// if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes
1278 				if (mirrorLevel.length !== selectionTree.length) {
1279 					this.removeElementContentWhitespaceObj(mirrorLevel);
1280 				}
1281 
1282 				for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
1283 					el = selectionTree[i];
1284 
1285 					// remove all objects in the mirrorLevel, which are BEFORE the cursor
1286 					// OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended)
1287 					if ((el.selection === 'none' && startMoving === false) || (el.domobj && el.domobj.nodeType === 3 && el === selectionTree[(selectionTree.length - 1)] && el.startOffset === el.domobj.data.length)) {
1288 						// iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer
1289 						// however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead
1290 						// otherwise the followUpContainer is invalid and takes up no vertical space
1291 
1292 						if (followUpContainer.textNodes().length > 1 || (el.domobj.nodeType === 1 && el.children.length === 0)) {
1293 							// note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition,
1294 							// where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be
1295 							// removed correctly otherwise
1296 							mirrorLevel.eq(i).remove();
1297 
1298 						} else if (GENTICS.Utils.Dom.isSplitObject(followUpContainer[0])) {
1299 							if (fillUpElement) {
1300 								followUpContainer.html(fillUpElement); // for your zoological german knowhow: ephemera = Eintagsfliege
1301 							} else {
1302 								followUpContainer.empty();
1303 							}
1304 
1305 1306 						} else {
1307 							followUpContainer.empty();
1308 							followUpContainer.addClass('preparedForRemoval');
1309 						}
1310 
1311 						continue;
1312 
1313 					} else {
1314 						// split objects, which are AT the cursor Position or directly above
1315 						if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
1316 							// TODO better check for selection == 'partial' here?
1317 							if (el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined) {
1318 								completeText = el.domobj.data;
1319 								if (el.startOffset > 0) { // first check, if there will be some text left in the splitObject
1320 									el.domobj.data = completeText.substr(0, el.startOffset);
1321 								} 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
1322 									jQuery(el.domobj).remove();
1323 								} else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break
1324 									// if the parent is a blocklevel element, we insert the fillup element
1325 									parent = jQuery(el.domobj).parent();
1326 									if (GENTICS.Utils.Dom.isSplitObject(parent[0])) {
1327 										if (fillUpElement) {
1328 											parent.html(fillUpElement);
1329 										} else {
1330 											parent.empty();
1331 										}
1332 
1333 									} else {
1334 										// if the parent is no blocklevel element and would be empty now, we completely remove it
1335 										parent.remove();
1336 									}
1337 								}
1338 								if (completeText.length - el.startOffset > 0) {
1339 									// 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
1340 									mirrorLevel[i].data = completeText.substr(el.startOffset, completeText.length);
1341 								} else if (mirrorLevel.length > 1) {
1342 									// if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed
1343 									mirrorLevel.eq((i)).remove();
1344 								} else if (GENTICS.Utils.Dom.isBlockLevelElement(followUpContainer[0])) {
1345 									// if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break
1346 									if (fillUpElement) {
1347 										followUpContainer.html(fillUpElement);
1348 									} else {
1349 										followUpContainer.empty();
1350 									}
1351 
1352 								} else {
1353 									// if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal
1354 									followUpContainer.empty();
1355 									followUpContainer.addClass('preparedForRemoval');
1356 								}
1357 							}
1358 
1359 							startMoving = true;
1360 
1361 							if (el.children.length > 0) {
1362 								this.splitRangeObjectHelper(el.children, rangeObject, mirrorLevel.eq(i), inBetweenMarkup);
1363 							}
1364 
1365 						} else {
1366 							// remove all objects in the origin, which are AFTER the cursor
1367 							if (el.selection === 'none' && startMoving === true) {
1368 								// iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer
1369 								jqObj = jQuery(el.domobj).remove();
1370 							}
1371 						}
1372 					}
1373 				}
1374 			} else {
1375 				Aloha.Log.error(this, 'can not split splitObject due to an empty selection tree');
1376 			}
1377 
1378 			// and finally cleanup: remove all fillUps > 1
1379 			splitObject.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
1380 			followUpContainer.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
1381 
1382 			// remove objects prepared for removal
1383 			splitObject.find('.preparedForRemoval').remove();
1384 			followUpContainer.find('.preparedForRemoval').remove();
1385 
1386 			// if splitObject / followUp are empty, place a fillUp inside
1387 			if (splitObject.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(splitObject[0]) && fillUpElement) {
1388 				splitObject.html(fillUpElement);
1389 			}
1390 
1391 			if (followUpContainer.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(followUpContainer[0]) && fillUpElement) {
1392 				followUpContainer.html(fillUpElement);
1393 			}
1394 		},
1395 
1396 		/**
1397 		 * returns a jQuery object fitting the passed splitObject as follow up object
1398 		 * examples,
1399 		 * - when passed a p it will return an empty p (clone of the passed p)
1400 		 * - 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)
1401 		 * @param rangeObject Aloha.RangeObject
1402 		 * @return void
1403 		 */
1404 		getSplitFollowUpContainer: function (rangeObject) {
1405 			var tagName = rangeObject.splitObject.nodeName.toLowerCase(),
1406 				returnObj,
1407 				inside,
1408 				lastObj;
1409 
1410 			switch (tagName) {
1411 			case 'h1':
1412 			case 'h2':
1413 			case 'h3':
1414 			case 'h4':
1415 			case 'h5':
1416 			case 'h6':
1417 				// get the last textnode in the splitobject, but don't consider aloha-cleanme elements
1418 				lastObj = jQuery(rangeObject.splitObject).textNodes(':not(.aloha-cleanme)').last()[0];
1419 				// special case: when enter is hit at the end of a heading, the followUp should be a <p>
1420 				if (lastObj && rangeObject.startContainer === lastObj && rangeObject.startOffset === lastObj.length) {
1421 					returnObj = jQuery('<p></p>');
1422 					inside = jQuery(rangeObject.splitObject.outerHTML).contents();
1423 					returnObj.append(inside);
1424 					return returnObj;
1425 				}
1426 				break;
1427 
1428 			case 'li':
1429 				// TODO check whether the li is the last one
1430 				// special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list)
1431 				if (rangeObject.startContainer.nodeName.toLowerCase() === 'br' && jQuery(rangeObject.startContainer).hasClass('aloha-ephemera')) {
1432 					returnObj = jQuery('<p></p>');
1433 					inside = jQuery(rangeObject.splitObject.outerHTML).contents();
1434 					returnObj.append(inside);
1435 					return returnObj;
1436 1437 				}
1438 				// when the li is the last one and empty, we also just return a <p>
1439 				if (!rangeObject.splitObject.nextSibling && jQuery.trim(jQuery(rangeObject.splitObject).text()).length === 0) {
1440 					returnObj = jQuery('<p></p>');
1441 					return returnObj;
1442 				}
1443 				break;
1444 			}
1445 
1446 			return jQuery(rangeObject.splitObject.outerHTML);
1447 		},
1448 
1449 		/**
1450 		 * Transform the given domobj into an object with the given new nodeName.
1451 		 * Preserves the content and all attributes. If a range object is given, also the range will be preserved
1452 		 * @param domobj dom object to transform
1453 		 * @param nodeName new node name
1454 		 * @param range range object
1455 		 * @api
1456 		 * @return new object as jQuery object
1457 		 */
1458 		transformDomObject: function (domobj, nodeName, range) {
1459 			// first create the new element
1460 			var jqOldObj = jQuery(domobj),
1461 				jqNewObj = jQuery('<' + nodeName + '>'),
1462 				i,
1463 				attributes = jqOldObj[0].cloneNode(false).attributes;
1464 
1465 			// TODO what about events?
1466 			// copy attributes
1467 			if (attributes) {
1468 				for (i = 0; i < attributes.length; ++i) {
1469 					if (typeof attributes[i].specified === 'undefined' || attributes[i].specified) {
1470 						jqNewObj.attr(attributes[i].nodeName, attributes[i].nodeValue);
1471 					}
1472 				}
1473 			}
1474 
1475 			// copy inline CSS
1476 			if (jqOldObj[0].style && jqOldObj[0].style.cssText) {
1477 				jqNewObj[0].style.cssText = jqOldObj[0].style.cssText;
1478 			}
1479 
1480 			// now move the contents of the old dom object into the new dom object
1481 			jqOldObj.contents().appendTo(jqNewObj);
1482 
1483 			// finally replace the old object with the new one
1484 			jqOldObj.replaceWith(jqNewObj);
1485 
1486 			// preserve the range
1487 			if (range) {
1488 				if (range.startContainer == domobj) {
1489 					range.startContainer = jqNewObj.get(0);
1490 				}
1491 
1492 				if (range.endContainer == domobj) {
1493 					range.endContainer = jqNewObj.get(0);
1494 				}
1495 			}
1496 
1497 			return jqNewObj;
1498 		},
1499 
1500 		/**
1501 		 * String representation
1502 		 * @return {String}
1503 		 */
1504 		toString: function () {
1505 			return 'Aloha.Markup';
1506 		}
1507 
1508 	});
1509 
1510 	Aloha.Markup = new Aloha.Markup();
1511 	return Aloha.Markup;
1512 });
1513