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