1 /*!
  2 * This file is part of Aloha Editor Project http://aloha-editor.org
  3 * Copyright © 2010-2011 Gentics Software GmbH, aloha@gentics.com
  4 * Contributors http://aloha-editor.org/contribution.php
  5 * Licensed unter the terms of http://www.aloha-editor.org/license.html
  6 *//*
  7 * Aloha Editor is free software: you can redistribute it and/or modify
  8 * it under the terms of the GNU Affero General Public License as published by
  9 * the Free Software Foundation, either version 3 of the License, or
 10 * (at your option) 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 Affero General Public License for more details.
 16 *
 17 * You should have received a copy of the GNU Affero General Public License
 18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 19 */
 20 
 21 define(
 22 [ 'aloha/core', 'util/class', 'aloha/jquery' ],
 23 function( Aloha, Class, jQuery ) {
 24 "use strict";
 25 
 26 var GENTICS = window.GENTICS;
 27 
 28 /**
 29  * Markup object
 30  */
 31 Aloha.Markup = Class.extend( {
 32 
 33 	/**
 34 	 * Key handlers for special key codes
 35 	 */
 36 	keyHandlers: {},
 37 
 38 	/**
 39 	 * Add a key handler for the given key code
 40 	 * @param keyCode key code
 41 	 * @param handler handler function
 42 	 */
 43 	addKeyHandler: function( keyCode, handler ) {
 44 		if ( !this.keyHandlers[ keyCode ] ) {
 45 			this.keyHandlers[ keyCode ] = [];
 46 		}
 47 
 48 		this.keyHandlers[ keyCode ].push( handler );
 49 	},
 50 
 51 	insertBreak: function() {
 52 		var range = Aloha.Selection.rangeObject,
 53 		    onWSIndex,
 54 		    nextTextNode,
 55 		    newBreak;
 56 
 57 		if ( !range.isCollapsed() ) {
 58 			this.removeSelectedMarkup();
 59 		}
 60 
 61 		newBreak = jQuery( '<br/>' );
 62 		GENTICS.Utils.Dom.insertIntoDOM( newBreak, range, Aloha.activeEditable.obj );
 63 
 64 		nextTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
 65 			newBreak.parent().get( 0 ),
 66 			GENTICS.Utils.Dom.getIndexInParent( newBreak.get( 0 ) ) + 1,
 67 			false
 68 		);
 69 
 70 		if ( nextTextNode ) {
 71 			// trim leading whitespace
 72  73 			nonWSIndex = nextTextNode.data.search( /\S/ );
 74 			if ( nonWSIndex > 0 ) {
 75 				nextTextNode.data = nextTextNode.data.substring( nonWSIndex );
 76 			}
 77 		}
 78 
 79 		range.startContainer = range.endContainer = newBreak.get( 0 ).parentNode;
 80 		range.startOffset = range.endOffset = GENTICS.Utils.Dom.getIndexInParent( newBreak.get( 0 ) ) + 1;
 81 		range.correctRange();
 82 		range.clearCaches();
 83 		range.select();
 84 	},
 85 
 86 	/**
 87 	 * first method to handle key strokes
 88 	 * @param event DOM event
 89 	 * @param rangeObject as provided by Aloha.Selection.getRangeObject();
 90 	 * @return "Aloha.Selection"
 91 	 */
 92 	preProcessKeyStrokes: function( event ) {
 93 		if ( event.type !== 'keydown' ) {
 94 			return false;
 95 		}
 96 
 97 		var rangeObject = Aloha.Selection.rangeObject,
 98 		    handlers,
 99 		    i;
100 
101 		if ( this.keyHandlers[ event.keyCode ] ) {
102 			handlers = this.keyHandlers[ event.keyCode ];
103 			for ( i = 0; i < handlers.length; ++i ) {
104 				if ( !handlers[i]( event ) ) {
105 					return false;
106 				}
107 			}
108 		}
109 
110 		// handle left (37) and right (39) keys for block detection
111 		if ( event.keyCode === 37 || event.keyCode === 39 ) {
112 			return this.processCursor( rangeObject, event.keyCode );
113 		}
114 
115 		// BACKSPACE
116 117 		if ( event.keyCode === 8 ) {
118 			event.preventDefault(); // prevent history.back() even on exception
119 			Aloha.execCommand( 'delete', false );
120 			return false;
121 		}
122 
123 		// DELETE
124 		if ( event.keyCode === 46 ) {
125 			Aloha.execCommand( 'forwarddelete', false );
126 			return false;
127 		}
128 
129 		// ENTER
130 		if  ( event.keyCode === 13 ) {
131 			if ( event.shiftKey ) {
132 				Aloha.execCommand( 'insertlinebreak', false );
133 				return false;
134 			} else {
135 				Aloha.execCommand( 'insertparagraph', false );
136 				return false;
137 			}
138 		}
139 
140 		return true;
141 	},
142 
143 	/**
144 	 * Processing of cursor keys
145 	 * will currently detect blocks (elements with contenteditable=false)
146 	 * and selects them (normally the cursor would jump right past them)
147 	 *
148 	 * For each block an 'aloha-block-selected' event will be triggered.
149 	 *
150 	 * @param range the current range object
151 	 * @param keyCode keyCode of current keypress
152 	 * @return false if a block was found to prevent further events, true otherwise
153 	 */
154 	processCursor: function( range, keyCode ) {
155 		var rt = range.getRangeTree(), // RangeTree reference
156 		    i = 0,
157 		    cursorLeft = keyCode === 37,
158 		    cursorRight = keyCode === 39,
159 		    nextSiblingIsBlock = false, // check whether the next sibling is a block (contenteditable = false)
160 		    cursorIsWithinBlock = false, // check whether the cursor is positioned within a block (contenteditable = false)
161 		    cursorAtLastPos = false, // check if the cursor is within the last position of the currently active dom element
162 		    obj; // will contain references to dom objects
163 
164 		if ( !range.isCollapsed() ) {
165 			return true;
166 		}
167 
168 		for (;i < rt.length; i++) {
169 			cursorAtLastPos = range.startOffset === rt[i].domobj.length;
170 			if ( !cursorAtLastPos || typeof rt[i].domobj === 'undefined' ) {
171 				continue;
172 			}
173 				
174 			if ( cursorAtLastPos ) {
175 				nextSiblingIsBlock = jQuery( rt[i].domobj.nextSibling ).attr('contenteditable') === 'false';
176 				cursorIsWithinBlock = jQuery( rt[i].domobj ).parents('[contenteditable=false]').length > 0;
177 
178 				if ( cursorRight && nextSiblingIsBlock ) {
179 					obj = rt[i].domobj.nextSibling;
180 					GENTICS.Utils.Dom.selectDomNode( obj );
181 					Aloha.trigger( 'aloha-block-selected', obj );
182 					Aloha.Selection.preventSelectionChanged();
183 					return false;
184 				}
185 
186 				if ( cursorLeft && cursorIsWithinBlock ) {
187 					obj = jQuery( rt[i].domobj ).parents('[contenteditable=false]').get(0);
188 					if ( jQuery( obj ).parent().hasClass('aloha-editable') ) {
189 						GENTICS.Utils.Dom.selectDomNode( obj );
190 						Aloha.trigger( 'aloha-block-selected', obj );
191 						Aloha.Selection.preventSelectionChanged();
192 						return false;
193 					}
194 				}
195 			}
196 		}
197 	},
198 
199 	/**
200 	 * method handling shiftEnter
201 	 * @param Aloha.Selection.SelectionRange of the current selection
202 	 * @return void
203 	 */
204 	processShiftEnter: function( rangeObject ) {
205 		this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject );
206 	},
207 
208 	/**
209 	 * method handling Enter
210 	 * @param Aloha.Selection.SelectionRange of the current selection
211 	 * @return void
212 	 */
213 	processEnter: function( rangeObject ) {
214 		if ( rangeObject.splitObject ) {
215 			// 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
216 			// if ( jQuery.browser.msie
217 			// 		&& GENTICS.Utils.Dom
218 			// 				.isListElement( rangeObject.splitObject ) ) {
219 			// 	jQuery( rangeObject.splitObject ).append(
220 			// 			jQuery( document.createTextNode( '' ) ) );
221 			// }
222 			this.splitRangeObject( rangeObject );
223 		} else { // if there is no split object, the Editable is the paragraph type itself (e.g. a p or h2)
224 			this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject );
225 		}
226 	},
227 
228 	/**
229 	 * Insert the given html markup at the current selection
230 	 * @param html html markup to be inserted
231 	 */
232 	insertHTMLCode: function( html ) {
233 		var rangeObject = Aloha.Selection.rangeObject;
234 		this.insertHTMLBreak( rangeObject.getSelectionTree(), rangeObject, jQuery( html ) );
235 	},
236 
237 	/**
238 	 * insert an HTML Break <br /> into current selection
239 	 * @param Aloha.Selection.SelectionRange of the current selection
240 	 * @return void
241 	 */
242 	insertHTMLBreak: function( selectionTree, rangeObject, inBetweenMarkup ) {
243 		var i,
244 		    treeLength,
245 		    el,
246 		    jqEl,
247 		    jqElBefore,
248 		    jqElAfter,
249 		    tmpObject,
250 		    offset,
251 		    checkObj;
252 
253 		inBetweenMarkup = inBetweenMarkup ? inBetweenMarkup: jQuery( '<br/>' );
254 
255 		for ( i = 0, treeLength = selectionTree.length; i < treeLength; ++i ) {
256 			el = selectionTree[ i ];
257 			jqEl = el.domobj ? jQuery( el.domobj ) : undefined;
258 
259 			if ( el.selection !== 'none' ) { // before cursor, leave this part inside the splitObject
260 				if ( el.selection == 'collapsed' ) {
261 					// collapsed selection found (between nodes)
262 					if ( i > 0 ) {
263 						// not at the start, so get the element to the left
264 						jqElBefore = jQuery( selectionTree[ i - 1 ].domobj );
265 
266 						// and insert the break after it
267 						jqElBefore.after( inBetweenMarkup );
268 
269 					} else {
270 						// at the start, so get the element to the right
271 						jqElAfter = jQuery( selectionTree[1].domobj );
272 
273 						// and insert the break before it
274 						jqElAfter.before( inBetweenMarkup );
275 					}
276 
277 					// now set the range
278 					rangeObject.startContainer = rangeObject.endContainer = inBetweenMarkup[0].parentNode;
279 					rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent( inBetweenMarkup[0] ) + 1;
280 					rangeObject.correctRange();
281 
282 				} else if ( el.domobj && el.domobj.nodeType === 3 ) { // textNode
283 					// when the textnode is immediately followed by a blocklevel element (like p, h1, ...) we need to add an additional br in between
284 					if ( el.domobj.nextSibling
285 						 && el.domobj.nextSibling.nodeType == 1
286 						 && Aloha.Selection.replacingElements[
287 								el.domobj.nextSibling.nodeName.toLowerCase()
288 							] ) {
289 						// TODO check whether this depends on the browser
290 						jqEl.after( '<br/>' );
291 					}
292 
293 					if ( this.needEndingBreak() ) {
294 						// when the textnode is the last inside a blocklevel element
295 						// (like p, h1, ...) we need to add an additional br as very
296 						// last object in the blocklevel element
297 						checkObj = el.domobj;
298 
299 						while ( checkObj ) {
300 							if ( checkObj.nextSibling ) {
301 								checkObj = false;
302 							} else {
303 								// go to the parent
304 								checkObj = checkObj.parentNode;
305 
306 								// found a blocklevel or list element, we are done
307 								if ( GENTICS.Utils.Dom.isBlockLevelElement( checkObj )
308 									 || GENTICS.Utils.Dom.isListElement( checkObj ) ) {
309 									break;
310 								}
311 
312 								// reached the limit object, we are done
313 								if ( checkObj === rangeObject.limitObject ) {
314 									checkObj = false;
315 								}
316 							}
317 						}
318 
319 						// when we found a blocklevel element, insert a break at the
320 						// end. Mark the break so that it is cleaned when the
321 						// content is fetched.
322 						if ( checkObj ) {
323 							jQuery( checkObj ).append( '<br class="aloha-cleanme" />' );
324 						}
325 					}
326 
327 					// insert the break
328 					jqEl.between( inBetweenMarkup, el.startOffset );
329 
330 					// correct the range
331 					// count the number of previous siblings
332 					offset = 0;
333 					tmpObject = inBetweenMarkup[0];
334 					while ( tmpObject ) {
335 						tmpObject = tmpObject.previousSibling;
336 						++offset;
337 					}
338 
339 					rangeObject.startContainer = inBetweenMarkup[0].parentNode;
340 					rangeObject.endContainer = inBetweenMarkup[0].parentNode;
341 					rangeObject.startOffset = offset;
342 					rangeObject.endOffset = offset;
343 					rangeObject.correctRange();
344 
345 				} else if ( el.domobj && el.domobj.nodeType === 1 ) { // other node, normally a break
346 					if ( jqEl.parent().find( 'br.aloha-ephemera' ).length === 0 ) {
347 						// but before putting it, remove all:
348 						jQuery( rangeObject.limitObject ).find( 'br.aloha-ephemera' ).remove();
349 
350 						//  now put it:
351 						jQuery( rangeObject.commonAncestorContainer )
352 							.append( this.getFillUpElement( rangeObject.splitObject ) );
353 					}
354 
355 					jqEl.after( inBetweenMarkup );
356 
357 					// now set the selection. Since we just added one break do the currect el
358 					// the new position must be el's position + 1. el's position is the index
359 					// of the el in the selection tree, which is i. then we must add
360 					// another +1 because we want to be AFTER the object, not before. therefor +2
361 					rangeObject.startContainer = rangeObject.commonAncestorContainer;
362 					rangeObject.endContainer = rangeObject.startContainer;
363 					rangeObject.startOffset = i + 2;
364 					rangeObject.endOffset = i + 2;
365 					rangeObject.update();
366 				}
367 			}
368 		}
369 		rangeObject.select();
370 	},
371 
372 	/**
373 	 * Check whether blocklevel elements need breaks at the end to visibly render a newline
374 	 * @return true if an ending break is necessary, false if not
375 	 */
376 	needEndingBreak: function() {
377 		// currently, all browser except IE need ending breaks
378 		return !jQuery.browser.msie;
379 	},
380 
381 	/**
382 	 * Get the currently selected text or false if nothing is selected (or the selection is collapsed)
383 	 * @return selected text
384 	 */
385 	getSelectedText: function() {
386 		var rangeObject = Aloha.Selection.rangeObject;
387 
388 		if ( rangeObject.isCollapsed() ) {
389 			return false;
390 		}
391 
392 		return this.getFromSelectionTree( rangeObject.getSelectionTree(), true );
393 	},
394 
395 	/**
396 	 * Recursive function to get the selected text from the selection tree starting at the given level
397 	 * @param selectionTree array of selectiontree elements
398 	 * @param astext true when the contents shall be fetched as text, false for getting as html markup
399 	 * @return selected text from that level (incluiding all sublevels)
400 	 */
401 	getFromSelectionTree: function( selectionTree, astext ) {
402 		var text = '', i, treeLength, el, clone;
403 		for ( i = 0, treeLength = selectionTree.length; i < treeLength; i++ ) {
404 			el = selectionTree[i];
405 			if ( el.selection == 'partial' ) {
406 				if ( el.domobj.nodeType === 3 ) {
407 					// partial text node selected, get the selected part
408 					text += el.domobj.data.substring( el.startOffset, el.endOffset );
409 				} else if ( el.domobj.nodeType === 1 && el.children ) {
410 					// partial element node selected, do the recursion into the children
411 					if ( astext ) {
412 						text += this.getFromSelectionTree( el.children, astext );
413 					} else {
414 						// when the html shall be fetched, we create a clone of the element and remove all the children
415 						clone = jQuery( el.domobj ).clone( false ).empty();
416 						// then we do the recursion and add the selection into the clone
417 						clone.html( this.getFromSelectionTree( el.children, astext ) );
418 						// finally we get the html of the clone
419 						text += clone.outerHTML();
420 					}
421 				}
422 			} else if ( el.selection == 'full' ) {
423 				if ( el.domobj.nodeType === 3 ) {
424 					// full text node selected, get the text
425 					text += jQuery( el.domobj ).text();
426 				} else if ( el.domobj.nodeType === 1 && el.children ) {
427 					// full element node selected, get the html of the node and all children
428 					text += astext ? jQuery( el.domobj ).text() : jQuery( el.domobj ).outerHTML();
429 				}
430 			}
431 		}
432 
433 		return text;
434 	},
435 
436 	/**
437 	 * Get the currently selected markup or false if nothing is selected (or the selection is collapsed)
438 	 * @return {?String}
439 	 */
440 	getSelectedMarkup: function() {
441 		var rangeObject = Aloha.Selection.rangeObject;
442 		return rangeObject.isCollapsed() ? null
443 444 			: this.getFromSelectionTree( rangeObject.getSelectionTree(), false );
445 	},
446 
447 	/**
448 	 * Remove the currently selected markup
449 	 */
450 	removeSelectedMarkup: function() {
451 		var rangeObject = Aloha.Selection.rangeObject, newRange;
452 
453 		if ( rangeObject.isCollapsed() ) {
454 			return;
455 		}
456 
457 		newRange = new Aloha.Selection.SelectionRange();
458 		// remove the selection
459 		this.removeFromSelectionTree( rangeObject.getSelectionTree(), newRange );
460 
461 		// do a cleanup now (starting with the commonancestorcontainer)
462 		newRange.update();
463 		GENTICS.Utils.Dom.doCleanup( { 'merge' : true, 'removeempty' : true }, Aloha.Selection.rangeObject );
464 		Aloha.Selection.rangeObject = newRange;
465 
466 		// need to set the collapsed selection now
467 		newRange.correctRange();
468 		newRange.update();
469 		newRange.select();
470 		Aloha.Selection.updateSelection();
471 	},
472 
473 	/**
474 	 * Recursively remove the selected items, starting with the given level in the selectiontree
475 	 * @param selectionTree current level of the selectiontree
476 	 * @param newRange new collapsed range to be set after the removal
477 	 */
478 	removeFromSelectionTree: function( selectionTree, newRange ) {
479 		// remember the first found partially selected element node (in case we need
480 		// to merge it with the last found partially selected element node)
481 		var firstPartialElement,
482 		    newdata,
483 		    i,
484 		    el,
485 		    adjacentTextNode,
486 		    treeLength;
487 
488 		// iterate through the selection tree
489 		for ( i = 0, treeLength = selectionTree.length; i < treeLength; i++ ) {
490 			el = selectionTree[ i ];
491 
492 			// check the type of selection
493 			if ( el.selection == 'partial' ) {
494 				if ( el.domobj.nodeType === 3 ) {
495 					// partial text node selected, so remove the selected portion
496 					newdata = '';
497 					if ( el.startOffset > 0 ) {
498 						newdata += el.domobj.data.substring( 0, el.startOffset );
499 					}
500 					if ( el.endOffset < el.domobj.data.length ) {
501 						newdata += el.domobj.data.substring( el.endOffset, el.domobj.data.length );
502 					}
503 					el.domobj.data = newdata;
504 
505 					// eventually set the new range (if not done before)
506 					if ( !newRange.startContainer ) {
507 						newRange.startContainer = newRange.endContainer = el.domobj;
508 						newRange.startOffset = newRange.endOffset = el.startOffset;
509 					}
510 				} else if ( el.domobj.nodeType === 1 && el.children ) {
511 					// partial element node selected, so do the recursion into the children
512 					this.removeFromSelectionTree( el.children, newRange );
513 
514 					if ( firstPartialElement ) {
515 						// when the first parially selected element is the same type
516 						// of element, we need to merge them
517 						if ( firstPartialElement.nodeName == el.domobj.nodeName ) {
518 							// merge the nodes
519 							jQuery( firstPartialElement ).append( jQuery( el.domobj ).contents() );
520 
521 							// and remove the latter one
522 							jQuery( el.domobj ).remove();
523 						}
524 
525 					} else {
526 						// remember this element as first partially selected element
527 						firstPartialElement = el.domobj;
528 					}
529 				}
530 
531 			} else if ( el.selection == 'full' ) {
532 				// eventually set the new range (if not done before)
533 				if ( !newRange.startContainer ) {
534 					adjacentTextNode = GENTICS.Utils.Dom.searchAdjacentTextNode(
535 						el.domobj.parentNode,
536 						GENTICS.Utils.Dom.getIndexInParent( el.domobj ) + 1,
537 						false,
538 						{ 'blocklevel' : false }
539 					);
540 
541 					if ( adjacentTextNode ) {
542 						newRange.startContainer = newRange.endContainer = adjacentTextNode;
543 						newRange.startOffset = newRange.endOffset = 0;
544 					} else {
545 						newRange.startContainer = newRange.endContainer = el.domobj.parentNode;
546 						newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent( el.domobj ) + 1;
547 					}
548 				}
549 
550 				// full node selected, so just remove it (will also remove all children)
551 				jQuery( el.domobj ).remove();
552 			}
553 		}
554 	},
555 
556 	/**
557 	 * split passed rangeObject without or with optional markup
558 	 * @param Aloha.Selection.SelectionRange of the current selection
559 	 * @param markup object (jQuery) to insert in between the split elements
560 	 * @return void
561 	 */
562 	splitRangeObject: function( rangeObject, markup ) {
563 		// UAAAA: first check where the markup can be inserted... *grrrrr*, then decide where to split
564 		// object which is split up
565 		var
566 			splitObject = jQuery( rangeObject.splitObject ),
567 			selectionTree, insertAfterObject, followUpContainer;
568 
569 		// update the commonAncestor with the splitObject (so that the selectionTree is correct)
570 		rangeObject.update( rangeObject.splitObject ); // set the splitObject as new commonAncestorContainer and update the selectionTree
571 
572 		// calculate the selection tree. NOTE: it is necessary to do this before
573 		// getting the followupcontainer, since getting the selection tree might
574 		// possibly merge text nodes, which would lead to differences in the followupcontainer
575 		selectionTree = rangeObject.getSelectionTree();
576 
577 		// object to be inserted after the splitObject
578 		followUpContainer = this.getSplitFollowUpContainer( rangeObject );
579 
580 		// now split up the splitObject into itself AND the followUpContainer
581 		this.splitRangeObjectHelper( selectionTree, rangeObject, followUpContainer ); // split the current object into itself and the followUpContainer
582 
583 		// check whether the followupcontainer is still marked for removal
584 		if ( followUpContainer.hasClass( 'preparedForRemoval' ) ) {
585 			// TODO shall we just remove the class or shall we not use the followupcontainer?
586 			followUpContainer.removeClass( 'preparedForRemoval' );
587 		}
588 
589 		// now let's find the place, where the followUp is inserted afterwards. normally that's the splitObject itself, but in
590 		// some cases it might be their parent (e.g. inside a list, a <p> followUp must be inserted outside the list)
591 		insertAfterObject = this.getInsertAfterObject( rangeObject, followUpContainer );
592 
593 		// now insert the followUpContainer
594 		jQuery( followUpContainer ).insertAfter( insertAfterObject ); // attach the followUpContainer right after the insertAfterObject
595 
596 		// in some cases, we want to remove the "empty" splitObject (e.g. LIs, if enter was hit twice)
597 		if ( rangeObject.splitObject.nodeName.toLowerCase() === 'li' && !Aloha.Selection.standardTextLevelSemanticsComparator( rangeObject.splitObject, followUpContainer ) ) {
598 			jQuery( rangeObject.splitObject ).remove();
599 		}
600 
601 		rangeObject.startContainer = null;
602 		// first check whether the followUpContainer starts with a <br/>
603 		// if so, place the cursor right before the <br/>
604 		var followContents = followUpContainer.contents();
605 		if ( followContents.length > 0
606 			 && followContents.get( 0 ).nodeType == 1
607 			 && followContents.get( 0 ).nodeName.toLowerCase() === 'br' ) {
608 			rangeObject.startContainer = followUpContainer.get( 0 );
609 		}
610 
611 		if ( !rangeObject.startContainer ) {
612 			// find a possible text node in the followUpContainer and set the selection to it
613 			// if no textnode is available, set the selection to the followup container itself
614 			rangeObject.startContainer = followUpContainer.textNodes( true, true ).first().get( 0 );
615 		}
616 		if ( !rangeObject.startContainer ) { // if no text node was found, select the parent object of <br class="aloha-ephemera" />
617 			rangeObject.startContainer = followUpContainer.textNodes( false ).first().parent().get( 0 );
618 		}
619 		if ( rangeObject.startContainer ) {
620 			// the cursor is always at the beginning of the followUp
621 			rangeObject.endContainer = rangeObject.startContainer;
622 			rangeObject.startOffset = 0;
623 			rangeObject.endOffset = 0;
624 		} else {
625 			rangeObject.startContainer = rangeObject.endContainer = followUpContainer.parent().get( 0 );
626 			rangeObject.startOffset = rangeObject.endOffset = GENTICS.Utils.Dom.getIndexInParent( followUpContainer.get( 0 ) );
627 		}
628 
629 		// finally update the range object again
630 		rangeObject.update();
631 
632 		// now set the selection
633 		rangeObject.select();
634 	},
635 
636 	/**
637 	 * method to get the object after which the followUpContainer can be inserted during splitup
638 	 * this is a helper method, not needed anywhere else
639 	 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
640 	 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
641 	 * @return object after which the followUpContainer can be inserted
642 	 */
643 	getInsertAfterObject: function( rangeObject, followUpContainer ) {
644 		var passedSplitObject, i, el;
645 
646 		for ( i = 0; i < rangeObject.markupEffectiveAtStart.length; i++ ) {
647 			el = rangeObject.markupEffectiveAtStart[ i ];
648 
649 			// check if we have already passed the splitObject (some other markup might come before)
650 			if ( el === rangeObject.splitObject ) {
651 				passedSplitObject = true;
652 			}
653 
654 			// if not passed splitObject, skip this markup
655 			if ( !passedSplitObject ) {
656 				continue;
657 			}
658 
659 			// once we are passed, check if the followUpContainer is allowed to be inserted into the currents el's parent
660 			if ( Aloha.Selection.canTag1WrapTag2( jQuery( el ).parent()[0].nodeName, followUpContainer[0].nodeName ) ) {
661 				return el;
662 			}
663 		}
664 665 
666 		return false;
667 	},
668 
669 	/**
670 	 * @fixme: Someone who knows what this function does, please refactor it.
671 	 *			1. splitObject arg is not used at all
672 	 *			2. Would be better to use ternary operation would be better than if else statement
673 	 *
674 	 * method to get the html code for a fillUpElement. this is needed for empty paragraphs etc., so that they take up their expected height
675 	 * @param splitObject split object (dom object)
676 	 * @return fillUpElement HTML Code
677 	 */
678 	getFillUpElement: function( splitObject ) {
679 		if ( jQuery.browser.msie ) {
680 			return false;
681 		} else {
682 			return jQuery( '<br class="aloha-cleanme"/>' );
683 		}
684 	},
685 
686 	/**
687 	 * removes textNodes from passed array, which only contain contentWhiteSpace (e.g. a \n between two tags)
688 	 * @param domArray array of domObjects
689 	 * @return void
690 	 */
691 	removeElementContentWhitespaceObj: function( domArray ) {
692 		var correction = 0,
693 		    removeLater = [],
694 		    i,
695 		    el, removeIndex;
696 
697 		for ( i = 0; i < domArray.length; ++i ) {
698 			el = domArray[ i ];
699 			if ( el.isElementContentWhitespace ) {
700 				removeLater[ removeLater.length ] = i;
701 			}
702 		}
703 
704 		for ( i = 0; i < removeLater.length; ++i ) {
705 			removeIndex = removeLater[ i ];
706 			domArray.splice( removeIndex - correction, 1 );
707 			++correction;
708 		}
709 	},
710 
711 	/**
712 	 * recursive method to parallelly walk through two dom subtrees, leave elements before startContainer in first subtree and move rest to other
713 	 * @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
714 715 	 * @param rangeObject Aloha.Selection.SelectionRange of the current selection
716 	 * @param followUpContainer optional jQuery object; if provided the rangeObject will be split and the second part will be insert inside of this object
717 	 * @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
718 	 * @return void
719 	 */
720 721 	splitRangeObjectHelper: function( selectionTree, rangeObject,
722 									  followUpContainer, inBetweenMarkup ) {
723 		if ( !followUpContainer ) {
724 			Aloha.Log.warn( this, 'no followUpContainer, no inBetweenMarkup, nothing to do...' );
725 		}
726 
727 		var fillUpElement = this.getFillUpElement( rangeObject.splitObject ),
728 		    splitObject = jQuery( rangeObject.splitObject ),
729 		    startMoving = false,
730 		    el,
731 		    i,
732 		    completeText,
733 		    jqObj,
734 		    mirrorLevel,
735 		    parent,
736 		    treeLength;
737 
738 		if ( selectionTree.length > 0 ) {
739 			mirrorLevel = followUpContainer.contents();
740 
741 			// if length of mirrorLevel and selectionTree are not equal, the mirrorLevel must be corrected. this happens, when the mirrorLevel contains whitespace textNodes
742 			if ( mirrorLevel.length !== selectionTree.length ) {
743 				this.removeElementContentWhitespaceObj( mirrorLevel );
744 			}
745 
746 			for ( i = 0, treeLength = selectionTree.length; i < treeLength; ++i ) {
747 				el = selectionTree[ i ];
748 
749 				// remove all objects in the mirrorLevel, which are BEFORE the cursor
750 				// OR if the cursor is at the last position of the last Textnode (causing an empty followUpContainer to be appended)
751 				if ( ( el.selection === 'none' && startMoving === false ) ||
752 					 ( el.domobj && el.domobj.nodeType === 3
753 						&& el === selectionTree[ ( selectionTree.length - 1 ) ]
754 						&& el.startOffset === el.domobj.data.length ) ) {
755 					// iteration is before cursor, leave this part inside the splitObject, remove from followUpContainer
756 					// however if the object to remove is the last existing textNode within the followUpContainer, insert a BR instead
757 					// otherwise the followUpContainer is invalid and takes up no vertical space
758 
759 					if ( followUpContainer.textNodes().length > 1
760 						 || ( el.domobj.nodeType === 1 && el.children.length === 0 ) ) {
761 						// note: the second part of the if (el.domobj.nodeType === 1 && el.children.length === 0) covers a very special condition,
762 						// where an empty tag is located right before the cursor when pressing enter. In this case the empty tag would not be
763 						// removed correctly otherwise
764 						mirrorLevel.eq( i ).remove();
765 
766 					} else if ( GENTICS.Utils.Dom.isSplitObject( followUpContainer[0] ) ) {
767 						if ( fillUpElement ) {
768 							followUpContainer.html( fillUpElement ); // for your zoological german knowhow: ephemera = Eintagsfliege
769 						} else {
770 							followUpContainer.empty();
771 						}
772 
773 					} else {
774 						followUpContainer.empty();
775 						followUpContainer.addClass( 'preparedForRemoval' );
776 					}
777 
778 					continue;
779 
780 				} else {
781 					// split objects, which are AT the cursor Position or directly above
782 					if ( el.selection !== 'none' ) { // before cursor, leave this part inside the splitObject
783 						// TODO better check for selection == 'partial' here?
784 						if ( el.domobj && el.domobj.nodeType === 3 && el.startOffset !== undefined ) {
785 							completeText = el.domobj.data;
786 							if ( el.startOffset > 0 ) {// first check, if there will be some text left in the splitObject
787 								el.domobj.data = completeText.substr( 0, el.startOffset );
788 							} 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
789 								jQuery( el.domobj ).remove();
790 							} else { // if the "empty" textnode is the last node left in the splitObject, replace it with a ephemera break
791 								// if the parent is a blocklevel element, we insert the fillup element
792 								parent = jQuery( el.domobj ).parent();
793 								if ( GENTICS.Utils.Dom.isSplitObject( parent[0] ) ) {
794 									if ( fillUpElement ) {
795 										parent.html( fillUpElement );
796 									} else {
797 										parent.empty();
798 									}
799 
800 								} else {
801 									// if the parent is no blocklevel element and would be empty now, we completely remove it
802 									parent.remove();
803 								}
804 							}
805 							if ( completeText.length - el.startOffset > 0 ) {
806 								// 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
807 								mirrorLevel[i].data = completeText.substr( el.startOffset, completeText.length );
808 							} else if ( mirrorLevel.length > 1 ) {
809 								// if not, check if the followUpContainer contains more than one node, because if yes, the "empty" textnode can be removed
810 								mirrorLevel.eq( ( i ) ).remove();
811 							} else if ( GENTICS.Utils.Dom.isBlockLevelElement( followUpContainer[0] ) ) {
812 								// if the "empty" textnode is the last node left in the followUpContainer (which is a blocklevel element), replace it with a ephemera break
813 								if ( fillUpElement ) {
814 									followUpContainer.html( fillUpElement );
815 								} else {
816 									followUpContainer.empty();
817 								}
818 
819 							} else {
820 								// if the "empty" textnode is the last node left in a non-blocklevel element, mark it for removal
821 								followUpContainer.empty();
822 								followUpContainer.addClass( 'preparedForRemoval' );
823 							}
824 						}
825 
826 						startMoving = true;
827 
828 						if ( el.children.length > 0 ) {
829 							this.splitRangeObjectHelper( el.children, rangeObject, mirrorLevel.eq( i ), inBetweenMarkup );
830 						}
831 
832 					} else {
833 						// remove all objects in the origin, which are AFTER the cursor
834 						if ( el.selection === 'none' && startMoving === true ) {
835 							// iteration is after cursor, remove from splitObject and leave this part inside the followUpContainer
836 							jqObj = jQuery( el.domobj ).remove();
837 						}
838 					}
839 				}
840 			}
841 		} else {
842 			Aloha.Log.error( this, 'can not split splitObject due to an empty selection tree' );
843 		}
844 
845 		// and finally cleanup: remove all fillUps > 1
846 		splitObject.find( 'br.aloha-ephemera:gt(0)' ).remove(); // remove all elements greater than (gt) 0, that also means: leave one
847 		followUpContainer.find( 'br.aloha-ephemera:gt(0)' ).remove(); // remove all elements greater than (gt) 0, that also means: leave one
848 
849 		// remove objects prepared for removal
850 		splitObject.find( '.preparedForRemoval' ).remove();
851 		followUpContainer.find( '.preparedForRemoval' ).remove();
852 
853 		// if splitObject / followUp are empty, place a fillUp inside
854 		if ( splitObject.contents().length === 0
855 			 && GENTICS.Utils.Dom.isSplitObject( splitObject[0] )
856 			 && fillUpElement ) {
857 			splitObject.html( fillUpElement );
858 		}
859 
860 		if ( followUpContainer.contents().length === 0
861 			 && GENTICS.Utils.Dom.isSplitObject( followUpContainer[0] )
862 			 && fillUpElement ) {
863 			followUpContainer.html( fillUpElement );
864 		}
865 	},
866 
867 	/**
868 	 * returns a jQuery object fitting the passed splitObject as follow up object
869 	 * examples,
870 	 * - when passed a p it will return an empty p (clone of the passed p)
871 	 * - 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)
872 	 * @param rangeObject Aloha.RangeObject
873 	 * @return void
874 	 */
875 	getSplitFollowUpContainer: function( rangeObject ) {
876 		var tagName = rangeObject.splitObject.nodeName.toLowerCase(),
877 		    returnObj,
878 		    inside,
879 		    lastObj;
880 
881 		switch ( tagName ) {
882 			case 'h1':
883 			case 'h2':
884 			case 'h3':
885 			case 'h4':
886 			case 'h5':
887 			case 'h6':
888 				// get the last textnode in the splitobject, but don't consider aloha-cleanme elements
889 				lastObj = jQuery( rangeObject.splitObject ).textNodes( ':not(.aloha-cleanme)' ).last()[0];
890 				// special case: when enter is hit at the end of a heading, the followUp should be a <p>
891 				if ( lastObj && rangeObject.startContainer === lastObj
892 					 && rangeObject.startOffset === lastObj.length ) {
893 					returnObj = jQuery( '<p></p>' );
894 					inside = jQuery( rangeObject.splitObject ).clone().contents();
895 					returnObj.append( inside );
896 					return returnObj;
897 				}
898 				break;
899 
900 			case 'li':
901 				// TODO check whether the li is the last one
902 				// special case: if enter is hit twice inside a list, the next item should be a <p> (and inserted outside the list)
903 				if ( rangeObject.startContainer.nodeName.toLowerCase() === 'br'
904 					 && jQuery( rangeObject.startContainer ).hasClass( 'aloha-ephemera' ) ) {
905 					returnObj = jQuery( '<p></p>' );
906 					inside = jQuery( rangeObject.splitObject ).clone().contents();
907 					returnObj.append( inside );
908 					return returnObj;
909 				}
910 				// when the li is the last one and empty, we also just return a <p>
911 				if ( !rangeObject.splitObject.nextSibling
912 					 && jQuery.trim( jQuery( rangeObject.splitObject ).text() ).length === 0 ) {
913 					returnObj = jQuery( '<p></p>' );
914 					return returnObj;
915 				}
916 		}
917 
918 		return jQuery( rangeObject.splitObject ).clone();
919 	},
920 
921 	/**
922 	 * Transform the given domobj into an object with the given new nodeName.
923 	 * Preserves the content and all attributes. If a range object is given, also the range will be preserved
924 	 * @param domobj dom object to transform
925 	 * @param nodeName new node name
926 	 * @param range range object
927 	 * @api
928 	 * @return new object as jQuery object
929 	 */
930 	transformDomObject: function( domobj, nodeName, range ) {
931 		// first create the new element
932 		var jqOldObj = jQuery( domobj ),
933 		    jqNewObj = jQuery( '<' + nodeName + '></' + nodeName + '>' ),
934 		    i;
935 
936 		// TODO what about events? css properties?
937 
938 		// copy attributes
939 		if ( jqOldObj[0].attributes ) {
940 			for ( i = 0; i < jqOldObj[0].attributes.length; ++i ) {
941 				jqNewObj.attr(
942 					jqOldObj[0].attributes[ i ].nodeName,
943 					jqOldObj[0].attributes[ i ].nodeValue
944 				);
945 			}
946 		}
947 
948 		// copy inline CSS
949 		if ( jqOldObj[0].style && jqOldObj[0].style.cssText ) {
950 			jqNewObj[0].style.cssText = jqOldObj[0].style.cssText;
951 		}
952 
953 		// now move the contents of the old dom object into the new dom object
954 		jqOldObj.contents().appendTo( jqNewObj );
955 
956 		// finally replace the old object with the new one
957 		jqOldObj.replaceWith( jqNewObj );
958 
959 		// preserve the range
960 		if ( range ) {
961 			if ( range.startContainer == domobj ) {
962 				range.startContainer = jqNewObj.get( 0 );
963 			}
964 
965 			if ( range.endContainer == domobj ) {
966 				range.endContainer = jqNewObj.get( 0 );
967 			}
968 		}
969 
970 		return jqNewObj;
971 	},
972 
973 	/**
974 	 * String representation
975 	 * @return {String}
976 	 */
977 	toString: function() {
978 		return 'Aloha.Markup';
979 	}
980 
981 } );
982 
983 Aloha.Markup = new Aloha.Markup();
984 
985 return Aloha.Markup;
986 
987 } );
988