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