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