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 = !!(Aloha.browser.msie && 9 > parseInt(Aloha.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 160 			return true;
161 		}
162 
163 		var isText = isTextNode(node);
164 
165 		// If within a text node, then ignore superfluous white-spaces,
166 		// since they are invisible to the user.
167 		if (isText && node.data.replace(/\s+$/, '').length === offset) {
168 			return true;
169 		}
170 
171 		if (1 === length && !isText) {
172 			return isBR(node.childNodes[0]);
173 		}
174 
175 		return false;
176 	}
177 
178 	function blink(node) {
179 		jQuery(node).stop(true).css({
180 			opacity: 0
181 		}).fadeIn(0).delay(100).fadeIn(function () {
182 			jQuery(node).css({
183 				opacity: 1
184 			});
185 		});
186 
187 		return node;
188 	}
189 
190 	function nodeContains(node1, node2) {
191 		return isOldIE ? (shims.compareDocumentPosition(node1, node2) & 16) : 0 < jQuery(node1).find(node2).length;
192 	}
193 
194 	function isInsidePlaceholder(range) {
195 		var start = range.startContainer;
196 		var end = range.endContainer;
197 		var $placeholder = window.$_alohaPlaceholder;
198 
199 		return $placeholder.is(start) || $placeholder.is(end) || nodeContains($placeholder[0], start) || nodeContains($placeholder[0], end);
200 	}
201 
202 	function cleanupPlaceholders(range) {
203 		if (window.$_alohaPlaceholder && !isInsidePlaceholder(range)) {
204 			if (0 === window.$_alohaPlaceholder.html().replace(/^( )*$/, '').length) {
205 				window.$_alohaPlaceholder.remove();
206 			}
207 
208 			window.$_alohaPlaceholder = null;
209 		}
210 	}
211 
212 	/**
213 	 * @TODO(petro): We need to be more intelligent about whether we insert a
214 	 *               block-level placeholder or a phrasing level element.
215 	 * @TODO(petro): test with <pre>
216 	 * @TODO: move to block-jump.js
217 	 */
218 	function jumpBlock(block, isGoingLeft, currentRange) {
219 		var range = new GENTICS.Utils.RangeObject();
220 		var sibling = isGoingLeft ? prevVisibleNode(block) : nextVisibleNode(block);
221 
222 		if (!sibling || isBlock(sibling)) {
223 			var $landing = jQuery('<div> </div>');
224 
225 			if (isGoingLeft) {
226 				jQuery(block).before($landing);
227 			} else {
228 				jQuery(block).after($landing);
229 			}
230 
231 			range.startContainer = range.endContainer = $landing[0];
232 			range.startOffset = range.endOffset = 0;
233 
234 			// Clear out any old placeholder first ...
235 			cleanupPlaceholders(range);
236 
237 			window.$_alohaPlaceholder = $landing;
238 		} else {
239 
240 			// Don't jump the block yet if the cursor is moving to the
241 			// beginning or end of a text node, or if it is about to leave
242 			// an element node. Both these cases require a hack in some
243 			// browsers.
244 			var moveToBoundaryPositionInIE = ( // To the beginning or end of a text node?
245 				(currentRange.startContainer.nodeType === 3
246 				 && currentRange.startContainer === currentRange.endContainer
247 				 && currentRange.startContainer.nodeValue !== ""
248 				 && (isGoingLeft ? currentRange.startOffset === 1 : currentRange.endOffset + 1 === currentRange.endContainer.length))
249 				// Leaving an element node?
250 					|| (currentRange.startContainer.nodeType === 1
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 			}
525 			return true;
526 		},
527 
528 		/**
529 		 * processing up and down cursor keys inside tables
530 		 * will only try to figure out if cursor is at first
531 		 * or last position in table and exit to the next
532 		 * editable node from there
533 		 *
534 		 * won't do anything if range is not collapsed
535 		 * 
536 		 * @param {RangyRange} range A range object for the current selection.
537 		 * @param {number} keyCode Code of the currently pressed key.
538 		 * @return {boolean} true if something was done, false if browser should 
539 		 * continue handling the event
540 		 */
541 		processCursorUpDown: function (range, keyCode) {
542 			if (!range.collapsed) {
543 				return false;
544 			}
545 
546 			var node = range.startContainer,
547 				tableWrapper,
548 				cursorNode;
549 550 
			// UP
551 			if (keyCode === 38 &&
552 					isFrontPosition(node, range.startOffset) &&
553 					isChildOf(node, ['TD', 'TH']) &&
554 					isFirstNode(node, 'TABLE')) {
555 
556 				// we want to position the cursor now in the first 
557 				// element before the table, so we need to find the
558 				// table wrapper first ...
559 				tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0);
560 561 				if (!tableWrapper) {
562 					return false;
563 				}
564 
565 				// ... and then find it's previousSibling
566 				// which we will descend down to its deepest
567 				// nested child node, where we will put the
568 				// cursor
569 				// prefer previousElemntSibling because Firefox will land you in a
570 				// whitespace text node between a preceding <p> and the table otherwise
571 				if (tableWrapper.previousElementSibling) {
572 					cursorNode = tableWrapper.previousElementSibling;
573 				} else {
574 					cursorNode = tableWrapper.previousSibling;
575 				}
576 				while (cursorNode.nodeType !== 3) {
577 					cursorNode = cursorNode.lastChild;
578 					if (cursorNode === null) {
579 						// stop if there is no element to be entered before the table
580 						return false;
581 					}
582 				}
583 
584 				Aloha.Selection.rangeObject.startContainer = cursorNode;
585 				Aloha.Selection.rangeObject.endContainer = cursorNode;
586 				Aloha.Selection.rangeObject.startOffset = cursorNode.length;
587 				Aloha.Selection.rangeObject.endOffset = cursorNode.length;
588 				Aloha.Selection.rangeObject.select();
589 
590 				// Mozilla needs this fix or else the selection will not work
591 				if (Aloha.activeEditable && jQuery.browser.mozilla) {
592 					Aloha.activeEditable.obj.focus();
593 				}
594 
595 				return true;
596 
597 			// DOWN
598 			} else if (keyCode === 40 &&
599 					isEndPosition(node, range.startOffset) &&
600 					isChildOf(node, ['TD', 'TH']) &&
601 					isLastNode(node, 'TABLE')) {
602 
603 				// we want to put the cursor in the first element right 
604 				// after the table so we need to find the table wrapper first
605 				tableWrapper = jQuery(node).parents('div.aloha-table-wrapper').get(0);
606 				if (!tableWrapper) {
607 					return false;
608 				}
609 
610 				// and now find its following sibling where we will put
611 				// the cursor in the first position
612 				// the next elementSibling is preffered over the nextSibling
613 				// because Mozilla will sometimes have an empty text node
614 				// right next to the table - but we most likely want to put 
615 				// the cursor into the next paragraph
616 				if (tableWrapper.nextElementSibling) {
617 					cursorNode = tableWrapper.nextElementSibling;
618 				} else {
619 					cursorNode = tableWrapper.nextSibling;
620 				}
621 
622 				while (cursorNode.nodeType !== 3) {
623 					cursorNode = cursorNode.firstChild;
624 					if (cursorNode === null) {
625 						return false;
626 					}
627 				}
628 
629 				Aloha.Selection.rangeObject.startContainer = cursorNode;
630 				Aloha.Selection.rangeObject.endContainer = cursorNode;
631 				Aloha.Selection.rangeObject.startOffset = 0;
632 				Aloha.Selection.rangeObject.endOffset = 0;
633 				Aloha.Selection.rangeObject.select();
634 
635 				// Mozilla needs this fix or else the selection will not work
636 				if (Aloha.activeEditable && jQuery.browser.mozilla) {
637 					Aloha.activeEditable.obj.focus();
638 				}
639 
640 				return true;
641 
642 			} else {
643 				return false;
644 			}
645 		},
646 
647 		/**
648 		 * Processing of cursor keys.
649 		 * Detect blocks (elements with contenteditable=false) and will select them
650 		 * (normally the cursor would simply jump right past them).
651 		 *
652 		 * For each block that is selected, an 'aloha-block-selected' event will be
653 		 * triggered.
654 		 *
655 		 * TODO: the above is what should happen. Currently we just skip past blocks.
656 		 *
657 		 * @param {RangyRange} range A range object for the current selection.
658 		 * @param {number} keyCode Code of the currently pressed key.
659 		 * @return {boolean} False if a block was found, to prevent further events,
660 		 *                   true otherwise.
661 		 * @TODO move to block-jump.js
662 		 */
663 		processCursor: function (range, keyCode) {
664 			if (!range.collapsed) {
665 				return true;
666 			}
667 
668 			BlockJump.removeZeroWidthTextNodeFix();
669 
670 			var node = range.startContainer,
671 				selection = Aloha.getSelection();
672 
673 			if (!node) {
674 				return true;
675 			}
676 
677 			var sibling, offset;
678 
679 			// special handling for moving Cursor around zero-width whitespace in IE7
680 			if (Aloha.browser.msie && parseInt(Aloha.browser.version, 10) <= 7 && isTextNode(node)) {
681 				if (keyCode == 37) {
682 					// moving left -> skip zwsp to the left
683 					offset = range.startOffset;
684 					while (offset > 0 && node.data.charAt(offset - 1) === '\u200b') {
685 						offset--;
686 					}
687 					if (offset != range.startOffset) {
688 						range.setStart(range.startContainer, offset);
689 						range.setEnd(range.startContainer, offset);
690 						selection = Aloha.getSelection();
691 						selection.removeAllRanges();
692 						selection.addRange(range);
693 					}
694 				} else if (keyCode == 39) {
695 					// moving right -> skip zwsp to the right
696 					offset = range.startOffset;
697 					while (offset < node.data.length && node.data.charAt(offset) === '\u200b') {
698 						offset++;
699 					}
700 					if (offset != range.startOffset) {
701 						range.setStart(range.startContainer, offset);
702 						range.setEnd(range.startContainer, offset);
703 						selection.removeAllRanges();
704 						selection.addRange(range);
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 ( Aloha.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 		 * @param html html markup to be inserted
789 		 */
790 		insertHTMLCode: function (html) {
791 			var rangeObject = Aloha.Selection.rangeObject;
792 			this.insertHTMLBreak(rangeObject.getSelectionTree(), rangeObject, jQuery(html));
793 		},
794 
795 		/**
796 		 * insert an HTML Break <br /> into current selection
797 		 * @param Aloha.Selection.SelectionRange of the current selection
798 		 * @return void
799 		 */
800 		insertHTMLBreak: function (selectionTree, rangeObject, inBetweenMarkup) {
801 			var i,
802 			    treeLength,
803 			    el,
804 			    jqEl,
805 			    jqElBefore,
806 			    jqElAfter,
807 			    tmpObject,
808 			    offset,
809 			    checkObj;
810 
811 			inBetweenMarkup = inBetweenMarkup || jQuery('<br/>');
812 
813 			for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
814 				el = selectionTree[i];
815 				jqEl = el.domobj ? jQuery(el.domobj) : undefined;
816 
817 				if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
818 					if (el.selection == 'collapsed') {
819 						// collapsed selection found (between nodes)
820 						if (i > 0) {
821 							// not at the start, so get the element to the left
822 							jqElBefore = jQuery(selectionTree[i - 1].domobj);
823 
824 							// and insert the break after it
825 							jqElBefore.after(inBetweenMarkup);
826 
827 						} else {
828 							// at the start, so get the element to the right
829 							jqElAfter = jQuery(selectionTree[1].domobj);
830 
831 							// and insert the break before it
832 							jqElAfter.before(inBetweenMarkup);
833 						}
834 
835 						// now set the range
836 						rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
837 						rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(inBetweenMarkup[0]) + 1;
838 						rangeObject.correctRange();
839 
840 					} else if (el.domobj && el.domobj.nodeType === 3) { // textNode
841 						// when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
842 						if (el.domobj.nextSibling && el.domobj.nextSibling.nodeType == 1 && Aloha.Selection.replacingElements[el.domobj.nextSibling.nodeName.toLowerCase()]) {
843 							// TODO check whether this depends on the browser
844 							jqEl.after('<br/>');
845 						}
846 
847 						if (this.needEndingBreak()) {
848 							// when the textnode is the last inside a blocklevel element
849 							// (like p, h1, ...) we need to add an additional br as very
850 							// last object in the blocklevel element
851 							checkObj = el.domobj;
852 
853 							while (checkObj) {
854 								if (checkObj.nextSibling) {
855 									checkObj = false;
856 								} else {
857 									// go to the parent
858 									checkObj = checkObj.parentNode;
859 
860 									// found a blocklevel or list element, we are done
861 									if (GENTICS.Utils.Dom.isBlockLevelElement(checkObj) || GENTICS.Utils.Dom.isListElement(checkObj)) {
862 										break;
863 									}
864 
865 									// reached the limit object, we are done
866 									if (checkObj === rangeObject.limitObject) {
867 										checkObj = false;
868 									}
869 								}
870 							}
871 
872 							// when we found a blocklevel element, insert a break at the
873 							// end. Mark the break so that it is cleaned when the
874 							// content is fetched.
875 							if (checkObj) {
876 								jQuery(checkObj).append('<br class="aloha-cleanme" />');
877 							}
878 						}
879 
880 						// insert the break
881 						jqEl.between(inBetweenMarkup, el.startOffset);
882 883 
						// correct the range
884 						// count the number of previous siblings
885 						offset = 0;
886 						tmpObject = inBetweenMarkup[0];
887 						while (tmpObject) {
888 							tmpObject = tmpObject.previousSibling;
889 							++offset;
890 						}
891 
892 						rangeObject.startContainer = inBetweenMarkup[0].parentNode;
893 						rangeObject.endContainer = inBetweenMarkup[0].parentNode;
894 						rangeObject.startOffset = offset;
895 						rangeObject.endOffset = offset;
896 						rangeObject.correctRange();
897 
898 					} else if (el.domobj && el.domobj.nodeType === 1) { // other node, normally a break
899 						if (jqEl.parent().find('br.aloha-ephemera').length === 0) {
900 							// but before putting it, remove all:
901 							jQuery(rangeObject.limitObject).find('br.aloha-ephemera').remove();
902 
903 							//  now put it:
904 							jQuery(rangeObject.commonAncestorContainer).append(this.getFillUpElement(rangeObject.splitObject));
905 						}
906 
907 						jqEl.after(inBetweenMarkup);
908 
909 						// now set the selection. Since we just added one break do the currect el
910 						// the new position must be el's position + 1. el's position is the index
911 						// of the el in the selection tree, which is i. then we must add
912 						// another +1 because we want to be AFTER the object, not before. therefor +2
913 						rangeObject.startContainer = rangeObject.commonAncestorContainer;
914 						rangeObject.endContainer = rangeObject.startContainer;
915 						rangeObject.startOffset = i + 2;
916 						rangeObject.endOffset = i + 2;
917 						rangeObject.update();
918 					}
919 				}
920 			}
921 			rangeObject.select();
922 		},
923 
924 		/**
925 		 * Check whether blocklevel elements need breaks at the end to visibly render a newline
926 		 * @return true if an ending break is necessary, false if not
927 		 */
928 		needEndingBreak: function () {
929 			// currently, all browser except IE need ending breaks
930 			return !Aloha.browser.msie;
931 		},
932 
933 		/**
934 		 * Get the currently selected text or false if nothing is selected (or the selection is collapsed)
935 		 * @return selected text
936 		 */
937 		getSelectedText: function () {
938 			var rangeObject = Aloha.Selection.rangeObject;
939 
940 			if (rangeObject.isCollapsed()) {
941 				return false;
942 			}
943 
944 			return this.getFromSelectionTree(rangeObject.getSelectionTree(), true);
945 		},
946 
947 		/**
948 		 * Recursive function to get the selected text from the selection tree starting at the given level
949 		 * @param selectionTree array of selectiontree elements
950 		 * @param astext true when the contents shall be fetched as text, false for getting as html markup
951 		 * @return selected text from that level (incluiding all sublevels)
952 		 */
953 		getFromSelectionTree: function (selectionTree, astext) {
954 			var text = '', i, treeLength, el, clone;
955 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
956 				el = selectionTree[i];
957 				if (el.selection == 'partial') {
958 					if (el.domobj.nodeType === 3) {
959 						// partial text node selected, get the selected part
960 						text += el.domobj.data.substring(el.startOffset, el.endOffset);
961 					} else if (el.domobj.nodeType === 1 && el.children) {
962 						// partial element node selected, do the recursion into the children
963 						if (astext) {
964 							text += this.getFromSelectionTree(el.children, astext);
965 						} else {
966 							// when the html shall be fetched, we create a clone of
967 							// the element and remove all the children
968 							clone = jQuery(el.domobj.outerHTML).empty();
969 							// then we do the recursion and add the selection into the clone
970 							clone.html(this.getFromSelectionTree(el.children, astext));
971 							// finally we get the html of the clone
972 							text += clone.outerHTML();
973 						}
974 					}
975 				} else if (el.selection == 'full') {
976 					if (el.domobj.nodeType === 3) {
977 						// full text node selected, get the text
978 						text += jQuery(el.domobj).text();
979 					} else if (el.domobj.nodeType === 1 && el.children) {
980 						// full element node selected, get the html of the node and all children
981 						text += astext ? jQuery(el.domobj).text() : jQuery(el.domobj).outerHTML();
982 					}
983 				}
984 			}
985 
986 			return text;
987 		},
988 
989 		/**
990 		 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed)
991 		 * @return {?String}
992 		 */
993 		getSelectedMarkup: function () {
994 			var rangeObject = Aloha.Selection.rangeObject;
995 			return rangeObject.isCollapsed() ? null : this.getFromSelectionTree(rangeObject.getSelectionTree(), false);
996 		},
997 
998 		/**
999 		 * Remove the currently selected markup
1000 		 */
1001 		removeSelectedMarkup: function () {
1002 			var rangeObject = Aloha.Selection.rangeObject,
1003 				newRange;
1004 
1005 			if (rangeObject.isCollapsed()) {
1006 				return;
1007 			}
1008 
1009 			newRange = new Aloha.Selection.SelectionRange();
1010 			// remove the selection
1011 			this.removeFromSelectionTree(rangeObject.getSelectionTree(), newRange);
1012 
1013 			// do a cleanup now (starting with the commonancestorcontainer)
1014 			newRange.update();
1015 			GENTICS.Utils.Dom.doCleanup({
1016 				'merge': true,
1017 				'removeempty': true
1018 			}, Aloha.Selection.rangeObject);
1019 			Aloha.Selection.rangeObject = newRange;
1020 
1021 			// need to set the collapsed selection now
1022 			newRange.correctRange();
1023 			newRange.update();
1024 			newRange.select();
1025 			Aloha.Selection.updateSelection();
1026 		},
1027 
1028 		/**
1029 		 * Recursively remove the selected items, starting with the given level in the selectiontree
1030 		 * @param selectionTree current level of the selectiontree
1031 		 * @param newRange new collapsed range to be set after the removal
1032 		 */
1033 		removeFromSelectionTree: function (selectionTree, newRange) {
1034 			// remember the first found partially selected element node (in case we need
1035 			// to merge it with the last found partially selected element node)
1036 			var firstPartialElement, newdata, i, el, adjacentTextNode, treeLength;
1037 
1038 			// iterate through the selection tree
1039 			for (i = 0, treeLength = selectionTree.length; i < treeLength; i++) {
1040 				el = selectionTree[i];
1041 
1042 				// check the type of selection
1043 				if (el.selection == 'partial') {
1044 					if (el.domobj.nodeType === 3) {
1045 						// partial text node selected, so remove the selected portion
1046 						newdata = '';
1047 						if (el.startOffset > 0) {
1048 							newdata += el.domobj.data.substring(0, el.startOffset);
1049 						}
1050 						if (el.endOffset < el.domobj.data.length) {
1051 							newdata += el.domobj.data.substring(el.endOffset, el.domobj.data.length);
1052 						}
1053 						el.domobj.data = newdata;
1054 
1055 						// eventually set the new range (if not done before)
1056 						if (!newRange.startContainer) {
1057 							newRange.startContainer = newRange.endContainer = el.domobj;
1058 							newRange.startOffset = newRange.endOffset = el.startOffset;
1059 						}
1060 					} else if (el.domobj.nodeType === 1 && el.children) {
1061 						// partial element node selected, so do the recursion into the children
1062 						this.removeFromSelectionTree(el.children, newRange);
1063 
1064 						if (firstPartialElement) {
1065 							// when the first parially selected element is the same type
1066 							// of element, we need to merge them
1067 							if (firstPartialElement.nodeName == el.domobj.nodeName) {
1068 								// merge the nodes
1069 								jQuery(firstPartialElement).append(jQuery(el.domobj).contents());
1070 
1071 								// and remove the latter one
1072 								jQuery(el.domobj).remove();
1073 							}
1074 
1075 						} else {
1076 							// remember this element as first partially selected element
1077 							firstPartialElement = el.domobj;
1078 						}
1079 					}
1080 
1081 				} else if (el.selection == 'full') {
1082 					// eventually set the new range (if not done before)
1083 					if (!newRange.startContainer) {
1084 						adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
1085 							el.domobj.parentNode,
1086 							GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1,
1087 							false,
1088 							{
1089 								'blocklevel': false
1090 							}
1091 						);
1092 
1093 						if (adjacentTextNode) {
1094 							newRange.startContainer = newRange.endContainer = adjacentTextNode;
1095 							newRange.startOffset = newRange.endOffset = 0;
1096 						} else {
1097 							newRange.startContainer = newRange.endContainer = el.domobj.parentNode;
1098 							newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(el.domobj) + 1;
1099 						}
1100 					}
1101 
1102 					// full node selected, so just remove it (will also remove all children)
1103 					jQuery(el.domobj).remove();
1104 				}
1105 			}
1106 		},
1107 
1108 		/**
1109 		 * split passed rangeObject without or with optional markup
1110 		 * @param Aloha.Selection.SelectionRange of the current selection
1111 		 * @param markup object (jQuery) to insert in between the split elements
1112 		 * @return void
1113 		 */
1114 		splitRangeObject: function (rangeObject, markup) {
1115 			// UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split
1116 			// object which is split up
1117 			var splitObject = jQuery(rangeObject.splitObject),
1118 				selectionTree,
1119 			    insertAfterObject,
1120 			    followUpContainer;
1121 
1122 			// update the commonAncestor with the splitObject (so that the selectionTree is correct)
1123 			rangeObject.update(rangeObject.splitObject); // set the splitObject as new commonAncestorContainer and update the selectionTree
1124 
1125 			// calculate the selection tree. NOTE: it is necessary to do this before
1126 			// getting the followupcontainer, since getting the selection tree might
1127 			// possibly merge text nodes, which would lead to differences in the followupcontainer
1128 			selectionTree = rangeObject.getSelectionTree();
1129 
1130 			// object to be inserted after the splitObject
1131 			followUpContainer = this.getSplitFollowUpContainer(rangeObject);
1132 
1133 			// now split up the splitObject into itself AND the followUpContainer
1134 			this.splitRangeObjectHelper(selectionTree, rangeObject, followUpContainer); // split the current object into itself and the followUpContainer
1135 
1136 			// check whether the followupcontainer is still marked for removal
1137 			if (followUpContainer.hasClass('preparedForRemoval')) {
1138 				// TODO shall we just remove the class or shall we not use the followupcontainer?
1139 				followUpContainer.removeClass('preparedForRemoval');
1140 			}
1141 
1142 			// now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in
1143 			// some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list)
1144 			insertAfterObject = this.getInsertAfterObject(rangeObject, followUpContainer);
1145 
1146 			// now insert the followUpContainer
1147 			jQuery(followUpContainer).insertAfter(insertAfterObject); // attach the followUpContainer right after the insertAfterObject
1148 
1149 			// in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice)
1150 			if (rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator(rangeObject.splitObject, followUpContainer)) {
1151 				jQuery(rangeObject.splitObject).remove();
1152 			}
1153 
1154 			rangeObject.startContainer = null;
1155 			// first check whether the followUpContainer starts with a <br/>
1156 			// if so, place the cursor right before the <br/>
1157 			var followContents = followUpContainer.contents();
1158 			if (followContents.length > 0 && followContents.get(0).nodeType == 1 && followContents.get(0).nodeName.toLowerCase() === 'br') {
1159 				rangeObject.startContainer = followUpContainer.get(0);
1160 			}
1161 
1162 			if (!rangeObject.startContainer) {
1163 				// find a possible text node in the followUpContainer and set the selection to it
1164 				// if no textnode is available, set the selection to the followup container itself
1165 				rangeObject.startContainer = followUpContainer.textNodes(true, true).first().get(0);
1166 			}
1167 			if (!rangeObject.startContainer) { // if no text node was found, select the parent object of <br class="aloha-ephemera" />
1168 				rangeObject.startContainer = followUpContainer.textNodes(false).first().parent().get(0);
1169 			}
1170 			if (rangeObject.startContainer) {
1171 				// the cursor is always at the beginning of the followUp
1172 				rangeObject.endContainer = rangeObject.startContainer;
1173 				rangeObject.startOffset = 0;
1174 				rangeObject.endOffset = 0;
1175 			} else {
1176 				rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get(0);
1177 				rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent(followUpContainer.get(0));
1178 			}
1179 
1180 			// finally update the range object again
1181 			rangeObject.update();
1182 
1183 			// now set the selection
1184 			rangeObject.select();
1185 		},
1186 
1187 		/**
1188 		 * method to get the object after which the followUpContainer can be inserted during splitup
1189 		 * this is a helper method, not needed anywhere else
1190 		 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
1191 		 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
1192 		 * @return object after which the followUpContainer can be inserted
1193 		 */
1194 		getInsertAfterObject: function (rangeObject, followUpContainer) {
1195 			var passedSplitObject, i, el;
1196 
1197 			for (i = 0; i < rangeObject.markupEffectiveAtStart.length; i++) {
1198 				el = rangeObject.markupEffectiveAtStart[i];
1199 
1200 				// check if we have already passed the splitObject (some other markup might come before)
1201 				if (el === rangeObject.splitObject) {
1202 					passedSplitObject = true;
1203 				}
1204 
1205 				// if not passed splitObject, skip this markup
1206 				if (!passedSplitObject) {
1207 					continue;
1208 				}
1209 
1210 				// once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent
1211 				if (Aloha.Selection.canTag1WrapTag2(jQuery(el).parent()[0].nodeName, followUpContainer[0].nodeName)) {
1212 					return el;
1213 				}
1214 			}
1215 
1216 			return false;
1217 		},
1218 
1219 		/**
1220 		 * @fixme: Someone who knows what this function does, please refactor it.
1221 		 *			1. splitObject arg is not used at all
1222 		 *			2. Would be better to use ternary operation would be better than if else statement
1223 		 *
1224 		 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height
1225 		 * @param splitObject split object (dom object)
1226 		 * @return fillUpElement HTML Code
1227 		 */
1228 		getFillUpElement: function (splitObject) {
1229 			if (Aloha.browser.msie) {
1230 				return false;
1231 			}
1232 			return jQuery('<br class="aloha-cleanme"/>');
1233 		},
1234 
1235 		/**
1236 		 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags)
1237 		 * @param domArray array of domObjects
1238 		 * @return void
1239 		 */
1240 		removeElementContentWhitespaceObj: function (domArray) {
1241 			var correction = 0,
1242 				removeLater = [],
1243 				i,
1244 				el,
1245 			    removeIndex;
1246 
1247 			for (i = 0; i < domArray.length; ++i) {
1248 				el = domArray[i];
1249 				if (el.isElementContentWhitespace) {
1250 					removeLater[removeLater.length] = i;
1251 				}
1252 			}
1253 
1254 			for (i = 0; i < removeLater.length; ++i) {
1255 				removeIndex = removeLater[i];
1256 				domArray.splice(removeIndex - correction, 1);
1257 				++correction;
1258 			}
1259 		},
1260 
1261 		/**
1262 		 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other
1263 		 * @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
1264 		 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
1265 		 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
1266 		 * @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
1267 		 * @return void
1268 		 */
1269 		splitRangeObjectHelper: function (selectionTree, rangeObject, followUpContainer, inBetweenMarkup) {
1270 			if (!followUpContainer) {
1271 				Aloha.Log.warn(this, 'no followUpContainer, no inBetweenMarkup, nothing to do...');
1272 			}
1273 
1274 			var fillUpElement = this.getFillUpElement(rangeObject.splitObject),
1275 				splitObject = jQuery(rangeObject.splitObject),
1276 				startMoving = false,
1277 				el,
1278 				i,
1279 				completeText,
1280 				jqObj,
1281 				mirrorLevel,
1282 				parent,
1283 				treeLength;
1284 
1285 			if (selectionTree.length > 0) {
1286 				mirrorLevel = followUpContainer.contents();
1287 
1288 				// if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes
1289 				if (mirrorLevel.length !== selectionTree.length) {
1290 					this.removeElementContentWhitespaceObj(mirrorLevel);
1291 				}
1292 
1293 				for (i = 0, treeLength = selectionTree.length; i < treeLength; ++i) {
1294 					el = selectionTree[i];
1295 
1296 					// remove all objects in the mirrorLevel, which are BEFORE the cursor
1297 					// OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended)
1298 					if ((el.selection === 'none' && startMoving === false) || (el.domobj && el.domobj.nodeType === 3 && el === selectionTree[(selectionTree.length - 1)] && el.startOffset === el.domobj.data.length)) {
1299 						// iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer
1300 						// however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead
1301 						// otherwise the followUpContainer is invalid and takes up no vertical space
1302 
1303 						if (followUpContainer.textNodes().length > 1 || (el.domobj.nodeType === 1 && el.children.length === 0)) {
1304 							// note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition,
1305 							// where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be
1306 							// removed correctly otherwise
1307 							mirrorLevel.eq(i).remove();
1308 
1309 						} else if (GENTICS.Utils.Dom.isSplitObject(followUpContainer[0])) {
1310 							if (fillUpElement) {
1311 								followUpContainer.html(fillUpElement); // for your zoological german knowhow: ephemera = Eintagsfliege
1312 							} else {
1313 								followUpContainer.empty();
1314 							}
1315 
1316 						} else {
1317 							followUpContainer.empty();
1318 							followUpContainer.addClass('preparedForRemoval');
1319 						}
1320 
1321 						continue;
1322 
1323 					} else {
1324 						// split objects, which are AT the cursor Position or directly above
1325 						if (el.selection !== 'none') { // before cursor, leave this part inside the splitObject
1326 							// TODO better check for selection == 'partial' here?
1327 							if (el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined) {
1328 								completeText = el.domobj.data;
1329 								if (el.startOffset > 0) { // first check, if there will be some text left in the splitObject
1330 									el.domobj.data = completeText.substr(0, el.startOffset);
1331 								} 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
1332 									jQuery(el.domobj).remove();
1333 								} else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break
1334 									// if the parent is a blocklevel element, we insert the fillup element
1335 									parent = jQuery(el.domobj).parent();
1336 									if (GENTICS.Utils.Dom.isSplitObject(parent[0])) {
1337 										if (fillUpElement) {
1338 											parent.html(fillUpElement);
1339 										} else {
1340 											parent.empty();
1341 										}
1342 
1343 									} else {
1344 										// if the parent is no blocklevel element and would be empty now, we completely remove it
1345 										parent.remove();
1346 									}
1347 								}
1348 								if (completeText.length - el.startOffset > 0) {
1349 									// 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
1350 									mirrorLevel[i].data = completeText.substr(el.startOffset, completeText.length);
1351 								} else if (mirrorLevel.length > 1) {
1352 									// if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed
1353 									mirrorLevel.eq((i)).remove();
1354 								} else if (GENTICS.Utils.Dom.isBlockLevelElement(followUpContainer[0])) {
1355 									// if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break
1356 									if (fillUpElement) {
1357 										followUpContainer.html(fillUpElement);
1358 									} else {
1359 										followUpContainer.empty();
1360 									}
1361 
1362 								} else {
1363 									// if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal
1364 									followUpContainer.empty();
1365 									followUpContainer.addClass('preparedForRemoval');
1366 								}
1367 							}
1368 
1369 							startMoving = true;
1370 
1371 							if (el.children.length > 0) {
1372 								this.splitRangeObjectHelper(el.children, rangeObject, mirrorLevel.eq(i), inBetweenMarkup);
1373 							}
1374 
1375 						} else {
1376 							// remove all objects in the origin, which are AFTER the cursor
1377 							if (el.selection === 'none' && startMoving === true) {
1378 								// iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer
1379 								jqObj = jQuery(el.domobj).remove();
1380 							}
1381 						}
1382 					}
1383 				}
1384 			} else {
1385 				Aloha.Log.error(this, 'can not split splitObject due to an empty selection tree');
1386 			}
1387 
1388 			// and finally cleanup: remove all fillUps > 1
1389 			splitObject.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
1390 			followUpContainer.find('br.aloha-ephemera:gt(0)').remove(); // remove all elements greater than (gt) 0, that also means: leave one
1391 
1392 			// remove objects prepared for removal
1393 			splitObject.find('.preparedForRemoval').remove();
1394 			followUpContainer.find('.preparedForRemoval').remove();
1395 
1396 			// if splitObject / followUp are empty, place a fillUp inside
1397 			if (splitObject.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(splitObject[0]) && fillUpElement) {
1398 				splitObject.html(fillUpElement);
1399 			}
1400 
1401 			if (followUpContainer.contents().length === 0 && GENTICS.Utils.Dom.isSplitObject(followUpContainer[0]) && fillUpElement) {
1402 				followUpContainer.html(fillUpElement);
1403 			}
1404 		},
1405 
1406 		/**
1407 		 * returns a jQuery object fitting the passed splitObject as follow up object
1408 		 * examples,
1409 		 * - when passed a p it will return an empty p (clone of the passed p)
1410 		 * - 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)
1411 		 * @param rangeObject Aloha.RangeObject
1412 		 * @return void
1413 		 */
1414 		getSplitFollowUpContainer: function (rangeObject) {
1415 			var tagName = rangeObject.splitObject.nodeName.toLowerCase(),
1416 				returnObj,
1417 				inside,
1418 				lastObj;
1419 
1420 			switch (tagName) {
1421 			case 'h1':
1422 			case 'h2':
1423 			case 'h3':
1424 			case 'h4':
1425 			case 'h5':
1426 			case 'h6':
1427 				// get the last textnode in the splitobject, but don't consider aloha-cleanme elements
1428 				lastObj = jQuery(rangeObject.splitObject).textNodes(':not(.aloha-cleanme)').last()[0];
1429 				// special case: when enter is hit at the end of a heading, the followUp should be a <p>
1430 				if (lastObj && rangeObject.startContainer === lastObj && rangeObject.startOffset === lastObj.length) {
1431 					returnObj = jQuery('<p></p>');
1432 					inside = jQuery(rangeObject.splitObject.outerHTML).contents();
1433 					returnObj.append(inside);
1434 					return returnObj;
1435 				}
1436 				break;
1437 
1438 			case 'li':
1439 				// TODO check whether the li is the last one
1440 				// special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list)
1441 				if (rangeObject.startContainer.nodeName.toLowerCase() === 'br' && jQuery(rangeObject.startContainer).hasClass('aloha-ephemera')) {
1442 					returnObj = jQuery('<p></p>');
1443 					inside = jQuery(rangeObject.splitObject.outerHTML).contents();
1444 					returnObj.append(inside);
1445 					return returnObj;
1446 				}
1447 				// when the li is the last one and empty, we also just return a <p>
1448 				if (!rangeObject.splitObject.nextSibling && jQuery.trim(jQuery(rangeObject.splitObject).text()).length === 0) {
1449 					returnObj = jQuery('<p></p>');
1450 					return returnObj;
1451 				}
1452 				break;
1453 			}
1454 
1455 			return jQuery(rangeObject.splitObject.outerHTML);
1456 		},
1457 
1458 		/**
1459 		 * Transform the given domobj into an object with the given new nodeName.
1460 		 * Preserves the content and all attributes. If a range object is given, also the range will be preserved
1461 		 * @param domobj dom object to transform
1462 		 * @param nodeName new node name
1463 		 * @param range range object
1464 		 * @api
1465 		 * @return new object as jQuery object
1466 		 */
1467 		transformDomObject: function (domobj, nodeName, range) {
1468 			// first create the new element
1469 			var jqOldObj = jQuery(domobj),
1470 				jqNewObj = jQuery('<' + nodeName + '>'),
1471 				i,
1472 				attributes = jqOldObj[0].cloneNode(false).attributes;
1473 
1474 			// TODO what about events?
1475 			// copy attributes
1476 			if (attributes) {
1477 				for (i = 0; i < attributes.length; ++i) {
1478 					if (typeof attributes[i].specified === 'undefined' || attributes[i].specified) {
1479 						jqNewObj.attr(attributes[i].nodeName, attributes[i].nodeValue);
1480 					}
1481 				}
1482 			}
1483 
1484 			// copy inline CSS
1485 			if (jqOldObj[0].style && jqOldObj[0].style.cssText) {
1486 				jqNewObj[0].style.cssText = jqOldObj[0].style.cssText;
1487 			}
1488 
1489 			// now move the contents of the old dom object into the new dom object
1490 			jqOldObj.contents().appendTo(jqNewObj);
1491 
1492 			// finally replace the old object with the new one
1493 			jqOldObj.replaceWith(jqNewObj);
1494 
1495 			// preserve the range
1496 			if (range) {
1497 				if (range.startContainer == domobj) {
1498 					range.startContainer = jqNewObj.get(0);
1499 				}
1500 
1501 				if (range.endContainer == domobj) {
1502 					range.endContainer = jqNewObj.get(0);
1503 				}
1504 			}
1505 
1506 			return jqNewObj;
1507 		},
1508 
1509 		/**
1510 		 * String representation
1511 		 * @return {String}
1512 		 */
1513 		toString: function () {
1514 			return 'Aloha.Markup';
1515 		}
1516 
1517 	});
1518 
1519 	Aloha.Markup = new Aloha.Markup();
1520 	return Aloha.Markup;
1521 });
1522