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