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