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