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