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