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