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