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