1 /* selection.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  26  * recipients can access the Corresponding Source.
 27  */
 28 define([
 29 	'aloha/core',
 30 	'jquery',
 31 	'util/class',
 32 	'util/range',
 33 	'util/arrays',
 34 	'util/strings',
 35 	'util/dom',
 36  37 	'util/dom2',
 38 	'aloha/console',
 39 	'PubSub',
 40 	'aloha/engine',
 41 	'aloha/ecma5shims',
 42 	'aloha/rangy-core'
 43 ], function (
 44 	Aloha,
 45 	jQuery,
 46 	Class,
 47 	Range,
 48 	Arrays,
 49 	Strings,
 50 	Dom,
 51 	Dom2,
 52  53 	console,
 54 	PubSub,
 55 	Engine,
 56 	e5s
 57 ) {
 58 	"use strict";
 59 
 60 	var GENTICS = window.GENTICS;
 61 
 62 	function isCollapsedAndEmptyOrEndBr(rangeObject) {
 63 		var firstChild;
 64 		if (rangeObject.startContainer !== rangeObject.endContainer) {
 65 			return false;
 66 		}
 67 		// check whether the container starts in an element node
 68 		if (rangeObject.startContainer.nodeType != 1) {
 69 			return false;
 70 		}
 71 		firstChild = rangeObject.startContainer.firstChild;
 72 		return (!firstChild || (!firstChild.nextSibling && firstChild.nodeName == 'BR'));
 73 	}
 74 
 75 	function isCollapsedAndEndBr(rangeObject) {
 76 		if (rangeObject.startContainer !== rangeObject.endContainer) {
 77 			return false;
 78 		}
 79 		if (rangeObject.startContainer.nodeType != 1) {
 80 			return false;
 81 		}
 82 		return Engine.isEndBreak(rangeObject.startContainer);
 83 	}
 84 
 85 	var prevStartContext = null;
 86 	var prevEndContext = null;
 87 
 88 	function makeContextHtml(node, parents) {
 89 		var result = [],
 90 			parent,
 91 			len,
 92 			i;
 93 		if (1 === node.nodeType && node.nodeName !== 'BODY' && node.nodeName !== 'HTML') {
 94 			result.push(node.cloneNode(false).outerHTML);
 95 		} else {
 96 			result.push('#' + node.nodeType);
 97 		}
 98 		for (i = 0, len = parents.length; i < len; i++) {
 99 			parent = parents[i];
100 			if (parent.nodeName === 'BODY' || parent.nodeName === 'HTML') {
101 				// Although we limit the ancestors in most cases to the
102 				// active editable, in some cases (copy&paste) the
103 				// parent may be outside.
104 				// On IE7 this means the following code may clone the
105 				// HTML node too, which causes the browser to crash.
106 				// On other browsers, this is just an optimization
107 				// because the body and html elements should probably
108 				// not be considered part of the context of an edit
109 				// operation.
110 				break;
111 			}
112 			result.push(parent.cloneNode(false).outerHTML);
113 		}
114 		return result.join('');
115 	}
116 
117 	function getChangedContext(node, context) {
118 		var until = Aloha.activeEditable ? Aloha.activeEditable.obj.parent()[0] : null;
119 		var parents = jQuery(node).parentsUntil(until).get();
120 		var html = makeContextHtml(node, parents);
121 		var equal = (context && node === context.node && Arrays.equal(context.parents, parents) && html === context.html);
122 		return equal ? null : {
123 			node: node,
124 			parents: parents,
125 			html: html
126 		};
127 	}
128 
129 	function triggerSelectionContextChanged(rangeObject, event) {
130 		var startContainer = rangeObject.startContainer;
131 		var endContainer = rangeObject.endContainer;
132 		if (!startContainer || !endContainer) {
133 			console.warn("aloha/selection", "encountered range object without start or end container");
134 			return;
135 		}
136 		var startContext = getChangedContext(startContainer, prevStartContext);
137 		var endContext = getChangedContext(endContainer, prevEndContext);
138 		if (!startContext && !endContext) {
139 			return;
140 		}
141 		prevStartContext = startContext;
142 		prevEndContext = endContext;
143 
144 		/**
145 		 * @api documented in the guides
146 		 */
147 		PubSub.pub('aloha.selection.context-change', {
148 			range: rangeObject,
149 			event: event
150 		});
151 	}
152 
153 	/**
154 	 * @namespace Aloha
155 	 * @class Selection
156 	 * This singleton class always represents the current user selection
157 	 * @singleton
158 	 */
159 	var Selection = Class.extend({
160 		_constructor: function () {
161 			// Pseudo Range Clone being cleaned up for better HTML wrapping support
162 			this.rangeObject = {};
163 
164 			this.preventSelectionChangedFlag = false; // will remember if someone urged us to skip the next aloha-selection-changed event
165 
166 			// define basics first
167 			this.tagHierarchy = {
168 				'textNode': {},
169 				'abbr': {
170 					'textNode': true
171 				},
172 				'b': {
173 					'textNode': true,
174 					'b': true,
175 					'i': true,
176 					'em': true,
177 					'sup': true,
178 					'sub': true,
179 					'br': true,
180 					'span': true,
181 					'img': true,
182 					'a': true,
183 					'del': true,
184 					'ins': true,
185 					'u': true,
186 					'cite': true,
187 					'q': true,
188 					'code': true,
189 					'abbr': true,
190 					'strong': true
191 				},
192 				'pre': {
193 					'textNode': true,
194 					'b': true,
195 					'i': true,
196 					'em': true,
197 					'sup': true,
198 					'sub': true,
199 					'br': true,
200 					'span': true,
201 					'img': true,
202 					'a': true,
203 					'del': true,
204 					'ins': true,
205 					'u': true,
206 					'cite': true,
207 					'q': true,
208 					'code': true,
209 					'abbr': true
210 				},
211 				'blockquote': {
212 					'textNode': true,
213 					'b': true,
214 					'i': true,
215 					'em': true,
216 					'sup': true,
217 					'sub': true,
218 					'br': true,
219 					'span': true,
220 					'img': true,
221 					'a': true,
222 					'del': true,
223 					'ins': true,
224 					'u': true,
225 					'cite': true,
226 					'q': true,
227 					'code': true,
228 					'abbr': true,
229 					'p': true,
230 					'h1': true,
231 					'h2': true,
232 					'h3': true,
233 					'h4': true,
234 					'h5': true,
235 					'h6': true
236 				},
237 				'ins': {
238 					'textNode': true,
239 					'b': true,
240 					'i': true,
241 					'em': true,
242 					'sup': true,
243 					'sub': true,
244 					'br': true,
245 					'span': true,
246 					'img': true,
247 					'a': true,
248 					'u': true,
249 					'p': true,
250 					'h1': true,
251 					'h2': true,
252 					'h3': true,
253 					'h4': true,
254 					'h5': true,
255 					'h6': true
256 				},
257 				'ul': {
258 					'li': true
259 				},
260 				'ol': {
261 					'li': true
262 				},
263 				'li': {
264 					'textNode': true,
265 					'b': true,
266 					'i': true,
267 					'em': true,
268 					'sup': true,
269 					'sub': true,
270 					'br': true,
271 					'span': true,
272 					'img': true,
273 					'ul': true,
274 					'ol': true,
275 					'h1': true,
276 					'h2': true,
277 					'h3': true,
278 					'h4': true,
279 					'h5': true,
280 					'h6': true,
281 					'del': true,
282 					'ins': true,
283 					'u': true,
284 					'a': true
285 				},
286 				'tr': {
287 					'td': true,
288 					'th': true
289 				},
290 				'table': {
291 					'tr': true
292 				},
293 				'div': {
294 					'textNode': true,
295 					'b': true,
296 					'i': true,
297 					'em': true,
298 					'sup': true,
299 					'sub': true,
300 					'br': true,
301 					'span': true,
302 					'img': true,
303 					'ul': true,
304 					'ol': true,
305 					'table': true,
306 					'h1': true,
307 					'h2': true,
308 					'h3': true,
309 					'h4': true,
310 					'h5': true,
311 					'h6': true,
312 					'del': true,
313 					'ins': true,
314 					'u': true,
315 					'p': true,
316 					'div': true,
317 					'pre': true,
318 					'blockquote': true,
319 					'a': true
320 				},
321 				'h1': {
322 					'textNode': true,
323 					'b': true,
324 					'i': true,
325 					'em': true,
326 					'sup': true,
327 					'sub': true,
328 					'br': true,
329 					'span': true,
330 					'img': true,
331 					'a': true,
332 					'del': true,
333 					'ins': true,
334 					'u': true
335 				}
336 			};
337 
338 			// now reference the basics for all other equal tags (important: don't forget to include
339 			// the basics itself as reference: 'b' : this.tagHierarchy.b
340 			this.tagHierarchy = {
341 				'textNode': this.tagHierarchy.textNode,
342 				'abbr': this.tagHierarchy.abbr,
343 				'br': this.tagHierarchy.textNode,
344 				'img': this.tagHierarchy.textNode,
345 				'b': this.tagHierarchy.b,
346 				'strong': this.tagHierarchy.b,
347 				'code': this.tagHierarchy.b,
348 				'q': this.tagHierarchy.b,
349 				'blockquote': this.tagHierarchy.blockquote,
350 				'cite': this.tagHierarchy.b,
351 				'i': this.tagHierarchy.b,
352 				'em': this.tagHierarchy.b,
353 				'sup': this.tagHierarchy.b,
354 				'sub': this.tagHierarchy.b,
355 				'span': this.tagHierarchy.b,
356 				'del': this.tagHierarchy.del,
357 				'ins': this.tagHierarchy.ins,
358 				'u': this.tagHierarchy.b,
359 				'p': this.tagHierarchy.b,
360 				'pre': this.tagHierarchy.pre,
361 				'a': this.tagHierarchy.b,
362 				'ul': this.tagHierarchy.ul,
363 				'ol': this.tagHierarchy.ol,
364 				'li': this.tagHierarchy.li,
365 				'td': this.tagHierarchy.li,
366 				'div': this.tagHierarchy.div,
367 				'h1': this.tagHierarchy.h1,
368 				'h2': this.tagHierarchy.h1,
369 				'h3': this.tagHierarchy.h1,
370 				'h4': this.tagHierarchy.h1,
371 				'h5': this.tagHierarchy.h1,
372 				'h6': this.tagHierarchy.h1,
373 				'table': this.tagHierarchy.table
374 			};
375 
376 			// When applying this elements to selection they will replace the assigned elements
377 			this.replacingElements = {
378 				'h1': {
379 					'p': true,
380 					'h1': true,
381 					'h2': true,
382 					'h3': true,
383 					'h4': true,
384 					'h5': true,
385 					'h6': true,
386 					'pre': true,
387 					'blockquote': true
388 				}
389 			};
390 			this.replacingElements = {
391 				'h1': this.replacingElements.h1,
392 				'h2': this.replacingElements.h1,
393 				'h3': this.replacingElements.h1,
394 				'h4': this.replacingElements.h1,
395 				'h5': this.replacingElements.h1,
396 				'h6': this.replacingElements.h1,
397 				'pre': this.replacingElements.h1,
398 				'p': this.replacingElements.h1,
399 				'blockquote': this.replacingElements.h1
400 			};
401 			this.allowedToStealElements = {
402 				'h1': {
403 					'textNode': true
404 				}
405 			};
406 			this.allowedToStealElements = {
407 				'h1': this.allowedToStealElements.h1,
408 				'h2': this.allowedToStealElements.h1,
409 				'h3': this.allowedToStealElements.h1,
410 				'h4': this.allowedToStealElements.h1,
411 				'h5': this.allowedToStealElements.h1,
412 				'h6': this.allowedToStealElements.h1,
413 				'p': this.tagHierarchy.b
414 			};
415 		},
416 
417 		/**
418 		 * Class definition of a SelectionTree (relevant for all formatting / markup changes)
419 		 * TODO: remove this (was moved to range.js)
420 		 * Structure:
421 		 * +
422 		 * |-domobj: <reference to the DOM Object> (NOT jQuery)
423 		 * |-selection: defines if this node is marked by user [none|partial|full]
424 		 * |-children: recursive structure like this
425 		 * @hide
426 		 */
427 		SelectionTree: function () {
428 			this.domobj = {};
429 			this.selection = undefined;
430 			this.children = [];
431 		},
432 
433 		/**
434 		 * INFO: Method is used for integration with Gentics Aloha, has no use otherwise
435 		 * Updates the rangeObject according to the current user selection
436 		 * Method is always called on selection change
437 		 * @param objectClicked Object that triggered the selectionChange event
438 		 * @return true when rangeObject was modified, false otherwise
439 		 * @hide
440 		 */
441 		onChange: function (objectClicked, event, timeout, editableChanged) {
442 			if (this.updateSelectionTimeout) {
443 				window.clearTimeout(this.updateSelectionTimeout);
444 			}
445 
446 			// We have to update the selection in a timeout due to an IE
447 			// bug that is is caused by selecting some text and then
448 			// clicking once inside the selection (which collapses the
449 			// selection inside the previous selection).
450 			var selection = this;
451 			this.updateSelectionTimeout = window.setTimeout(function () {
452 				var range = new Aloha.Selection.SelectionRange(true);
453 				// We have to work around an IE bug that causes the user
454 				// selection to be incorrectly set on the body element
455 				// when the updateSelectionTimeout triggers. The
456 				// selection corrects itself after waiting a while.
457 				if (!range.startContainer || 'HTML' === range.startContainer.nodeName || 'BODY' === range.startContainer.nodeName) {
458 					if (!this.updateSelectionTimeout) {
459 						// First wait 5 millis, then 20 millis, 50 millis, 110 millis etc.
460 						selection.onChange(objectClicked, event, 10 + (timeout || 5) * 2);
461 					}
462 					return;
463 				} else {
464 					// And yet another IE workaround. Somehow the caret is not
465 					// positioned inside the clicked editable. This occures only
466 					// when switching editables in IE. In those cases the caret is
467 					// invisible. I tried to trace the origin of the issue but i
468 					// could not find the place where the caret is mispositioned.
469 					// I noticed that IE is sometimes adding drag handles to
470 					// editables. Aloha is removing those handles.
471 					// If those handles are visible it apears that two clicks are needed
472 					// to activate the editable. The first click is to select the
473 					// editable and the second to enable it and activeate it. I added a
474 					// range select call that will cirumvent this issue by resetting
475 					// the selection. I also checked the range object. In all cases
476 					// i found the range object contained correct properties. The
477 					// workaround will only be applied for IE.
478 					if (jQuery.browser.msie && editableChanged) {
479 						range.select();
480 					}
481 				}
482 				Aloha.Selection._updateSelection(event, range);
483 			}, timeout || 5);
484 		},
485 
486 		/**
487 		 * prevents the next aloha-selection-changed event from being triggered
488 		 */
489 		preventSelectionChanged: function () {
490 			this.preventSelectionChangedFlag = true;
491 		},
492 
493 		/**
494 		 * will return wheter selection change event was prevented or not, and reset the preventSelectionChangedFlag
495 		 * @return {Boolean} true if aloha-selection-change event was prevented
496 		 */
497 		isSelectionChangedPrevented: function () {
498 			var prevented = this.preventSelectionChangedFlag;
499 			this.preventSelectionChangedFlag = false;
500 			return prevented;
501 		},
502 
503 		/**
504 		 * Checks if the current rangeObject common ancector container is edtiable
505 		 * @return {Boolean} true if current common ancestor is editable
506 		 */
507 		isSelectionEditable: function () {
508 			return (this.rangeObject.commonAncestorContainer && jQuery(this.rangeObject.commonAncestorContainer).contentEditable());
509 		},
510 
511 		/**
512 		 * This method checks, if the current rangeObject common ancestor container has a 'data-aloha-floatingmenu-visible' Attribute.
513 		 * Needed in Floating Menu for exceptional display of floatingmenu.
514 		 */
515 		isFloatingMenuVisible: function () {
516 			var visible = jQuery(Aloha.Selection.rangeObject.commonAncestorContainer).attr('data-aloha-floatingmenu-visible');
517 			if (visible !== 'undefined') {
518 				if (visible === 'true') {
519 					return true;
520 				}
521 				return false;
522 			}
523 			return false;
524 		},
525 
526 		/**
527 		 * INFO: Method is used for integration with Gentics Aloha, has no use otherwise
528 		 * Updates the rangeObject according to the current user selection
529 		 * Method is always called on selection change
530 		 * @param event jQuery browser event object
531 		 * @return true when rangeObject was modified, false otherwise
532 		 * @hide
533 		 */
534 		updateSelection: function (event) {
535 			return this._updateSelection(event, null);
536 		},
537 
538 		/**
539 		 * Internal version of updateSelection that adds the range parameter to be
540 		 * able to work around an IE bug that caused the current user selection
541 		 * sometimes to be on the body element.
542 		 * @param {Object} event
543 		 * @param {Object} range a substitute for the current user selection. if not provided,
544 		 *   the current user selection will be used.
545 		 * @hide
546 		 */
547 		_updateSelection: function (event, range) {
548 			if (event && event.originalEvent &&
549 					true === event.originalEvent.stopSelectionUpdate) {
550 				return false;
551 			}
552 
553 			if (typeof range === 'undefined') {
554 				return false;
555 			}
556 
557 			this.rangeObject = range =
558 					range || new Aloha.Selection.SelectionRange(true);
559 
560 			// workaround for FF selection bug, where it is possible to move the selection INTO a hr
561 			if (range && range.startContainer
562 					&& 'HR' === range.startContainer.nodeName
563 					&& range.endContainer
564 					&& 'HR' === range.endContainer.nodeName) {
565 				Aloha.getSelection().removeAllRanges();
566 				return true;
567 			}
568 
569 			// Determine the common ancestor container and update the selection
570 			// tree.
571 			range.update();
572 
573 			// Workaround for nasty IE bug that allows the user to select
574 			// text nodes inside areas with contenteditable "false"
575 			if (range && range.startContainer && range.endContainer) {
576 				var inEditable =
577 						jQuery(range.commonAncestorContainer)
578 							.closest('.aloha-editable').length > 0;
579 
580 				if (inEditable) {
581 					var validStartPosition = this._validEditablePosition(range.startContainer);
582 					var validEndPosition = this._validEditablePosition(range.endContainer);
583 					var newPos;
584 					// when we are moving down (with the cursor down key), we want to position the
585 					// cursor AFTER the non-editable area
586 					// otherwise BEFORE the non-editable area
587 					var movingDown = event && (event.keyCode === 40);
588 
589 					if (!validStartPosition) {
590 						newPos = this._getNearestEditablePosition(range.startContainer, movingDown);
591 						if (newPos) {
592 							range.startContainer = newPos.container;
593 							range.startOffset = newPos.offset;
594 						}
595 					}
596 					if (!validEndPosition) {
597 						newPos = this._getNearestEditablePosition(range.endContainer, movingDown);
598 						if (newPos) {
599 							range.endContainer = newPos.container;
600 							range.endOffset = newPos.offset;
601 						}
602 					}
603 					if (!validStartPosition || !validEndPosition) {
604 						range.correctRange();
605 						range.select();
606 					}
607 				}
608 			}
609 
610 			// check if aloha-selection-changed event has been prevented
611 			if (this.isSelectionChangedPrevented()) {
612 				return true;
613 			}
614 
615 			Aloha.trigger('aloha-selection-changed-before', [this.rangeObject, event]);
616 
617 			// throw the event that the selection has changed. Plugins now have the
618 			// chance to react on the currentElements[childCount].children.lengthged selection
619 			Aloha.trigger('aloha-selection-changed', [this.rangeObject, event]);
620 
621 			triggerSelectionContextChanged(this.rangeObject, event);
622 
623 			Aloha.trigger('aloha-selection-changed-after', [this.rangeObject, event]);
624 
625 			return true;
626 		},
627 
628 		/**
629 		 * Check whether a position with the given node as container is a valid editable position
630 		 * @param {DOMObject} node DOM node
631 		 * @return true if the position is editable, false if not
632 		 */
633 		_validEditablePosition: function (node) {
634 			if (!node) {
635 				return false;
636 			}
637 			switch (node.nodeType) {
638 			case 1:
639 				return jQuery(node).contentEditable();
640 			case 3:
641 				return jQuery(node.parentNode).contentEditable();
642 			default:
643 				return false;
644 			}
645 		},
646 
647 		/**
648 		 * Starting with the given node (which is supposed to be not editable)
649 		 * find the nearest editable position
650 		 * 
651 		 * @param {DOMObject} node DOM node
652 		 * @param {Boolean} forward true for searching forward, false for searching backward
653 		 */
654 		_getNearestEditablePosition: function (node, forward) {
655 			var current = node;
656 			var parent = current.parentNode;
657 			while (parent !== null && !jQuery(parent).contentEditable()) {
658 				current = parent;
659 				parent = parent.parentNode;
660 			}
661 			if (current === null) {
662 				return false;
663 			}
664 			if (forward) {
665 				// check whether the element after the non editable element is editable and a blocklevel element
666 				if (Dom.isBlockLevelElement(current.nextSibling) && jQuery(current.nextSibling).contentEditable()) {
667 					return {
668 						container: current.nextSibling,
669 						offset: 0
670 					};
671 				} else {
672 					return {
673 						container: parent,
674 						offset: Dom.getIndexInParent(current) + 1
675 					};
676 				}
677 			} else {
678 				// check whether the element before the non editable element is editable and a blocklevel element
679 				if (Dom.isBlockLevelElement(current.previousSibling) && jQuery(current.previousSibling).contentEditable()) {
680 					return {
681 						container: current.previousSibling,
682 						offset: current.previousSibling.childNodes.length
683 					};
684 				} else {
685 					return {
686 						container: parent,
687 						offset: Dom.getIndexInParent(current)
688 					};
689 				}
690 			}
691 		},
692 
693 		/**
694 		 * creates an object with x items containing all relevant dom objects.
695 		 * Structure:
696 		 * +
697 		 * |-domobj: <reference to the DOM Object> (NOT jQuery)
698 		 * |-selection: defines if this node is marked by user [none|partial|full]
699 		 * |-children: recursive structure like this ("x.." because it's then shown last in DOM Browsers...)
700 		 * TODO: remove this (was moved to range.js)
701 		 *
702 		 * @param rangeObject "Aloha clean" range object including a commonAncestorContainer
703 		 * @return obj selection
704 		 * @hide
705 		 */
706 		getSelectionTree: function (rangeObject) {
707 			if (!rangeObject) { // if called without any parameters, the method acts as getter for this.selectionTree
708 				return this.rangeObject.getSelectionTree();
709 			}
710 			if (!rangeObject.commonAncestorContainer) {
711 				Aloha.Log.error(this, 'the rangeObject is missing the commonAncestorContainer');
712 				return false;
713 			}
714 
715 			this.inselection = false;
716 
717 			// before getting the selection tree, we do a cleanup
718 			if (GENTICS.Utils.Dom.doCleanup({ 'merge': true }, rangeObject)) {
719 				rangeObject.update();
720 				rangeObject.select();
721 			}
722 
723 			return this.recursiveGetSelectionTree(rangeObject, rangeObject.commonAncestorContainer);
724 		},
725 
726 		/**
727 		 * Recursive inner function for generating the selection tree.
728 		 * TODO: remove this (was moved to range.js)
729 		 * @param rangeObject range object
730 		 * @param currentObject current DOM object for which the selection tree shall be generated
731 		 * @return array of SelectionTree objects for the children of the current DOM object
732 		 * @hide
733 		 */
734 		recursiveGetSelectionTree: function (rangeObject, currentObject) {
735 			// get all direct children of the given object
736 			var jQueryCurrentObject = jQuery(currentObject),
737 				childCount = 0,
738 				that = this,
739 				currentElements = [];
740 
741 			jQueryCurrentObject.contents().each(function (index) {
742 				var selectionType = 'none',
743 					startOffset = false,
744 					endOffset = false,
745 					collapsedFound = false,
746 					i,
747 				    elementsLength,
748 					noneFound = false,
749 					partialFound = false,
750 					fullFound = false;
751 
752 				// check for collapsed selections between nodes
753 				if (rangeObject.isCollapsed() && currentObject === rangeObject.startContainer && rangeObject.startOffset == index) {
754 					// insert an extra selectiontree object for the collapsed selection here
755 					currentElements[childCount] = new Aloha.Selection.SelectionTree();
756 					currentElements[childCount].selection = 'collapsed';
757 					currentElements[childCount].domobj = undefined;
758 					that.inselection = false;
759 					collapsedFound = true;
760 					childCount++;
761 				}
762 
763 				if (!that.inselection && !collapsedFound) {
764 					// the start of the selection was not yet found, so look for it now
765 					// check whether the start of the selection is found here
766 
767 					// Try to read the nodeType property and return if we do not have permission
768 					// ie.: frame document to an external URL
769 					var nodeType;
770 					try {
771 						nodeType = this.nodeType;
772 773 					} catch (e) {
774 						return;
775 					}
776 
777 					// check is dependent on the node type
778 					switch (nodeType) {
779 					case 3:
780 						// text node
781 						if (this === rangeObject.startContainer) {
782 							// the selection starts here
783 							that.inselection = true;
784 
785 							// when the startoffset is > 0, the selection type is only partial
786 							selectionType = rangeObject.startOffset > 0 ? 'partial' : 'full';
787 							startOffset = rangeObject.startOffset;
788 							endOffset = this.length;
789 						}
790 						break;
791 					case 1:
792 						// element node
793 						if (this === rangeObject.startContainer && rangeObject.startOffset === 0) {
794 							// the selection starts here
795 							that.inselection = true;
796 							selectionType = 'full';
797 						}
798 						if (currentObject === rangeObject.startContainer && rangeObject.startOffset === index) {
799 							// the selection starts here
800 							that.inselection = true;
801 							selectionType = 'full';
802 						}
803 						break;
804 					}
805 				}
806 
807 				if (that.inselection && !collapsedFound) {
808 					if (selectionType == 'none') {
809 						selectionType = 'full';
810 					}
811 					// we already found the start of the selection, so look for the end of the selection now
812 					// check whether the end of the selection is found here
813 
814 					switch (this.nodeType) {
815 					case 3:
816 						// text node
817 						if (this === rangeObject.endContainer) {
818 							// the selection ends here
819 							that.inselection = false;
820 
821 							// check for partial selection here
822 							if (rangeObject.endOffset < this.length) {
823 								selectionType = 'partial';
824 							}
825 							if (startOffset === false) {
826 								startOffset = 0;
827 							}
828 							endOffset = rangeObject.endOffset;
829 						}
830 						break;
831 					case 1:
832 						// element node
833 						if (this === rangeObject.endContainer && rangeObject.endOffset === 0) {
834 							that.inselection = false;
835 						}
836 						break;
837 					}
838 					if (currentObject === rangeObject.endContainer && rangeObject.endOffset <= index) {
839 						that.inselection = false;
840 						selectionType = 'none';
841 					}
842 843 				}
844 
845 				// create the current selection tree entry
846 				currentElements[childCount] = new Aloha.Selection.SelectionTree();
847 				currentElements[childCount].domobj = this;
848 				currentElements[childCount].selection = selectionType;
849 				if (selectionType == 'partial') {
850 					currentElements[childCount].startOffset = startOffset;
851 					currentElements[childCount].endOffset = endOffset;
852 				}
853 
854 				// now do the recursion step into the current object
855 				currentElements[childCount].children = that.recursiveGetSelectionTree(rangeObject, this);
856 				elementsLength = currentElements[childCount].children.length;
857 
858 				// check whether a selection was found within the children
859 				if (elementsLength > 0) {
860 					for (i = 0; i < elementsLength; ++i) {
861 						switch (currentElements[childCount].children[i].selection) {
862 						case 'none':
863 							noneFound = true;
864 							break;
865 						case 'full':
866 							fullFound = true;
867 							break;
868 						case 'partial':
869 							partialFound = true;
870 							break;
871 						}
872 					}
873 
874 					if (partialFound || (fullFound && noneFound)) {
875 						// found at least one 'partial' selection in the children, or both 'full' and 'none', so this element is also 'partial' selected
876 						currentElements[childCount].selection = 'partial';
877 					} else if (fullFound && !partialFound && !noneFound) {
878 						// only found 'full' selected children, so this element is also 'full' selected
879 						currentElements[childCount].selection = 'full';
880 					}
881 				}
882 
883 				childCount++;
884 			});
885 
886 			// extra check for collapsed selections at the end of the current element
887 			if (rangeObject.isCollapsed() && currentObject === rangeObject.startContainer && rangeObject.startOffset == currentObject.childNodes.length) {
888 				currentElements[childCount] = new Aloha.Selection.SelectionTree();
889 				currentElements[childCount].selection = 'collapsed';
890 				currentElements[childCount].domobj = undefined;
891 			}
892 
893 			return currentElements;
894 		},
895 
896 		/**
897 		 * Get the currently selected range
898 		 * @return {Aloha.Selection.SelectionRange} currently selected range
899 		 * @method
900 		 */
901 		getRangeObject: function () {
902 903 			return this.rangeObject;
904 		},
905 
906 		/**
907 		 * method finds out, if a node is within a certain markup or not
908 		 * @param rangeObj Aloha rangeObject
909 		 * @param startOrEnd boolean; defines, if start or endContainer should be used: false for start, true for end
910 		 * @param markupObject jQuery object of the markup to look for
911 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
912 		 * @param limitObject dom object which limits the search are within the dom. normally this will be the active Editable
913 		 * @return true, if the markup is effective on the range objects start or end node
914 		 * @hide
915 		 */
916 		isRangeObjectWithinMarkup: function (rangeObject, startOrEnd, markupObject, tagComparator, limitObject) {
917 			var domObj = !startOrEnd ? rangeObject.startContainer : rangeObject.endContainer,
918 				that = this,
919 				parents = jQuery(domObj).parents(),
920 				returnVal = false,
921 				i = -1;
922 
923 			// check if a comparison method was passed as parameter ...
924 			if (typeof tagComparator !== 'undefined' && typeof tagComparator !== 'function') {
925 				Aloha.Log.error(this, 'parameter tagComparator is not a function');
926 			}
927 			// ... if not use this as standard tag comparison method
928 			if (typeof tagComparator === 'undefined') {
929 				tagComparator = function (domobj, markupObject) {
930 					return that.standardTextLevelSemanticsComparator(domobj, markupObject); // TODO should actually be this.getStandardTagComparator(markupObject)
931 				};
932 			}
933 
934 			if (parents.length > 0) {
935 				parents.each(function () {
936 					// the limit object was reached (normally the Editable Element)
937 					if (this === limitObject) {
938 						Aloha.Log.debug(that, 'reached limit dom obj');
939 						return false; // break() of jQuery .each(); THIS IS NOT THE FUNCTION RETURN VALUE
940 					}
941 					if (tagComparator(this, markupObject)) {
942 						if (returnVal === false) {
943 							returnVal = [];
944 						}
945 						Aloha.Log.debug(that, 'reached object equal to markup');
946 						i++;
947 						returnVal[i] = this;
948 						return true; // continue() of jQuery .each(); THIS IS NOT THE FUNCTION RETURN VALUE
949 					}
950 				});
951 			}
952 			return returnVal;
953 		},
954 
955 		/**
956 		 * standard method, to compare a domobj and a jquery object for sections and grouping content (e.g. p, h1, h2, ul, ....).
957 		 * is always used when no other tag comparator is passed as parameter
958 		 * @param domobj domobject to compare with markup
959 		 * @param markupObject jQuery object of the markup to compare with domobj
960 		 * @return true if objects are equal and false if not
961 		 * @hide
962 		 */
963 		standardSectionsAndGroupingContentComparator: function (domobj, markupObject) {
964 			if (domobj.nodeType !== 1) {
965 				Aloha.Log.debug(this, 'only element nodes (nodeType == 1) can be compared');
966 				return false;
967 			}
968 			if (!markupObject[0].nodeName) {
969 				return false;
970 			}
971 			var elemMap = Aloha.Selection.replacingElements[domobj.nodeName.toLowerCase()];
972 			return elemMap && elemMap[markupObject[0].nodeName.toLowerCase()];
973 		},
974 
975 		/**
976 		 * standard method, to compare a domobj and a jquery object for their tagName (aka span elements, e.g. b, i, sup, span, ...).
977 		 * is always used when no other tag comparator is passed as parameter
978 		 * @param domobj domobject to compare with markup
979 		 * @param markupObject jQuery object of the markup to compare with domobj
980 		 * @return true if objects are equal and false if not
981 		 * @hide
982 		 */
983 		standardTagNameComparator: function (domobj, markupObject) {
984 			if (domobj.nodeType === 1) {
985 				if (domobj.nodeName != markupObject[0].nodeName) {
986 					return false;
987 				}
988 				return true;
989 			}
990 			Aloha.Log.debug(this, 'only element nodes (nodeType == 1) can be compared');
991 			return false;
992 		},
993 
994 		/**
995 		 * standard method, to compare a domobj and a jquery object for text level semantics (aka span elements, e.g. b, i, sup, span, ...).
996 		 * is always used when no other tag comparator is passed as parameter
997 		 * @param domobj domobject to compare with markup
998 		 * @param markupObject jQuery object of the markup to compare with domobj
999 		 * @return true if objects are equal and false if not
1000 		 * @hide
1001 		 */
1002 		standardTextLevelSemanticsComparator: function (domobj, markupObject) {
1003 			// only element nodes can be compared
1004 			if (domobj.nodeType === 1) {
1005 				if (domobj.nodeName != markupObject[0].nodeName) {
1006 					return false;
1007 				}
1008 				if (!this.standardAttributesComparator(domobj, markupObject)) {
1009 					return false;
1010 				}
1011 				return true;
1012 			}
1013 			Aloha.Log.debug(this, 'only element nodes (nodeType == 1) can be compared');
1014 			return false;
1015 		},
1016 
1017 
1018 		/**
1019 		 * standard method, to compare attributes of one dom obj and one markup obj (jQuery)
1020 		 * @param domobj domobject to compare with markup
1021 		 * @param markupObject jQuery object of the markup to compare with domobj
1022 		 * @return true if objects are equal and false if not
1023 		 * @hide
1024 		 */
1025 		standardAttributesComparator: function (domobj, markupObject) {
1026 			var classesA = Strings.words((domobj && domobj.className) || '');
1027 			var classesB = Strings.words((markupObject.length && markupObject[0].className) || '');
1028 			Arrays.sortUnique(classesA);
1029 			Arrays.sortUnique(classesB);
1030 			return Arrays.equal(classesA, classesB);
1031 		},
1032 
1033 		/**
1034 		 * method finds out, if a node is within a certain markup or not
1035 		 * @param rangeObj Aloha rangeObject
1036 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1037 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1038 		 * @return void; TODO: should return true if the markup applied successfully and false if not
1039 		 * @hide
1040 		 */
1041 		changeMarkup: function (rangeObject, markupObject, tagComparator) {
1042 			var tagName = markupObject[0].tagName.toLowerCase(),
1043 				newCAC,
1044 			    limitObject,
1045 				backupRangeObject,
1046 				relevantMarkupObjectsAtSelectionStart = this.isRangeObjectWithinMarkup(rangeObject, false, markupObject, tagComparator, limitObject),
1047 				relevantMarkupObjectsAtSelectionEnd = this.isRangeObjectWithinMarkup(rangeObject, true, markupObject, tagComparator, limitObject),
1048 				nextSibling,
1049 			    relevantMarkupObjectAfterSelection,
1050 				prevSibling,
1051 			    relevantMarkupObjectBeforeSelection,
1052 				extendedRangeObject;
1053 			var parentElement;
1054 
1055 			// if the element is a replacing element (like p/h1/h2/h3/h4/h5/h6...), which must not wrap each other
1056 			// use a clone of rangeObject
1057 			if (this.replacingElements[tagName]) {
1058 				// backup rangeObject for later selection;
1059 				backupRangeObject = rangeObject;
1060 
1061 				// create a new range object to not modify the orginal
1062 				rangeObject = new this.SelectionRange(rangeObject);
1063 
1064 				// either select the active Editable as new commonAncestorContainer (CAC) or use the body
1065 				if (Aloha.activeEditable) {
1066 					newCAC = Aloha.activeEditable.obj.get(0);
1067 				} else {
1068 					newCAC = jQuery('body');
1069 				}
1070 				// update rangeObject by setting the newCAC and automatically recalculating the selectionTree
1071 				rangeObject.update(newCAC);
1072 
1073 				// store the information, that the markupObject can be replaced (not must be!!) inside the jQuery markup object
1074 				markupObject.isReplacingElement = true;
1075 			} else {
1076 				// if the element is NOT a replacing element, then something needs to be selected, otherwise it can not be wrapped
1077 				// therefor the method can return false, if nothing is selected ( = rangeObject is collapsed)
1078 				if (rangeObject.isCollapsed()) {
1079 					Aloha.Log.debug(this, 'early returning from applying markup because nothing is currently selected');
1080 					return false;
1081 				}
1082 			}
1083 
1084 			// is Start/End DOM Obj inside the markup to change
1085 			if (Aloha.activeEditable) {
1086 				limitObject = Aloha.activeEditable.obj[0];
1087 			} else {
1088 				limitObject = jQuery('body');
1089 			}
1090 
1091 			if (!markupObject.isReplacingElement && rangeObject.startOffset === 0) { // don't care about replacers, because they never extend
1092 				if (null != (prevSibling = this.getTextNodeSibling(false, rangeObject.commonAncestorContainer.parentNode, rangeObject.startContainer))) {
1093 					relevantMarkupObjectBeforeSelection = this.isRangeObjectWithinMarkup({
1094 						startContainer: prevSibling,
1095 						startOffset: 0
1096 					}, false, markupObject, tagComparator, limitObject);
1097 				}
1098 			}
1099 			if (!markupObject.isReplacingElement && (rangeObject.endOffset === rangeObject.endContainer.length)) { // don't care about replacers, because they never extend
1100 				if (null != (nextSibling = this.getTextNodeSibling(true, rangeObject.commonAncestorContainer.parentNode, rangeObject.endContainer))) {
1101 					relevantMarkupObjectAfterSelection = this.isRangeObjectWithinMarkup({
1102 						startContainer: nextSibling,
1103 						startOffset: 0
1104 					}, false, markupObject, tagComparator, limitObject);
1105 				}
1106 			}
1107 
1108 			// decide what to do (expand or reduce markup)
1109 			// Alternative A: from markup to no-markup: markup will be removed in selection;
1110 			// reapplied from original markup start to selection start
1111 			if (!markupObject.isReplacingElement && (relevantMarkupObjectsAtSelectionStart && !relevantMarkupObjectsAtSelectionEnd)) {
1112 				Aloha.Log.info(this, 'markup 2 non-markup');
1113 				this.prepareForRemoval(rangeObject.getSelectionTree(), markupObject, tagComparator);
1114 				jQuery(relevantMarkupObjectsAtSelectionStart).addClass('preparedForRemoval');
1115 				this.insertCroppedMarkups(relevantMarkupObjectsAtSelectionStart, rangeObject, false, tagComparator);
1116 			} else if (!markupObject.isReplacingElement && relevantMarkupObjectsAtSelectionStart && relevantMarkupObjectsAtSelectionEnd) {
1117 				// Alternative B: from markup to markup:
1118 				// remove selected markup (=split existing markup if single, shrink if two different)
1119 				Aloha.Log.info(this, 'markup 2 markup');
1120 				this.prepareForRemoval(rangeObject.getSelectionTree(), markupObject, tagComparator);
1121 				this.splitRelevantMarkupObject(relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject, tagComparator);
1122 			} else if (!markupObject.isReplacingElement && ((!relevantMarkupObjectsAtSelectionStart && relevantMarkupObjectsAtSelectionEnd) || relevantMarkupObjectAfterSelection || relevantMarkupObjectBeforeSelection)) { //
1123 				// Alternative C: from no-markup to markup OR with next2markup:
1124 				// new markup is wrapped from selection start to end of originalmarkup, original is remove afterwards
1125 				Aloha.Log.info(this, 'non-markup 2 markup OR with next2markup');
1126 				// move end of rangeObject to end of relevant markups
1127 				if (relevantMarkupObjectBeforeSelection && relevantMarkupObjectAfterSelection) {
1128 					extendedRangeObject = new Aloha.Selection.SelectionRange(rangeObject);
1129 					extendedRangeObject.startContainer = jQuery(relevantMarkupObjectBeforeSelection[relevantMarkupObjectBeforeSelection.length - 1]).textNodes()[0];
1130 					extendedRangeObject.startOffset = 0;
1131 					extendedRangeObject.endContainer = jQuery(relevantMarkupObjectAfterSelection[relevantMarkupObjectAfterSelection.length - 1]).textNodes().last()[0];
1132 					extendedRangeObject.endOffset = extendedRangeObject.endContainer.length;
1133 					extendedRangeObject.update();
1134 					this.applyMarkup(extendedRangeObject.getSelectionTree(), rangeObject, markupObject, tagComparator);
1135 					Aloha.Log.info(this, 'double extending previous markup(previous and after selection), actually wrapping it ...');
1136 
1137 				} else if (relevantMarkupObjectBeforeSelection && !relevantMarkupObjectAfterSelection && !relevantMarkupObjectsAtSelectionEnd) {
1138 					this.extendExistingMarkupWithSelection(relevantMarkupObjectBeforeSelection, rangeObject, false, tagComparator);
1139 					Aloha.Log.info(this, 'extending previous markup');
1140 
1141 				} else if (relevantMarkupObjectBeforeSelection && !relevantMarkupObjectAfterSelection && relevantMarkupObjectsAtSelectionEnd) {
1142 					extendedRangeObject = new Aloha.Selection.SelectionRange(rangeObject);
1143 					extendedRangeObject.startContainer = jQuery(relevantMarkupObjectBeforeSelection[relevantMarkupObjectBeforeSelection.length - 1]).textNodes()[0];
1144 					extendedRangeObject.startOffset = 0;
1145 					extendedRangeObject.endContainer = jQuery(relevantMarkupObjectsAtSelectionEnd[relevantMarkupObjectsAtSelectionEnd.length - 1]).textNodes().last()[0];
1146 					extendedRangeObject.endOffset = extendedRangeObject.endContainer.length;
1147 					extendedRangeObject.update();
1148 					this.applyMarkup(extendedRangeObject.getSelectionTree(), rangeObject, markupObject, tagComparator);
1149 					Aloha.Log.info(this, 'double extending previous markup(previous and relevant at the end), actually wrapping it ...');
1150 
1151 				} else if (!relevantMarkupObjectBeforeSelection && relevantMarkupObjectAfterSelection) {
1152 					this.extendExistingMarkupWithSelection(relevantMarkupObjectAfterSelection, rangeObject, true, tagComparator);
1153 					Aloha.Log.info(this, 'extending following markup backwards');
1154 
1155 				} else {
1156 					this.extendExistingMarkupWithSelection(relevantMarkupObjectsAtSelectionEnd, rangeObject, true, tagComparator);
1157 				}
1158 			} else if (markupObject.isReplacingElement || (!relevantMarkupObjectsAtSelectionStart && !relevantMarkupObjectsAtSelectionEnd && !relevantMarkupObjectBeforeSelection && !relevantMarkupObjectAfterSelection)) {
1159 				// Alternative D: no-markup to no-markup: easy
1160 				Aloha.Log.info(this, 'non-markup 2 non-markup');
1161 
1162 				// workaround to keep the caret at the right position if it's an empty element
1163 				// applyMarkup was not working correctly and has a lot of overhead we don't need in that case
1164 				if (isCollapsedAndEmptyOrEndBr(rangeObject)) {
1165 					var newMarkup = markupObject.clone();
1166 
1167 					if (isCollapsedAndEndBr(rangeObject)) {
1168 						newMarkup[0].appendChild(Engine.createEndBreak());
1169 					}
1170 
1171 					// setting the focus is needed for mozilla and IE 7 to have a working rangeObject.select()
1172 					if (Aloha.activeEditable && jQuery.browser.mozilla) {
1173 						Aloha.activeEditable.obj.focus();
1174 					}
1175 
1176 					if (Engine.isEditable(rangeObject.startContainer)) {
1177 						Engine.copyAttributes(rangeObject.startContainer, newMarkup[0]);
1178 						jQuery(rangeObject.startContainer).after(newMarkup[0]).remove();
1179 					} else if (Engine.isEditingHost(rangeObject.startContainer)) {
1180 						jQuery(rangeObject.startContainer).append(newMarkup[0]);
1181 						Engine.ensureContainerEditable(newMarkup[0]);
1182 					}
1183 
1184 					backupRangeObject.startContainer = newMarkup[0];
1185 					backupRangeObject.endContainer = newMarkup[0];
1186 					backupRangeObject.startOffset = 0;
1187 					backupRangeObject.endOffset = 0;
1188 					return;
1189 				}
1190 				this.applyMarkup(rangeObject.getSelectionTree(), rangeObject, markupObject, tagComparator, {
1191 					setRangeObject2NewMarkup: true
1192 				});
1193 				backupRangeObject.startContainer = rangeObject.startContainer;
1194 				backupRangeObject.endContainer = rangeObject.endContainer;
1195 				backupRangeObject.startOffset = rangeObject.startOffset;
1196 				backupRangeObject.endOffset = rangeObject.endOffset;
1197 			}
1198 
1199 			if (markupObject.isReplacingElement) {
1200 				//Check if the startContainer is one of the zapped elements
1201 				if (backupRangeObject && backupRangeObject.startContainer.className && backupRangeObject.startContainer.className.indexOf('preparedForRemoval') > -1) {
1202 					//var parentElement = jQuery(backupRangeObject.startContainer).closest(markupObject[0].tagName).get(0);
1203 					parentElement = jQuery(backupRangeObject.startContainer).parents(markupObject[0].tagName).get(0);
1204 					backupRangeObject.startContainer = parentElement;
1205 					rangeObject.startContainer = parentElement;
1206 				}
1207 				//check if the endContainer is one of the zapped elements
1208 				if (backupRangeObject && backupRangeObject.endContainer.className && backupRangeObject.endContainer.className.indexOf('preparedForRemoval') > -1) {
1209 					//var parentElement = jQuery(backupRangeObject.endContainer).closest(markupObject[0].tagName).get(0);
1210 					parentElement = jQuery(backupRangeObject.endContainer).parents(markupObject[0].tagName).get(0);
1211 					backupRangeObject.endContainer = parentElement;
1212 					rangeObject.endContainer = parentElement;
1213 				}
1214 			}
1215 			// remove all marked items
1216 			jQuery('.preparedForRemoval').zap();
1217 
1218 			// recalculate cac and selectionTree
1219 
1220 			// update selection
1221 			if (markupObject.isReplacingElement) {
1222 				//After the zapping we have to check for wrong offsets
1223 				if (e5s.Node.ELEMENT_NODE === backupRangeObject.startContainer.nodeType && backupRangeObject.startContainer.childNodes && backupRangeObject.startContainer.childNodes.length < backupRangeObject.startOffset) {
1224 					backupRangeObject.startOffset = backupRangeObject.startContainer.childNodes.length;
1225 					rangeObject.startOffset = backupRangeObject.startContainer.childNodes.length;
1226 				}
1227 				if (e5s.Node.ELEMENT_NODE === backupRangeObject.endContainer.nodeType && backupRangeObject.endContainer.childNodes && backupRangeObject.endContainer.childNodes.length < backupRangeObject.endOffset) {
1228 					backupRangeObject.endOffset = backupRangeObject.endContainer.childNodes.length;
1229 					rangeObject.endOffset = backupRangeObject.endContainer.childNodes.length;
1230 				}
1231 				rangeObject.endContainer = backupRangeObject.endContainer;
1232 				rangeObject.endOffset = backupRangeObject.endOffset;
1233 				rangeObject.startContainer = backupRangeObject.startContainer;
1234 				rangeObject.startOffset = backupRangeObject.startOffset;
1235 				backupRangeObject.update();
1236 				backupRangeObject.select();
1237 			} else {
1238 				rangeObject.update();
1239 				rangeObject.select();
1240 			}
1241 		},
1242 
1243 		/**
1244 		 * method compares a JS array of domobjects with a range object and decides, if the rangeObject spans the whole markup objects. method is used to decide if a markup2markup selection can be completely remove or if it must be splitted into 2 separate markups
1245 		 * @param relevantMarkupObjectsAtSelectionStart JS Array of dom objects, which are parents to the rangeObject.startContainer
1246 		 * @param relevantMarkupObjectsAtSelectionEnd JS Array of dom objects, which are parents to the rangeObject.endContainer
1247 		 * @param rangeObj Aloha rangeObject
1248 		 * @return true, if rangeObjects and markup objects are identical, false otherwise
1249 		 * @hide
1250 		 */
1251 		areMarkupObjectsAsLongAsRangeObject: function (relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject) {
1252 			var i, el, textNode, relMarkupEnd, relMarkupStart;
1253 
1254 			if (rangeObject.startOffset !== 0) {
1255 				return false;
1256 			}
1257 
1258 			for (i = 0, relMarkupStart = relevantMarkupObjectsAtSelectionStart.length; i < relMarkupStart; i++) {
1259 				el = jQuery(relevantMarkupObjectsAtSelectionStart[i]);
1260 				if (el.textNodes().first()[0] !== rangeObject.startContainer) {
1261 					return false;
1262 				}
1263 			}
1264 
1265 			for (i = 0, relMarkupEnd = relevantMarkupObjectsAtSelectionEnd.length; i < relMarkupEnd; i++) {
1266 				el = jQuery(relevantMarkupObjectsAtSelectionEnd[i]);
1267 				textNode = el.textNodes().last()[0];
1268 				if (textNode !== rangeObject.endContainer || textNode.length != rangeObject.endOffset) {
1269 					return false;
1270 				}
1271 			}
1272 
1273 			return true;
1274 		},
1275 
1276 		/**
1277 		 * method used to remove/split markup from a "markup2markup" selection
1278 		 * @param relevantMarkupObjectsAtSelectionStart JS Array of dom objects, which are parents to the rangeObject.startContainer
1279 		 * @param relevantMarkupObjectsAtSelectionEnd JS Array of dom objects, which are parents to the rangeObject.endContainer
1280 		 * @param rangeObj Aloha rangeObject
1281 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1282 		 * @return true (always, since no "false" case is currently known...but might be added)
1283 		 * @hide
1284 		 */
1285 		splitRelevantMarkupObject: function (relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject, tagComparator) {
1286 			// mark them to be deleted
1287 			jQuery(relevantMarkupObjectsAtSelectionStart).addClass('preparedForRemoval');
1288 			jQuery(relevantMarkupObjectsAtSelectionEnd).addClass('preparedForRemoval');
1289 
1290 			// check if the rangeObject is identical with the relevantMarkupObjects (in this case the markup can simply be removed)
1291 			if (this.areMarkupObjectsAsLongAsRangeObject(relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd, rangeObject)) {
1292 				return true;
1293 			}
1294 
1295 			// find intersection (this can always only be one dom element (namely the highest) because all others will be removed
1296 			var relevantMarkupObjectAtSelectionStartAndEnd = this.intersectRelevantMarkupObjects(relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd);
1297 
1298 			if (relevantMarkupObjectAtSelectionStartAndEnd) {
1299 				this.insertCroppedMarkups([relevantMarkupObjectAtSelectionStartAndEnd], rangeObject, false, tagComparator);
1300 				this.insertCroppedMarkups([relevantMarkupObjectAtSelectionStartAndEnd], rangeObject, true, tagComparator);
1301 			} else {
1302 				this.insertCroppedMarkups(relevantMarkupObjectsAtSelectionStart, rangeObject, false, tagComparator);
1303 				this.insertCroppedMarkups(relevantMarkupObjectsAtSelectionEnd, rangeObject, true, tagComparator);
1304 			}
1305 			return true;
1306 		},
1307 
1308 		/**
1309 		 * method takes two arrays of bottom up dom objects, compares them and returns either the object closest to the root or false
1310 		 * @param relevantMarkupObjectsAtSelectionStart JS Array of dom objects
1311 		 * @param relevantMarkupObjectsAtSelectionEnd JS Array of dom objects
1312 		 * @return dom object closest to the root or false
1313 		 * @hide
1314 		 */
1315 		intersectRelevantMarkupObjects: function (relevantMarkupObjectsAtSelectionStart, relevantMarkupObjectsAtSelectionEnd) {
1316 			var intersection = false, i, elStart, j, elEnd, relMarkupStart, relMarkupEnd;
1317 			if (!relevantMarkupObjectsAtSelectionStart || !relevantMarkupObjectsAtSelectionEnd) {
1318 				return intersection; // we can only intersect, if we have to arrays!
1319 			}
1320 			relMarkupStart = relevantMarkupObjectsAtSelectionStart.length;
1321 			relMarkupEnd = relevantMarkupObjectsAtSelectionEnd.length;
1322 			for (i = 0; i < relMarkupStart; i++) {
1323 				elStart = relevantMarkupObjectsAtSelectionStart[i];
1324 				for (j = 0; j < relMarkupEnd; j++) {
1325 					elEnd = relevantMarkupObjectsAtSelectionEnd[j];
1326 					if (elStart === elEnd) {
1327 						intersection = elStart;
1328 					}
1329 				}
1330 			}
1331 			return intersection;
1332 		},
1333 
1334 		/**
1335 		 * method used to add markup to a nonmarkup2markup selection
1336 		 * @param relevantMarkupObjects JS Array of dom objects effecting either the start or endContainer of a selection (which should be extended)
1337 		 * @param rangeObject Aloha rangeObject the markups should be extended to
1338 		 * @param startOrEnd boolean; defines, if the existing markups should be extended forwards or backwards (is propably redundant and could be found out by comparing start or end container with the markup array dom objects)
1339 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1340 		 * @return true
1341 		 * @hide
1342 		 */
1343 		extendExistingMarkupWithSelection: function (relevantMarkupObjects, rangeObject, startOrEnd, tagComparator) {
1344 			var extendMarkupsAtStart, extendMarkupsAtEnd, objects, i, relMarkupLength, el, textnodes, nodeNr;
1345 			if (!startOrEnd) { // = Start
1346 				// start part of rangeObject should be used, therefor existing markups are cropped at the end
1347 				extendMarkupsAtStart = true;
1348 			}
1349 			if (startOrEnd) { // = End
1350 				// end part of rangeObject should be used, therefor existing markups are cropped at start (beginning)
1351 				extendMarkupsAtEnd = true;
1352 			}
1353 			objects = [];
1354 			for (i = 0, relMarkupLength = relevantMarkupObjects.length; i < relMarkupLength; i++) {
1355 				objects[i] = new this.SelectionRange();
1356 				el = relevantMarkupObjects[i];
1357 				if (extendMarkupsAtEnd && !extendMarkupsAtStart) {
1358 					objects[i].startContainer = rangeObject.startContainer; // jQuery(el).contents()[0];
1359 					objects[i].startOffset = rangeObject.startOffset;
1360 					textnodes = jQuery(el).textNodes(true);
1361 
1362 					nodeNr = textnodes.length - 1;
1363 					objects[i].endContainer = textnodes[nodeNr];
1364 					objects[i].endOffset = textnodes[nodeNr].length;
1365 					objects[i].update();
1366 					this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, {
1367 						setRangeObject2NewMarkup: true
1368 					});
1369 				}
1370 				if (!extendMarkupsAtEnd && extendMarkupsAtStart) {
1371 					textnodes = jQuery(el).textNodes(true);
1372 					objects[i].startContainer = textnodes[0]; // jQuery(el).contents()[0];
1373 					objects[i].startOffset = 0;
1374 					objects[i].endContainer = rangeObject.endContainer;
1375 					objects[i].endOffset = rangeObject.endOffset;
1376 					objects[i].update();
1377 					this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, {
1378 						setRangeObject2NewMarkup: true
1379 					});
1380 				}
1381 			}
1382 			return true;
1383 		},
1384 
1385 		/**
1386 		 * method creates an empty markup jQuery object from a dom object passed as paramter
1387 		 * @param domobj domobject to be cloned, cleaned and emptied
1388 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1389 		 * @return jQuery wrapper object to be passed to e.g. this.applyMarkup(...)
1390 		 * @hide
1391 		 */
1392 		getClonedMarkup4Wrapping: function (domobj) {
1393 			var wrapper = jQuery(domobj.outerHTML).removeClass('preparedForRemoval').empty();
1394 			if (wrapper.attr('class').length === 0) {
1395 				wrapper.removeAttr('class');
1396 			}
1397 			return wrapper;
1398 		},
1399 
1400 		/**
1401 		 * method used to subtract the range object from existing markup. in other words: certain markup is removed from the selections defined by the rangeObject
1402 		 * @param relevantMarkupObjects JS Array of dom objects effecting either the start or endContainer of a selection (which should be extended)
1403 		 * @param rangeObject Aloha rangeObject the markups should be removed from
1404 		 * @param startOrEnd boolean; defines, if the existing markups should be reduced at the beginning of the tag or at the end (is propably redundant and could be found out by comparing start or end container with the markup array dom objects)
1405 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1406 		 * @return true
1407 		 * @hide
1408 		 */
1409 		insertCroppedMarkups: function (relevantMarkupObjects, rangeObject, startOrEnd, tagComparator) {
1410 			var cropMarkupsAtEnd, cropMarkupsAtStart, textnodes, objects, i, el, textNodes;
1411 			if (!startOrEnd) { // = Start
1412 				// start part of rangeObject should be used, therefor existing markups are cropped at the end
1413 				cropMarkupsAtEnd = true;
1414 			} else { // = End
1415 				// end part of rangeObject should be used, therefor existing markups are cropped at start (beginning)
1416 				cropMarkupsAtStart = true;
1417 			}
1418 			objects = [];
1419 			for (i = 0; i < relevantMarkupObjects.length; i++) {
1420 				objects[i] = new this.SelectionRange();
1421 				el = relevantMarkupObjects[i];
1422 				if (cropMarkupsAtEnd && !cropMarkupsAtStart) {
1423 					textNodes = jQuery(el).textNodes(true);
1424 					objects[i].startContainer = textNodes[0];
1425 					objects[i].startOffset = 0;
1426 					// if the existing markup startContainer & startOffset are equal to the rangeObject startContainer and startOffset,
1427 					// then markupobject does not have to be added again, because it would have no content (zero-length)
1428 					if (objects[i].startContainer === rangeObject.startContainer && objects[i].startOffset === rangeObject.startOffset) {
1429 						continue;
1430 					}
1431 					if (rangeObject.startOffset === 0) {
1432 						objects[i].endContainer = this.getTextNodeSibling(false, el, rangeObject.startContainer);
1433 						objects[i].endOffset = objects[i].endContainer.length;
1434 					} else {
1435 						objects[i].endContainer = rangeObject.startContainer;
1436 						objects[i].endOffset = rangeObject.startOffset;
1437 					}
1438 
1439 					objects[i].update();
1440 
1441 					this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, {
1442 						setRangeObject2NextSibling: true
1443 					});
1444 				}
1445 
1446 				if (!cropMarkupsAtEnd && cropMarkupsAtStart) {
1447 					objects[i].startContainer = rangeObject.endContainer; // jQuery(el).contents()[0];
1448 					objects[i].startOffset = rangeObject.endOffset;
1449 					textnodes = jQuery(el).textNodes(true);
1450 					objects[i].endContainer = textnodes[textnodes.length - 1];
1451 					objects[i].endOffset = textnodes[textnodes.length - 1].length;
1452 					objects[i].update();
1453 					this.applyMarkup(objects[i].getSelectionTree(), rangeObject, this.getClonedMarkup4Wrapping(el), tagComparator, {
1454 						setRangeObject2PreviousSibling: true
1455 					});
1456 				}
1457 			}
1458 			return true;
1459 		},
1460 
1461 		/**
1462 		 * apply a certain markup to the current selection
1463 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1464 		 * @return void
1465 		 * @hide
1466 		 */
1467 		changeMarkupOnSelection: function (markupObject) {
1468 			var rangeObject = this.getRangeObject();
1469 
1470 			// change the markup
1471 			this.changeMarkup(rangeObject, markupObject, this.getStandardTagComparator(markupObject));
1472 
1473 			// merge text nodes
1474 			GENTICS.Utils.Dom.doCleanup({
1475 				'merge': true
1476 			}, rangeObject);
1477 
1478 			// update the range and select it
1479 			rangeObject.update();
1480 			rangeObject.select();
1481 			this.rangeObject = rangeObject;
1482 		},
1483 
1484 		/**
1485 		 * apply a certain markup to the selection Tree
1486 		 * @param selectionTree SelectionTree Object markup should be applied to
1487 		 * @param rangeObject Aloha rangeObject which will be modified to reflect the dom changes, after the markup was applied (only if activated via options)
1488 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1489 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1490 		 * @param options JS object, with the following boolean properties: setRangeObject2NewMarkup, setRangeObject2NextSibling, setRangeObject2PreviousSibling
1491 		 * @return void
1492 		 * @hide
1493 		 */
1494 		applyMarkup: function (selectionTree, rangeObject, markupObject, tagComparator, options) {
1495 			var optimizedSelectionTree, i, el, breakpoint;
1496 1497 			options = options || {};
1498 			// first same tags from within fully selected nodes for removal
1499 			this.prepareForRemoval(selectionTree, markupObject, tagComparator);
1500 
1501 			// first let's optimize the selection Tree in useful groups which can be wrapped together
1502 			optimizedSelectionTree = this.optimizeSelectionTree4Markup(selectionTree, markupObject, tagComparator);
1503 			breakpoint = true;
1504 
1505 			// now iterate over grouped elements and either recursively dive into object or wrap it as a whole
1506 			for (i = 0; i < optimizedSelectionTree.length; i++) {
1507 				el = optimizedSelectionTree[i];
1508 				if (el.wrappable) {
1509 					this.wrapMarkupAroundSelectionTree(el.elements, rangeObject, markupObject, tagComparator, options);
1510 				} else {
1511 					Aloha.Log.debug(this, 'dive further into non-wrappable object');
1512 					this.applyMarkup(el.element.children, rangeObject, markupObject, tagComparator, options);
1513 				}
1514 			}
1515 		},
1516 
1517 		/**
1518 		 * returns the type of the given markup (trying to match HTML5)
1519 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1520 		 * @return string name of the markup type
1521 		 * @hide
1522 		 */
1523 		getMarkupType: function (markupObject) {
1524 			var nn = jQuery(markupObject)[0].nodeName.toLowerCase();
1525 			if (markupObject.outerHtml) {
1526 				Aloha.Log.debug(this, 'Node name detected: ' + nn + ' for: ' + markupObject.outerHtml());
1527 			}
1528 			if (nn == '#text') {
1529 				return 'textNode';
1530 			}
1531 			if (this.replacingElements[nn]) {
1532 				return 'sectionOrGroupingContent';
1533 			}
1534 			if (this.tagHierarchy[nn]) {
1535 				return 'textLevelSemantics';
1536 			}
1537 			Aloha.Log.warn(this, 'unknown markup passed to this.getMarkupType(...): ' + markupObject.outerHtml());
1538 		},
1539 
1540 		/**
1541 		 * returns the standard tag comparator for the given markup object
1542 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1543 		 * @return function tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1544 		 * @hide
1545 		 */
1546 		getStandardTagComparator: function (markupObject) {
1547 			var that = this,
1548 				result;
1549 			switch (this.getMarkupType(markupObject)) {
1550 			case 'textNode':
1551 				result = function (p1, p2) {
1552 					return false;
1553 				};
1554 				break;
1555 
1556 			case 'sectionOrGroupingContent':
1557 				result = function (domobj, markupObject) {
1558 					return that.standardSectionsAndGroupingContentComparator(domobj, markupObject);
1559 				};
1560 				break;
1561 
1562 			//case 'textLevelSemantics' covered by default
1563 			default:
1564 				result = function (domobj, markupObject) {
1565 					return that.standardTextLevelSemanticsComparator(domobj, markupObject);
1566 				};
1567 				break;
1568 			}
1569 			return result;
1570 		},
1571 
1572 		/**
1573 		 * searches for fully selected equal markup tags
1574 		 * @param selectionTree SelectionTree Object markup should be applied to
1575 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1576 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1577 		 * @return void
1578 		 * @hide
1579 		 */
1580 		prepareForRemoval: function (selectionTree, markupObject, tagComparator) {
1581 			var that = this, i, el;
1582 
1583 			// check if a comparison method was passed as parameter ...
1584 			if (typeof tagComparator !== 'undefined' && typeof tagComparator !== 'function') {
1585 				Aloha.Log.error(this, 'parameter tagComparator is not a function');
1586 			}
1587 			// ... if not use this as standard tag comparison method
1588 			if (typeof tagComparator === 'undefined') {
1589 				tagComparator = this.getStandardTagComparator(markupObject);
1590 			}
1591 			for (i = 0; i < selectionTree.length; i++) {
1592 				el = selectionTree[i];
1593 				if (el.domobj && (el.selection == 'full' || (el.selection == 'partial' && markupObject.isReplacingElement))) {
1594 					// mark for removal
1595 					if (el.domobj.nodeType === 1 && tagComparator(el.domobj, markupObject)) {
1596 						Aloha.Log.debug(this, 'Marking for removal: ' + el.domobj.nodeName);
1597 						jQuery(el.domobj).addClass('preparedForRemoval');
1598 					}
1599 				}
1600 				if (el.selection != 'none' && el.children.length > 0) {
1601 					this.prepareForRemoval(el.children, markupObject, tagComparator);
1602 				}
1603 
1604 			}
1605 		},
1606 
1607 		/**
1608 		 * searches for fully selected equal markup tags
1609 		 * @param selectionTree SelectionTree Object markup should be applied to
1610 		 * @param rangeObject Aloha rangeObject the markup will be applied to
1611 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1612 		 * @param tagComparator method, which is used to compare the dom object and the jQuery markup object. the method must accept 2 parameters, the first is the domobj, the second is the jquery object. if no method is specified, the method this.standardTextLevelSemanticsComparator is used
1613 		 * @param options JS object, with the following boolean properties: setRangeObject2NewMarkup, setRangeObject2NextSibling, setRangeObject2PreviousSibling
1614 		 * @return void
1615 		 * @hide
1616 		 */
1617 		wrapMarkupAroundSelectionTree: function (selectionTree, rangeObject, markupObject, tagComparator, options) {
1618 			// first let's find out if theoretically the whole selection can be wrapped with one tag and save it for later use
1619 			var objects2wrap = [], // // this will be used later to collect objects
1620 				j = -1, // internal counter,
1621 				breakpoint = true,
1622 				preText = '',
1623 				postText = '',
1624 				prevOrNext,
1625 				textNode2Start,
1626 				textnodes,
1627 				newMarkup,
1628 				i,
1629 			    el,
1630 			    middleText;
1631 
1632 			Aloha.Log.debug(this, 'The formatting <' + markupObject[0].tagName + '> will be wrapped around the selection');
1633 
1634 			// now lets iterate over the elements
1635 			for (i = 0; i < selectionTree.length; i++) {
1636 				el = selectionTree[i];
1637 
1638 				// check if markup is allowed inside the elements parent
1639 				if (el.domobj && !this.canTag1WrapTag2(el.domobj.parentNode.tagName.toLowerCase(), markupObject[0].tagName.toLowerCase())) {
1640 					Aloha.Log.info(this, 'Skipping the wrapping of <' + markupObject[0].tagName.toLowerCase() + '> because this tag is not allowed inside <' + el.domobj.parentNode.tagName.toLowerCase() + '>');
1641 					continue;
1642 				}
1643 
1644 				// skip empty text nodes
1645 				if (el.domobj && el.domobj.nodeType === 3 && jQuery.trim(el.domobj.nodeValue).length === 0) {
1646 					continue;
1647 				}
1648 
1649 				// partial element, can either be a textnode and therefore be wrapped (at least partially)
1650 				// or can be a nodeType == 1 (tag) which must be dived into
1651 				if (el.domobj && el.selection == 'partial' && !markupObject.isReplacingElement) {
1652 					if (el.startOffset !== undefined && el.endOffset === undefined) {
1653 						j++;
1654 						preText += el.domobj.data.substr(0, el.startOffset);
1655 						el.domobj.data = el.domobj.data.substr(el.startOffset, el.domobj.data.length - el.startOffset);
1656 						objects2wrap[j] = el.domobj;
1657 					} else if (el.endOffset !== undefined && el.startOffset === undefined) {
1658 						j++;
1659 						postText += el.domobj.data.substr(el.endOffset, el.domobj.data.length - el.endOffset);
1660 						el.domobj.data = el.domobj.data.substr(0, el.endOffset);
1661 						objects2wrap[j] = el.domobj;
1662 					} else if (el.endOffset !== undefined && el.startOffset !== undefined) {
1663 						if (el.startOffset == el.endOffset) { // do not wrap empty selections
1664 							Aloha.Log.debug(this, 'skipping empty selection');
1665 							continue;
1666 						}
1667 						j++;
1668 						preText += el.domobj.data.substr(0, el.startOffset);
1669 						middleText = el.domobj.data.substr(el.startOffset, el.endOffset - el.startOffset);
1670 						postText += el.domobj.data.substr(el.endOffset, el.domobj.data.length - el.endOffset);
1671 						el.domobj.data = middleText;
1672 						objects2wrap[j] = el.domobj;
1673 					} else {
1674 						// a partially selected item without selectionStart/EndOffset is a nodeType 1 Element on the way to the textnode
1675 						Aloha.Log.debug(this, 'diving into object');
1676 						this.applyMarkup(el.children, rangeObject, markupObject, tagComparator, options);
1677 					}
1678 				}
1679 				// fully selected dom elements can be wrapped as whole element
1680 				if (el.domobj && (el.selection == 'full' || (el.selection == 'partial' && markupObject.isReplacingElement))) {
1681 					j++;
1682 					objects2wrap[j] = el.domobj;
1683 				}
1684 			}
1685 
1686 			if (objects2wrap.length > 0) {
1687 				// wrap collected DOM object with markupObject
1688 				objects2wrap = jQuery(objects2wrap);
1689 
1690 				// make a fix for text nodes in <li>'s in ie
1691 				jQuery.each(objects2wrap, function (index, element) {
1692 					if (jQuery.browser.msie && element.nodeType == 3 && !element.nextSibling && !element.previousSibling && element.parentNode && element.parentNode.nodeName.toLowerCase() == 'li') {
1693 						element.data = jQuery.trim(element.data);
1694 					}
1695 				});
1696 
1697 				newMarkup = objects2wrap.wrapAll(markupObject).parent();
1698 				newMarkup.before(preText).after(postText);
1699 
1700 				if (options.setRangeObject2NewMarkup) { // this is used, when markup is added to normal/normal Text
1701 					textnodes = objects2wrap.textNodes();
1702 
1703 					if (textnodes.index(rangeObject.startContainer) != -1) {
1704 1705 						rangeObject.startOffset = 0;
1706 					}
1707 					if (textnodes.index(rangeObject.endContainer) != -1) {
1708 						rangeObject.endOffset = rangeObject.endContainer.length;
1709 					}
1710 					breakpoint = true;
1711 				}
1712 				if (options.setRangeObject2NextSibling) {
1713 					prevOrNext = true;
1714 					textNode2Start = newMarkup.textNodes(true).last()[0];
1715 					if (objects2wrap.index(rangeObject.startContainer) != -1) {
1716 						rangeObject.startContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start);
1717 						rangeObject.startOffset = 0;
1718 					}
1719 					if (objects2wrap.index(rangeObject.endContainer) != -1) {
1720 						rangeObject.endContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start);
1721 						rangeObject.endOffset = rangeObject.endOffset - textNode2Start.length;
1722 					}
1723 				}
1724 				if (options.setRangeObject2PreviousSibling) {
1725 					prevOrNext = false;
1726 					textNode2Start = newMarkup.textNodes(true).first()[0];
1727 					if (objects2wrap.index(rangeObject.startContainer) != -1) {
1728 						rangeObject.startContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start);
1729 						rangeObject.startOffset = 0;
1730 					}
1731 					if (objects2wrap.index(rangeObject.endContainer) != -1) {
1732 						rangeObject.endContainer = this.getTextNodeSibling(prevOrNext, newMarkup.parent(), textNode2Start);
1733 						rangeObject.endOffset = rangeObject.endContainer.length;
1734 					}
1735 				}
1736 			}
1737 		},
1738 
1739 		/**
1740 		 * takes a text node and return either the next recursive text node sibling or the previous
1741 		 * @param previousOrNext boolean, false for previous, true for next sibling
1742 		 * @param commonAncestorContainer dom object to be used as root for the sibling search
1743 		 * @param currentTextNode dom object of the originating text node
1744 		 * @return dom object of the sibling text node
1745 		 * @hide
1746 		 */
1747 		getTextNodeSibling: function (previousOrNext, commonAncestorContainer, currentTextNode) {
1748 			var textNodes = jQuery(commonAncestorContainer).textNodes(true), newIndex, index;
1749 
1750 			index = textNodes.index(currentTextNode);
1751 			if (index == -1) { // currentTextNode was not found
1752 				return false;
1753 			}
1754 			newIndex = index + (!previousOrNext ? -1 : 1);
1755 			return textNodes[newIndex] || false;
1756 		},
1757 
1758 		/**
1759 		 * takes a selection tree and groups it into markup wrappable selection trees
1760 		 * @param selectionTree rangeObject selection tree
1761 		 * @param markupObject jQuery object of the markup to be applied (e.g. created with obj = jQuery('<b></b>'); )
1762 		 * @return JS array of wrappable selection trees
1763 		 * @hide
1764 		 */
1765 		optimizeSelectionTree4Markup: function (selectionTree, markupObject, tagComparator) {
1766 			var groupMap = [],
1767 				outerGroupIndex = 0,
1768 				innerGroupIndex = 0,
1769 				that = this,
1770 				i,
1771 			    j,
1772 				endPosition,
1773 			    startPosition;
1774 
1775 			if (typeof tagComparator === 'undefined') {
1776 				tagComparator = function (domobj, markupObject) {
1777 					return that.standardTextLevelSemanticsComparator(markupObject);
1778 				};
1779 			}
1780 			for (i = 0; i < selectionTree.length; i++) {
1781 				// we are just interested in selected item, but not in non-selected items
1782 				if (selectionTree[i].domobj && selectionTree[i].selection != 'none') {
1783 					if (markupObject.isReplacingElement && tagComparator(markupObject[0], jQuery(selectionTree[i].domobj))) {
1784 						if (groupMap[outerGroupIndex] !== undefined) {
1785 							outerGroupIndex++;
1786 						}
1787 						groupMap[outerGroupIndex] = {};
1788 						groupMap[outerGroupIndex].wrappable = true;
1789 						groupMap[outerGroupIndex].elements = [];
1790 						groupMap[outerGroupIndex].elements[innerGroupIndex] = selectionTree[i];
1791 						outerGroupIndex++;
1792 
1793 					} else if (this.canMarkupBeApplied2ElementAsWhole([selectionTree[i]], markupObject)) {
1794 						// now check, if the children of our item could be wrapped all together by the markup object
1795 						// if yes, add it to the current group
1796 						if (groupMap[outerGroupIndex] === undefined) {
1797 							groupMap[outerGroupIndex] = {};
1798 							groupMap[outerGroupIndex].wrappable = true;
1799 							groupMap[outerGroupIndex].elements = [];
1800 						}
1801 						if (markupObject.isReplacingElement) { //  && selectionTree[i].domobj.nodeType === 3
1802 							/* we found the node to wrap for a replacing element. however there might
1803 							 * be siblings which should be included as well
1804 							 * although they are actually not selected. example:
1805 							 * li
1806 							 * |-textNode ( .selection = 'none')
1807 							 * |-textNode (cursor inside, therefor .selection = 'partial')
1808 							 * |-textNode ( .selection = 'none')
1809 							 *
1810 							 * in this case it would be useful to select the previous and following textNodes as well (they might result from a previous DOM manipulation)
1811 							 * Think about other cases, where the parent is the Editable. In this case we propably only want to select from and until the next <br /> ??
1812 							 * .... many possibilities, here I realize the two described cases
1813 							 */
1814 
1815 							// first find start element starting from the current element going backwards until sibling 0
1816 							startPosition = i;
1817 							for (j = i - 1; j >= 0; j--) {
1818 								if (this.canMarkupBeApplied2ElementAsWhole([selectionTree[j]], markupObject) && this.isMarkupAllowedToStealSelectionTreeElement(selectionTree[j], markupObject)) {
1819 									startPosition = j;
1820 								} else {
1821 									break;
1822 								}
1823 							}
1824 
1825 							// now find the end element starting from the current element going forward until the last sibling
1826 							endPosition = i;
1827 							for (j = i + 1; j < selectionTree.length; j++) {
1828 								if (this.canMarkupBeApplied2ElementAsWhole([selectionTree[j]], markupObject) && this.isMarkupAllowedToStealSelectionTreeElement(selectionTree[j], markupObject)) {
1829 									endPosition = j;
1830 								} else {
1831 									break;
1832 								}
1833 							}
1834 
1835 							// now add the elements to the groupMap
1836 							innerGroupIndex = 0;
1837 							for (j = startPosition; j <= endPosition; j++) {
1838 								groupMap[outerGroupIndex].elements[innerGroupIndex] = selectionTree[j];
1839 								groupMap[outerGroupIndex].elements[innerGroupIndex].selection = 'full';
1840 								innerGroupIndex++;
1841 							}
1842 							innerGroupIndex = 0;
1843 						} else {
1844 							// normal text level semantics object, no siblings need to be selected
1845 							groupMap[outerGroupIndex].elements[innerGroupIndex] = selectionTree[i];
1846 							innerGroupIndex++;
1847 						}
1848 					} else {
1849 						// if no, isolate it in its own group
1850 						if (groupMap[outerGroupIndex] !== undefined) {
1851 							outerGroupIndex++;
1852 						}
1853 						groupMap[outerGroupIndex] = {};
1854 						groupMap[outerGroupIndex].wrappable = false;
1855 						groupMap[outerGroupIndex].element = selectionTree[i];
1856 						innerGroupIndex = 0;
1857 						outerGroupIndex++;
1858 					}
1859 				}
1860 			}
1861 			return groupMap;
1862 		},
1863 
1864 		/**
1865 		 * very tricky method, which decides, if a certain markup (normally a replacing markup element like p, h1, blockquote)
1866 		 * is allowed to extend the user selection to other dom objects (represented as selectionTreeElement)
1867 		 * to understand the purpose: if the user selection is collapsed inside e.g. some text, which is currently not
1868 		 * wrapped by the markup to be applied, and therefor the markup does not have an equal markup to replace, then the DOM
1869 		 * manipulator has to decide which objects to wrap. real example:
1870 		 * <div>
1871 		 *	<h1>headline</h1>
1872 		 *	some text blabla bla<br>
1873 		 *	more text HERE THE | CURSOR BLINKING and <b>even more bold text</b>
1874 		 * </div>
1875 		 * when the user now wants to apply e.g. a <p> tag, what will be wrapped? it could be useful if the manipulator would actually
1876 		 * wrap everything inside the div except the <h1>. but for this purpose someone has to decide, if the markup is
1877 		 * allowed to wrap certain dom elements in this case the question would be, if the <p> is allowed to wrap
1878 		 * textNodes, <br> and <b> and <h1>. therefore this tricky method should answer the question for those 3 elements
1879 		 * with true, but for for the <h1> it should return false. and since the method does not know this, there is a configuration
1880 		 * for this
1881 		 *
1882 		 * @param selectionTree rangeObject selection tree element (only one, not an array of)
1883 		 * @param markupObject lowercase string of the tag to be verified (e.g. "b")
1884 		 * @return true if the markup is allowed to wrap the selection tree element, false otherwise
1885 		 * @hide
1886 		 */
1887 		isMarkupAllowedToStealSelectionTreeElement: function (selectionTreeElement, markupObject) {
1888 			if (!selectionTreeElement.domobj) {
1889 				return false;
1890 			}
1891 			var maybeTextNodeName = selectionTreeElement.domobj.nodeName.toLowerCase(),
1892 				nodeName = (maybeTextNodeName == '#text') ? 'textNode' : maybeTextNodeName,
1893 				markupName = markupObject[0].nodeName.toLowerCase(),
1894 				elemMap = this.allowedToStealElements[markupName];
1895 			return elemMap && elemMap[nodeName];
1896 		},
1897 
1898 		/**
1899 		 * checks if a selection can be completey wrapped by a certain html tags (helper method for this.optimizeSelectionTree4Markup
1900 		 * @param selectionTree rangeObject selection tree
1901 		 * @param markupObject lowercase string of the tag to be verified (e.g. "b")
1902 		 * @return true if selection can be applied as whole, false otherwise
1903 		 * @hide
1904 		 */
1905 		canMarkupBeApplied2ElementAsWhole: function (selectionTree, markupObject) {
1906 			var htmlTag, i, el, returnVal;
1907 
1908 			if (markupObject.jquery) {
1909 				htmlTag = markupObject[0].tagName;
1910 			}
1911 			if (markupObject.tagName) {
1912 				htmlTag = markupObject.tagName;
1913 			}
1914 
1915 			returnVal = true;
1916 			for (i = 0; i < selectionTree.length; i++) {
1917 				el = selectionTree[i];
1918 				if (el.domobj && (el.selection != "none" || markupObject.isReplacingElement)) {
1919 					// Aloha.Log.debug(this, 'Checking, if  <' + htmlTag + '> can be applied to ' + el.domobj.nodeName);
1920 					if (!this.canTag1WrapTag2(htmlTag, el.domobj.nodeName)) {
1921 						return false;
1922 					}
1923 					if (el.children.length > 0 && !this.canMarkupBeApplied2ElementAsWhole(el.children, markupObject)) {
1924 						return false;
1925 					}
1926 				}
1927 			}
1928 			return returnVal;
1929 		},
1930 
1931 		/**
1932 		 * checks if a tag 1 (first parameter) can wrap tag 2 (second parameter).
1933 		 * IMPORTANT: the method does not verify, if there have to be other tags in between
1934 		 * Example: this.canTag1WrapTag2("table", "td") will return true, because the method does not take into account, that there has to be a "tr" in between
1935 		 * @param t1 string: tagname of outer tag to verify, e.g. "b"
1936 		 * @param t2 string: tagname of inner tag to verify, e.g. "b"
1937 		 * @return true if tag 1 can wrap tag 2, false otherwise
1938 		 * @hide
1939 		 */
1940 		canTag1WrapTag2: function (t1, t2) {
1941 			t1 = (t1 == '#text') ? 'textNode' : t1.toLowerCase();
1942 			t2 = (t2 == '#text') ? 'textNode' : t2.toLowerCase();
1943 			var t1Map = this.tagHierarchy[t1];
1944 			if (!t1Map) {
1945 				return true;
1946 			}
1947 			if (!this.tagHierarchy[t2]) {
1948 				return true;
1949 			}
1950 			return t1Map[t2];
1951 		},
1952 
1953 		/**
1954 		 * Check whether it is allowed to insert the given tag at the start of the
1955 		 * current selection. This method will check whether the markup effective for
1956 		 * the start and outside of the editable part (starting with the editable tag
1957 		 * itself) may wrap the given tag.
1958 		 * @param tagName {String} name of the tag which shall be inserted
1959 		 * @return true when it is allowed to insert that tag, false if not
1960 		 * @hide
1961 		 */
1962 		mayInsertTag: function (tagName) {
1963 			var i;
1964 			if (typeof this.rangeObject.unmodifiableMarkupAtStart == 'object') {
1965 				// iterate over all DOM elements outside of the editable part
1966 				for (i = 0; i < this.rangeObject.unmodifiableMarkupAtStart.length; ++i) {
1967 					// check whether an element may not wrap the given
1968 					if (!this.canTag1WrapTag2(this.rangeObject.unmodifiableMarkupAtStart[i].nodeName, tagName)) {
1969 						// found a DOM element which forbids to insert the given tag, we are done
1970 						return false;
1971 					}
1972 				}
1973 
1974 				// all of the found DOM elements allow inserting the given tag
1975 				return true;
1976 			}
1977 			Aloha.Log.warn(this, 'Unable to determine whether tag ' + tagName + ' may be inserted');
1978 			return true;
1979 		},
1980 
1981 		/**
1982 		 * String representation
1983 		 * @return "Aloha.Selection"
1984 		 * @hide
1985 		 */
1986 		toString: function () {
1987 			return 'Aloha.Selection';
1988 		},
1989 
1990 		/**
1991 		 * @namespace Aloha.Selection
1992 		 * @class SelectionRange
1993 		 * @extends GENTICS.Utils.RangeObject
1994 		 * Constructor for a range object.
1995 		 * Optionally you can pass in a range object that's properties will be assigned to the new range object.
1996 		 * @param rangeObject A range object thats properties will be assigned to the new range object.
1997 		 * @constructor
1998 		 */
1999 		SelectionRange: GENTICS.Utils.RangeObject.extend({
2000 			_constructor: function (rangeObject) {
2001 				this._super(rangeObject);
2002 				// If a range object was passed in we apply the values to the new range object
2003 				if (rangeObject) {
2004 					if (rangeObject.commonAncestorContainer) {
2005 						this.commonAncestorContainer = rangeObject.commonAncestorContainer;
2006 					}
2007 					if (rangeObject.selectionTree) {
2008 						this.selectionTree = rangeObject.selectionTree;
2009 					}
2010 					if (rangeObject.limitObject) {
2011 						this.limitObject = rangeObject.limitObject;
2012 					}
2013 					if (rangeObject.markupEffectiveAtStart) {
2014 						this.markupEffectiveAtStart = rangeObject.markupEffectiveAtStart;
2015 					}
2016 					if (rangeObject.unmodifiableMarkupAtStart) {
2017 						this.unmodifiableMarkupAtStart = rangeObject.unmodifiableMarkupAtStart;
2018 					}
2019 					if (rangeObject.splitObject) {
2020 						this.splitObject = rangeObject.splitObject;
2021 					}
2022 				}
2023 			},
2024 
2025 			/**
2026 			 * DOM object of the common ancestor from startContainer and endContainer
2027 			 * @hide
2028 			 */
2029 			commonAncestorContainer: undefined,
2030 
2031 			/**
2032 			 * The selection tree
2033 			 * @hide
2034 			 */
2035 			selectionTree: undefined,
2036 
2037 			/**
2038 			 * Array of DOM objects effective for the start container and inside the
2039 			 * editable part (inside the limit object). relevant for the button status
2040 			 * @hide
2041 			 */
2042 			markupEffectiveAtStart: [],
2043 
2044 			/**
2045 			 * Array of DOM objects effective for the start container, which lies
2046 			 * outside of the editable portion (starting with the limit object)
2047 			 * @hide
2048 			 */
2049 			unmodifiableMarkupAtStart: [],
2050 
2051 			/**
2052 			 * DOM object being the limit for all markup relevant activities
2053 			 * @hide
2054 			 */
2055 			limitObject: undefined,
2056 
2057 			/**
2058 			 * DOM object being split when enter key gets hit
2059 			 * @hide
2060 			 */
2061 			splitObject: undefined,
2062 
2063 			/**
2064 			 * Sets the visible selection in the Browser based on the range object.
2065 			 * If the selection is collapsed, this will result in a blinking cursor,
2066 			 * otherwise in a text selection.
2067 			 * @method
2068 			 */
2069 			select: function () {
2070 				// Call Utils' select()
2071 				this._super();
2072 
2073 				// update the selection
2074 				Aloha.Selection.updateSelection();
2075 			},
2076 
2077 			/**
2078 			 * Method to update a range object internally
2079 			 * @param commonAncestorContainer (DOM Object); optional Parameter; if set, the parameter
2080 			 * will be used instead of the automatically calculated CAC
2081 			 * @return void
2082 			 * @hide
2083 			 */
2084 			update: function (commonAncestorContainer) {
2085 				this.updatelimitObject();
2086 				this.updateMarkupEffectiveAtStart();
2087 				this.updateCommonAncestorContainer(commonAncestorContainer);
2088 
2089 				// reset the selectiontree (must be recalculated)
2090 				this.selectionTree = undefined;
2091 			},
2092 
2093 			/**
2094 			 * Get the selection tree for this range
2095 			 * TODO: remove this (was moved to range.js)
2096 			 * @return selection tree
2097 			 * @hide
2098 			 */
2099 			getSelectionTree: function () {
2100 				// if not yet calculated, do this now
2101 				if (!this.selectionTree) {
2102 					this.selectionTree = Aloha.Selection.getSelectionTree(this);
2103 				}
2104 
2105 				return this.selectionTree;
2106 			},
2107 
2108 			/**
2109 			 * TODO: move this to range.js
2110 			 * Get an array of domobj (in dom tree order) of siblings of the given domobj, which are contained in the selection
2111 			 * @param domobj dom object to start with
2112 			 * @return array of siblings of the given domobj, which are also selected
2113 			 * @hide
2114 			 */
2115 			getSelectedSiblings: function (domobj) {
2116 				var selectionTree = this.getSelectionTree();
2117 
2118 				return this.recursionGetSelectedSiblings(domobj, selectionTree);
2119 			},
2120 
2121 			/**
2122 			 * TODO: move this to range.js
2123 			 * Recursive method to find the selected siblings of the given domobj (which should be selected as well)
2124 			 * @param domobj dom object for which the selected siblings shall be found
2125 			 * @param selectionTree current level of the selection tree
2126 			 * @return array of selected siblings of dom objects or false if none found
2127 			 * @hide
2128 			 */
2129 			recursionGetSelectedSiblings: function (domobj, selectionTree) {
2130 				var selectedSiblings = false,
2131 					foundObj = false,
2132 					i;
2133 
2134 				for (i = 0; i < selectionTree.length; ++i) {
2135 					if (selectionTree[i].domobj === domobj) {
2136 						foundObj = true;
2137 						selectedSiblings = [];
2138 					} else if (!foundObj && selectionTree[i].children) {
2139 						// do the recursion
2140 						selectedSiblings = this.recursionGetSelectedSiblings(domobj, selectionTree[i].children);
2141 						if (selectedSiblings !== false) {
2142 							break;
2143 						}
2144 					} else if (foundObj && selectionTree[i].domobj && selectionTree[i].selection != 'collapsed' && selectionTree[i].selection != 'none') {
2145 						selectedSiblings.push(selectionTree[i].domobj);
2146 					} else if (foundObj && selectionTree[i].selection == 'none') {
2147 						break;
2148 					}
2149 				}
2150 
2151 				return selectedSiblings;
2152 			},
2153 
2154 			/**
2155 			 * TODO: move this to range.js
2156 			 * Method updates member var markupEffectiveAtStart and splitObject, which is relevant primarily for button status and enter key behaviour
2157 			 * @return void
2158 			 * @hide
2159 			 */
2160 			updateMarkupEffectiveAtStart: function () {
2161 				// reset the current markup
2162 				this.markupEffectiveAtStart = [];
2163 				this.unmodifiableMarkupAtStart = [];
2164 
2165 				var parents = this.getStartContainerParents(),
2166 					limitFound = false,
2167 					splitObjectWasSet,
2168 					i,
2169 				    el;
2170 
2171 				for (i = 0; i < parents.length; i++) {
2172 					el = parents[i];
2173 					if (!limitFound && (el !== this.limitObject)) {
2174 						this.markupEffectiveAtStart[i] = el;
2175 						if (!splitObjectWasSet && GENTICS.Utils.Dom.isSplitObject(el)) {
2176 							splitObjectWasSet = true;
2177 							this.splitObject = el;
2178 						}
2179 					} else {
2180 						limitFound = true;
2181 						this.unmodifiableMarkupAtStart.push(el);
2182 					}
2183 				}
2184 				if (!splitObjectWasSet) {
2185 					this.splitObject = false;
2186 				}
2187 				return;
2188 			},
2189 
2190 			/**
2191 			 * TODO: remove this
2192 			 * Method updates member var markupEffectiveAtStart, which is relevant primarily for button status
2193 			 * @return void
2194 			 * @hide
2195 			 */
2196 			updatelimitObject: function () {
2197 				if (Aloha.editables && Aloha.editables.length > 0) {
2198 					var parents = this.getStartContainerParents(),
2199 						editables = Aloha.editables,
2200 						i,
2201 					    el,
2202 					    j,
2203 					    editable;
2204 					for (i = 0; i < parents.length; i++) {
2205 						el = parents[i];
2206 						for (j = 0; j < editables.length; j++) {
2207 							editable = editables[j].obj[0];
2208 							if (el === editable) {
2209 								this.limitObject = el;
2210 								return true;
2211 							}
2212 						}
2213 					}
2214 				}
2215 				this.limitObject = jQuery('body');
2216 				return true;
2217 			},
2218 
2219 			/**
2220 			 * string representation of the range object
2221 			 * @param	verbose	set to true for verbose output
2222 			 * @return string representation of the range object
2223 			 * @hide
2224 			 */
2225 			toString: function (verbose) {
2226 				if (!verbose) {
2227 					return 'Aloha.Selection.SelectionRange';
2228 				}
2229 				return 'Aloha.Selection.SelectionRange {start [' + this.startContainer.nodeValue + '] offset ' + this.startOffset + ', end [' + this.endContainer.nodeValue + '] offset ' + this.endOffset + '}';
2230 			}
2231 
2232 		}) // SelectionRange
2233 
2234 	}); // Selection
2235 
2236 
2237 	/**
2238 	 * This method implements an ugly workaround for a selection problem in ie:
2239 	 * when the cursor shall be placed at the end of a text node in a li element, that is followed by a nested list,
2240 	 * the selection would always snap into the first li of the nested list
2241 	 * therefore, we make sure that the text node ends with a space and place the cursor right before it
2242 	 */
2243 	function nestedListInIEWorkaround(range) {
2244 		var nextSibling;
2245 		if (jQuery.browser.msie && range.startContainer === range.endContainer && range.startOffset === range.endOffset && range.startContainer.nodeType == 3 && range.startOffset == range.startContainer.data.length && range.startContainer.nextSibling) {
2246 			nextSibling = range.startContainer.nextSibling;
2247 			if ('OL' === nextSibling.nodeName || 'UL' === nextSibling.nodeName) {
2248 				if (range.startContainer.data[range.startContainer.data.length - 1] == ' ') {
2249 					range.startOffset = range.endOffset = range.startOffset - 1;
2250 				} else {
2251 					range.startContainer.data = range.startContainer.data + ' ';
2252 				}
2253 			}
2254 		}
2255 	}
2256 
2257 	function correctRange(range) {
2258 		nestedListInIEWorkaround(range);
2259 		return range;
2260 	}
2261 
2262 	/**
2263 	 * Implements Selection http://html5.org/specs/dom-range.html#selection
2264 	 * @namespace Aloha
2265 	 * @class Selection This singleton class always represents the
2266 	 *        current user selection
2267 	 * @singleton
2268 	 */
2269 	var AlohaSelection = Class.extend({
2270 
2271 		_constructor: function (nativeSelection) {
2272 
2273 			this._nativeSelection = nativeSelection;
2274 			this.ranges = [];
2275 
2276 			// will remember if urged to not change the selection
2277 			this.preventChange = false;
2278 
2279 		},
2280 
2281 		/**
2282 		 * Returns the element that contains the start of the selection. Returns null if there's no selection.
2283 		 * @readonly
2284 		 * @type Node
2285 		 */
2286 		anchorNode: null,
2287 
2288 		/**
2289 		 * Returns the offset of the start of the selection relative to the element that contains the start 
2290 		 * of the selection. Returns 0 if there's no selection.
2291 		 * @readonly
2292 		 * @type int
2293 		 */
2294 		anchorOffset: 0,
2295 
2296 		/**
2297 		 * Returns the element that contains the end of the selection.
2298 		 * Returns null if there's no selection.
2299 		 * @readonly
2300 		 * @type Node
2301 		 */
2302 		focusNode: null,
2303 
2304 		/**
2305 		 * Returns the offset of the end of the selection relative to the element that contains the end 
2306 		 * of the selection. Returns 0 if there's no selection.
2307 		 * @readonly
2308 		 * @type int
2309 		 */
2310 		focusOffset: 0,
2311 
2312 		/**
2313 		 * Returns true if there's no selection or if the selection is empty. Otherwise, returns false.
2314 		 * @readonly
2315 		 * @type boolean
2316 		 */
2317 		isCollapsed: false,
2318 
2319 		/**
2320 		 * Returns the number of ranges in the selection.
2321 		 * @readonly
2322 		 * @type int
2323 		 */
2324 		rangeCount: 0,
2325 
2326 		/**
2327 		 * Replaces the selection with an empty one at the given position.
2328 		 * @throws a WRONG_DOCUMENT_ERR exception if the given node is in a different document.
2329 		 * @param parentNode Node of new selection
2330 		 * @param offest offest of new Selection in parentNode
2331 		 * @void
2332 		 */
2333 		collapse: function (parentNode, offset) {
2334 			this._nativeSelection.collapse(parentNode, offset);
2335 		},
2336 
2337 		/**
2338 		 * Replaces the selection with an empty one at the position of the start of the current selection.
2339 		 * @throws an INVALID_STATE_ERR exception if there is no selection.
2340 		 * @void
2341 		 */
2342 		collapseToStart: function () {
2343 			throw "NOT_IMPLEMENTED";
2344 		},
2345 
2346 		/** 
2347 		 * @void
2348 		 */
2349 		extend: function (parentNode, offset) {
2350 
2351 		},
2352 
2353 		/**
2354 		 * @param alter DOMString 
2355 		 * @param direction DOMString 
2356 		 * @param granularity DOMString 
2357 		 * @void
2358 		 */
2359 		modify: function (alter, direction, granularity) {
2360 
2361 		},
2362 
2363 		/**
2364 		 * Replaces the selection with an empty one at the position of the end of the current selection.
2365 		 * @throws an INVALID_STATE_ERR exception if there is no selection.
2366 		 * @void
2367 		 */
2368 		collapseToEnd: function () {
2369 			this._nativeSelection.collapseToEnd();
2370 		},
2371 
2372 		/**
2373 		 * Replaces the selection with one that contains all the contents of the given element.
2374 		 * @throws a WRONG_DOCUMENT_ERR exception if the given node is in a different document.
2375 		 * @param parentNode Node the Node fully select
2376 		 * @void
2377 		 */
2378 		selectAllChildren: function (parentNode) {
2379 			throw "NOT_IMPLEMENTED";
2380 		},
2381 
2382 		/**
2383 		 * Deletes the contents of the selection
2384 		 */
2385 		deleteFromDocument: function () {
2386 			throw "NOT_IMPLEMENTED";
2387 		},
2388 
2389 		/**
2390 		 * NB!
2391 		 * We have serious problem in IE.
2392 		 * The range that we get in IE is not the same as the range we had set,
2393 		 * so even if we normalize it during getRangeAt, in IE, we will be
2394 		 * correcting the range to the "correct" place, but still not the place
2395 		 * where it was originally set.
2396 		 * 
2397 		 * Returns the given range.
2398 		 * The getRangeAt(index) method returns the indexth range in the list. 
2399 		 * NOTE: Aloha Editor only support 1 range! index can only be 0
2400 		 * @throws INDEX_SIZE_ERR DOM exception if index is less than zero or 
2401 		 * greater or equal to the value returned by the rangeCount.
2402 		 * @param index int 
2403 		 * @return Range return the selected range from index
2404 		 */
2405 		getRangeAt: function (index) {
2406 			return correctRange(this._nativeSelection.getRangeAt(index));
2407 			//if ( index < 0 || this.rangeCount ) {
2408 			//	throw "INDEX_SIZE_ERR DOM";
2409 			//}
2410 			//return this._ranges[index];
2411 		},
2412 
2413 		/**
2414 		 * Adds the given range to the selection.
2415 		 * The addRange(range) method adds the given range Range object to the list of
2416 		 * selections, at the end (so the newly added range is the new last range). 
2417 		 * NOTE: Aloha Editor only support 1 range! The added range will replace the 
2418 		 * range at index 0
2419 		 * see http://html5.org/specs/dom-range.html#selection note about addRange
2420 		 * @throws an INVALID_NODE_TYPE_ERR exception if the given Range has a boundary point
2421 		 * node that's not a Text or Element node, and an INVALID_MODIFICATION_ERR exception 
2422 		 * if it has a boundary point node that doesn't descend from a Document.
2423 		 * @param range Range adds the range to the selection
2424 		 * @void
2425 		 */
2426 		addRange: function (range) {
2427 			// set readonly attributes
2428 			this._nativeSelection.addRange(range);
2429 			// We will correct the range after rangy has processed the native
2430 			// selection range, so that our correction will be the final fix on
2431 			// the range according to the guarentee's that Aloha wants to make
2432 			this._nativeSelection._ranges[0] = correctRange(range);
2433 
2434 			// make sure, the old Aloha selection will be updated (until all implementations use the new AlohaSelection)
2435 			Aloha.Selection.updateSelection();
2436 		},
2437 
2438 		/**
2439 		 * Removes the given range from the selection, if the range was one of the ones in the selection.
2440 		 * NOTE: Aloha Editor only support 1 range! The added range will replace the 
2441 		 * range at with index 0
2442 		 * @param range Range removes the range from the selection
2443 		 * @void
2444 		 */
2445 		removeRange: function (range) {
2446 			this._nativeSelection.removeRange();
2447 		},
2448 
2449 		/**
2450 		 * Removes all the ranges in the selection.
2451 		 * @viod
2452 		 */
2453 		removeAllRanges: function () {
2454 			this._nativeSelection.removeAllRanges();
2455 		},
2456 
2457 		/**
2458 		 * INFO: Method is used for integration with Gentics
2459 		 * Aloha, has no use otherwise Updates the rangeObject
2460 		 * according to the current user selection Method is
2461 		 * always called on selection change
2462 		 * 
2463 		 * @param event
2464 		 *            jQuery browser event object
2465 		 * @return true when rangeObject was modified, false
2466 		 *         otherwise
2467 		 * @hide
2468 		 */
2469 		refresh: function (event) {
2470 
2471 		},
2472 
2473 		/**
2474 		 * String representation
2475 		 * 
2476 		 * @return "Aloha.Selection"
2477 		 * @hide
2478 		 */
2479 		toString: function () {
2480 			return 'Aloha.Selection';
2481 		},
2482 
2483 		getRangeCount: function () {
2484 			return this._nativeSelection.rangeCount;
2485 		}
2486 
2487 	});
2488 
2489 	/**
2490 	 * A wrapper for the function of the same name in the rangy core-depdency.
2491 	 * This function should be preferred as it hides the global rangy object.
2492 	 * For more information look at the following sites:
2493 	 * http://html5.org/specs/dom-range.html
2494 	 * @param window optional - specifices the window to get the selection of
2495 	 */
2496 	Aloha.getSelection = function (target) {
2497 		target = (target !== document || target !== window) ? window : target;
2498 		// Aloha.Selection.refresh()
2499 		// implement Aloha Selection 
2500 		// TODO cache
2501 		return new AlohaSelection(window.rangy.getSelection(target));
2502 	};
2503 
2504 	/**
2505 	 * A wrapper for the function of the same name in the rangy core-depdency.
2506 	 * This function should be preferred as it hides the global rangy object.
2507 	 * Please note: when the range object is not needed anymore,
2508 	 *   invoke the detach method on it. It is currently unknown to me why
2509 	 *   this is required, but that's what it says in the rangy specification.
2510 	 * For more information look at the following sites:
2511 	 * http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
2512 	 * @param document optional - specifies which document to create the range for
2513 	 */
2514 	Aloha.createRange = function (givenWindow) {
2515 		return window.rangy.createRange(givenWindow);
2516 	};
2517 
2518 	var selection = new Selection();
2519 	Aloha.Selection = selection;
2520 
2521 	return selection;
2522 });
2523