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