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