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