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