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