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