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