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 define(
 21 ['aloha/core', 'aloha/jquery', 'aloha/ext', 'util/class', 'aloha/console', 'vendor/jquery.store'],
 22 function(Aloha, jQuery, Ext, Class, console) {
 23 	"use strict";
 24 	var GENTICS = window.GENTICS;
 25 
 26 	/**
 27 	 * Constructor for a floatingmenu tab
 28 	 * @namespace Aloha.FloatingMenu
 29 	 * @class Tab
 30 	 * @constructor
 31 	 * @param {String} label label of the tab
 32 	 */
 33 	var Tab = Class.extend({
 34 		_constructor: function(label) {
 35 			this.label = label;
 36 			this.groups = [];
 37 			this.groupMap = {};
 38 			this.visible = true;
 39 		},
 40 
 41 		/**
 42 		 * Get the group with given index. If it does not yet exist, create a new one
 43 		 * @method
 44 		 * @param {int} group group index of the group to get
 45 		 * @return group object
 46 		 */
 47 		getGroup: function(group) {
 48 			var groupObject = this.groupMap[group];
 49 			if (typeof groupObject === 'undefined') {
 50 				groupObject = new Group();
 51 				this.groupMap[group] = groupObject;
 52 				this.groups.push(groupObject);
 53 				// TODO resort the groups
 54 			}
 55 
 56 			return groupObject;
 57 		},
 58 
 59 		/**
 60 		 * Get the EXT component representing the tab
 61 		 * @return EXT component (EXT.Panel)
 62 		 * @hide
 63 		 */
 64 		getExtComponent: function () {
 65 			var that = this;
 66 
 67 			if (!this.extPanel) {
 68 				this.extPanel = new Ext.Panel({
 69 					'tbar' : [],
 70 					'title' : this.label,
 71 					'style': 'margin-top:0px',
 72 					'bodyStyle': 'display:none',
 73 					'autoScroll': true
 74 				});
 75 			}
 76 
 77 			jQuery.each(this.groups, function(index, group) {
 78 				// let each group generate its ext component and add them to
 79 				// the panel once.
 80 				if (!group.extButtonGroup) {
 81 					that.extPanel.getTopToolbar().add(group.getExtComponent());
 82 				}
 83 			});
 84 
 85 			return this.extPanel;
 86 		},
 87 
 88 		/**
 89 		 * Recalculate the visibility of all groups within the tab
 90 		 * @hide
 91 		 */
 92 		doLayout: function() {
 93 			var that = this;
 94 
 95 			if (Aloha.Log.isDebugEnabled()) {
 96 				Aloha.Log.debug(this, 'doLayout called for tab ' + this.label);
 97 			}
 98 			this.visible = false;
 99 
100 			// check all groups in this tab
101 			jQuery.each(this.groups, function(index, group) {
102 				that.visible |= group.doLayout();
103 			});
104 
105 			if (Aloha.Log.isDebugEnabled()) {
106 				Aloha.Log.debug(this, 'tab ' + this.label + (this.visible ? ' is ' : ' is not ') + 'visible now');
107 			}
108 
109 			return this.visible;
110 		}
111 	});
112 
113 	/**
114 	 * Constructor for a floatingmenu group
115 	 * @namespace Aloha.FloatingMenu
116 	 * @class Group
117 	 * @constructor
118 	 */
119 	var Group = Class.extend({
120 		_constructor: function() {
121 			this.buttons = [];
122 			this.fields = [];
123 		},
124 
125 		/**
126 		 * Add a button to this group
127 		 * @param {Button} buttonInfo to add to the group
128 		 */
129 		addButton: function(buttonInfo) {
130 			if (buttonInfo.button instanceof Aloha.ui.AttributeField) {
131 				if (this.fields.length < 2) {
132 					this.fields.push(buttonInfo);
133 				} else {
134 					throw new Error("Too much fields in this group");
135 				}
136 			} else {
137 				// Every plugin API entryPoint (method) should be securised enough
138 				// to avoid Aloha to block at startup even
139 				// if a plugin is badly designed
140 				if (typeof buttonInfo.button !== "undefined"){
141 					this.buttons.push(buttonInfo);
142 				}
143 			}
144 		},
145 
146 		/**
147 		 * Get the EXT component representing the group (Ext.ButtonGroup)
148 		 * @return the Ext.ButtonGroup
149 		 * @hide
150 		 */
151 		getExtComponent: function () {
152 			var that = this, l,
153 				items = [],
154 				buttonCount = 0,
155 				columnCount = 0,
156 				len, idx, half;
157 
158 
159 			if (typeof this.extButtonGroup === 'undefined') {
160 			
161 				if (this.fields.length > 1) {
162 					columnCount = 1;
163 				}
164 
165 				jQuery.each(this.buttons, function(index, button) {
166 					// count the number of buttons (large buttons count as 2)
167 					buttonCount += button.button.size == 'small' ? 1 : 2;
168 				});
169 				columnCount = columnCount + Math.ceil(buttonCount / 2);
170 
171 				len = this.buttons.length;
172 				idx = 0;
173 				half =  Math.ceil(this.buttons.length / 2) - this.buttons.length % 2 ;
174 
175 				if (this.fields.length > 0) {
176 					that.buttons.push(this.fields[0]);
177 					items.push(this.fields[0].button.getExtConfigProperties());
178 				}
179 
180 				while (--len >= half) {
181 					items.push(this.buttons[idx++].button.getExtConfigProperties());
182 				}
183 				++len;
184 				if (this.fields.length > 1) {
185 					that.buttons.push(this.fields[1]);
186 					items.push(this.fields[1].button.getExtConfigProperties());
187 				}
188 				while (--len >=0) {
189 					items.push(this.buttons[idx++].button.getExtConfigProperties());
190 				}
191 
192 				this.extButtonGroup = new Ext.ButtonGroup({
193 					'columns' : columnCount,
194 					'items': items
195 				});
196 
197 	//			jQuery.each(this.fields, function(id, field){
198 	//				that.buttons.push(field);
199 	//			});
200 				// now find the Ext.Buttons and set to the GENTICS buttons
201 				jQuery.each(this.buttons, function(index, buttonInfo) {
202 					buttonInfo.button.extButton = that.extButtonGroup.findById(buttonInfo.button.id);
203 					// the following code is a work arround because ExtJS initializes later.
204 					// The ui wrapper store the information and here we use it... ugly.
205 					// if there are any listeners added before initializing the extButtons
206 					if ( buttonInfo.button.listenerQueue && buttonInfo.button.listenerQueue.length > 0 ) {
207 						while ( true ) {
208 							l = buttonInfo.button.listenerQueue.shift();
209 							if ( !l ) {break;}
210 							buttonInfo.button.extButton.addListener(l.eventName, l.handler, l.scope, l.options);
211 						}
212 					}
213 					if (buttonInfo.button.extButton.setObjectTypeFilter) {
214 						if (buttonInfo.button.objectTypeFilter) {
215 							buttonInfo.button.extButton.noQuery = false;
216 						}
217 						if ( buttonInfo.button.objectTypeFilter == 'all' ) {
218 							buttonInfo.button.objectTypeFilter = null;
219 						}
220 						buttonInfo.button.extButton.setObjectTypeFilter(buttonInfo.button.objectTypeFilter);
221 						if ( buttonInfo.button.displayField) {
222 							buttonInfo.button.extButton.displayField = buttonInfo.button.displayField;
223 						}
224 						if ( buttonInfo.button.tpl ) {
225 							buttonInfo.button.extButton.tpl = buttonInfo.button.tpl;
226 						}
227 					}
228 				});
229 			}
230 
231 			return this.extButtonGroup;
232 		},
233 
234 		/**
235 		 * Recalculate the visibility of the buttons and the group
236 		 * @hide
237 		 */
238 		doLayout: function () {
239 			var groupVisible = false,
240 				that = this;
241 			jQuery.each(this.buttons, function(index, button) {
242 				if (typeof button.button !== "undefined") {
243 					var extButton = that.extButtonGroup.findById(button.button.id),
244 					buttonVisible = button.button.isVisible() && button.scopeVisible;
245 					
246 					if (!extButton) {
247 						return;
248 					}
249 				
250 					if (buttonVisible && extButton.hidden) {
251 						extButton.show();
252 					} else if (!buttonVisible && extButton && !extButton.hidden) {
253 						extButton.hide();
254 					}
255 				
256 					groupVisible |= buttonVisible;
257 				}
258 			});
259 			if (groupVisible && this.extButtonGroup.hidden) {
260 				this.extButtonGroup.show();
261 			} else if (!groupVisible && !this.extButtonGroup.hidden) {
262 				this.extButtonGroup.hide();
263 			}
264 		
265 			return groupVisible;
266 
267 		}
268 	});
269 
270 	//=========================================================================
271 	//
272 	// Floating Menu
273 	//
274 	//=========================================================================
275 
276 	var lastFloatingMenuPos = {
277 		top: null,
278 		left: null
279 	};
280 
281 	/**
282 	 * Handler for window scroll event.  Positions the floating menu
283 	 * appropriately.
284 	 *
285 	 * @param {Aloha.FloatingMenu} floatingmenu
286 	 */
287 	function onWindowScroll( floatingmenu ) {
288 		if ( !Aloha.activeEditable ) {
289 			return;
290 		}
291 
292 		var element = floatingmenu.obj;
293 		var editablePos = Aloha.activeEditable.obj.offset();
294 		var isTopAligned = floatingmenu.behaviour === 'topalign';
295 		var isAppended = floatingmenu.behaviour === 'append';
296 		var isManuallyPinned = floatingmenu.pinned
297 							 && ( parseInt( element.css( 'left' ), 10 )
298 								  != ( editablePos.left
299 									   + floatingmenu.horizontalOffset
300 									 ) );
301 
302 		// no calculation when pinned manually or has behaviour 'append'
303 		if ( isTopAligned && isManuallyPinned || isAppended ) {
304 			return;
305 		}
306 
307 		var floatingmenuHeight = element.height();
308 		var scrollTop = jQuery( document ).scrollTop();
309 
310 		// This value is what the top position of the floating menu *would* be
311 		// if we tried to position it above the active editable.
312 		var floatingmenuTop = editablePos.top - floatingmenuHeight
313 		                    + floatingmenu.marginTop
314 		                    - floatingmenu.topalignOffset;
315 
316 		// The floating menu does not fit in the space between the top of the
317 		// viewport and the editable, so position it at the top of the viewport
318 		// and over the editable.
319 		if ( scrollTop > floatingmenuTop ) {
320 			editablePos.top = isTopAligned
321 							? scrollTop + floatingmenu.marginTop
322 							: floatingmenu.marginTop;
323 
324 		// There is enough space on top of the editable to fit the entire
325 		// floating menu, so we do so.
326 		} else if ( scrollTop <= floatingmenuTop ) {
327 			editablePos.top -= floatingmenuHeight
328 							 + ( isTopAligned
329 								 ? floatingmenu.marginTop +
330 								   floatingmenu.topalignOffset
331 								 : 0 );
332 		}
333 
334 		floatingmenu.floatTo( editablePos );
335 	}
336 
337 	/**
338 	 * Aloha's Floating Menu
339 	 * @namespace Aloha
340 	 * @class FloatingMenu
341 	 * @singleton
342 	 */
343 	var FloatingMenu = Class.extend({
344 		/**
345 		 * Define the default scopes
346 		 * @property
347 		 * @type Object
348 		 */
349 		scopes: {
350 			'Aloha.empty' : {
351 				'name' : 'Aloha.empty',
352 				'extendedScopes' : [],
353 				'buttons' : []
354 			},
355 			'Aloha.global' : {
356 				'name' : 'Aloha.global',
357 				'extendedScopes' : ['Aloha.empty'],
358 				'buttons' : []
359 			},
360 			'Aloha.continuoustext' : {
361 				'name' : 'Aloha.continuoustext',
362 				'extendedScopes' : ['Aloha.global'],
363 				'buttons' : []
364 			}
365 		},
366 
367 		/**
368 		 * Array of tabs within the floatingmenu
369 		 * @hide
370 		 */
371 		tabs: [],
372 
373 		/**
374 		 * 'Map' of tabs (for easy access)
375 		 * @hide
376 		 */
377 		tabMap: {},
378 
379 		/**
380 		 * Flag to mark whether the floatingmenu is initialized
381 		 * @hide
382 		 */
383 		initialized: false,
384 
385 		/**
386 		 * Array containing all buttons
387 		 * @hide
388 		 */
389 		allButtons: [],
390 
391 		/**
392 		 * top part of the floatingmenu position
393 		 * @hide
394 		 */
395 		top: 100,
396 
397 		/**
398 		 * left part of the floatingmenu position
399 		 * @hide
400 		 */
401 		left: 100,
402 
403 		/**
404 		 * store pinned status - true, if the FloatingMenu is pinned
405 		 * @property
406 		 * @type boolean
407 		 */
408 		pinned: false,
409 
410 		/**
411 		 * just a reference to the jQuery(window) object, which is used quite often
412 		 */
413 		window: jQuery(window),
414 
415 		/**
416 		 * Aloha.settings.floatingmenu.behaviour
417 		 * 
418 		 * Is used to define the floating menu (fm) float behaviour.
419 		 *
420 		 * available: 
421 		 *  'float' (default) the fm will float next to the position where the caret is,
422 		 *  'topalign' the fm is fixed above the contentEditable which is active,
423 		 *  'append' the fm is appended to the defined 'element' element position (top/left)
424 		 */
425 		behaviour: 'float',
426 
427 		/**
428 		 * Aloha.settings.floatingmenu.element
429 		 *
430 		 * Is used to define the element where the floating menu is positioned when
431 		 * Aloha.settings.floatingmenu.behaviour is set to 'append'
432 		 * 
433 		 */
434 		element: 'floatingmenu',
435 
436 		/**
437 		 * topalign offset to be used for topalign behavior
438 		 */
439 		topalignOffset: 0,
440 		
441 		/**
442 		 * topalign offset to be used for topalign behavior
443 		 */
444 		horizontalOffset: 0,
445 		
446 		/**
447 		 * will only be hounoured when behaviour is set to 'topalign'. Adds a margin,
448 		 * so the floating menu is not directly attached to the top of the page
449 		 */
450 		marginTop: 10,
451 		
452 		/**
453 		 * Define whether the floating menu shall be draggable or not via Aloha.settings.floatingmanu.draggable
454 		 * Default is: true 
455 		 */
456 		draggable: true,
457 		
458 		/**
459 		 * Define whether the floating menu shall be pinned or not via Aloha.settings.floatingmanu.pin
460 		 * Default is: false 
461 		 */
462 		pin: false,
463 		
464 		/**
465 		 * A list of all buttons that have been added to the floatingmenu
466 		 * This needs to be tracked, as adding buttons twice will break the fm
467 		 */
468 		buttonsAdded: [],
469 		
470 		/**
471 		 * Will be initialized by checking Aloha.settings.toolbar, which will contain the config for
472 		 * the floating menu. If there is no config, tabs and groups will be generated programmatically
473 		 */
474 		fromConfig: false,
475 
476 		/**
477 		 * Initialize the floatingmenu
478 		 * @hide
479 		 */
480 		init: function() {
481 
482 			// check for behaviour setting of the floating menu
483 		    if ( Aloha.settings.floatingmenu ) {
484 		    	if ( typeof Aloha.settings.floatingmenu.draggable ===
485 				         'boolean' ) {
486 		    		this.draggable = Aloha.settings.floatingmenu.draggable;
487 		    	}
488 
489 				if ( typeof Aloha.settings.floatingmenu.behaviour ===
490 				         'string' ) {
491 					this.behaviour = Aloha.settings.floatingmenu.behaviour;
492 				}
493 
494 				if ( typeof Aloha.settings.floatingmenu.topalignOffset !==
495 					    'undefined' ) {
496 					this.topalignOffset = parseInt(
497 						Aloha.settings.floatingmenu.topalignOffset, 10 );
498 				}
499 
500 				if ( typeof Aloha.settings.floatingmenu.horizontalOffset !==
501 				         'undefined' ) {
502 					this.horizontalOffset = parseInt(
503 						Aloha.settings.floatingmenu.horizontalOffset , 10 );
504 				}
505 
506 				if ( typeof Aloha.settings.floatingmenu.marginTop ===
507 				         'number' ) {
508 				    this.marginTop = parseInt(
509 						Aloha.settings.floatingmenu.marginTop , 10 );
510 				}
511 
512 				if ( typeof Aloha.settings.floatingmenu.element ===
513 						'string' ) {
514 					this.element = Aloha.settings.floatingmenu.element;
515 				}
516 				if ( typeof Aloha.settings.floatingmenu.pin ===
517 						'boolean' ) {
518 					this.pin = Aloha.settings.floatingmenu.pin;
519 				}
520 
521 
522 				if ( typeof Aloha.settings.floatingmenu.width !==
523 				         'undefined' ) {
524 					this.width = parseInt( Aloha.settings.floatingmenu.width,
525 						10 );
526 				}
527 		    }
528 
529 			jQuery.storage = new jQuery.store();
530 
531 			this.currentScope = 'Aloha.global';
532 
533 			var that = this;
534 
535 			this.window.unload(function () {
536 				// store fm position if the panel is pinned to be able to restore it next time
537 				if (that.pinned) {
538 					jQuery.storage.set('Aloha.FloatingMenu.pinned', 'true');
539 					jQuery.storage.set('Aloha.FloatingMenu.top', that.top);
540 					jQuery.storage.set('Aloha.FloatingMenu.left', that.left);
541 					if (Aloha.Log.isInfoEnabled()) {
542 						Aloha.Log.info(this, 'stored FloatingMenu pinned position {' + that.left
543 								+ ', ' + that.top + '}');
544 					}
545 				} else {
546 					// delete old localStorages
547 					jQuery.storage.del('Aloha.FloatingMenu.pinned');
548 					jQuery.storage.del('Aloha.FloatingMenu.top');
549 					jQuery.storage.del('Aloha.FloatingMenu.left');
550 				}
551 				if (that.userActivatedTab) {
552 					jQuery.storage.set('Aloha.FloatingMenu.activeTab', that.userActivatedTab);
553 				}
554 			}).resize(function () {
555 				if (that.behaviour === 'float') {
556 					if (that.pinned) {
557 						that.fixPinnedPosition();
558 						that.refreshShadow();
559 						that.extTabPanel.setPosition(that.left, that.top);
560 					} else {
561 						var target = that.calcFloatTarget(Aloha.Selection.getRangeObject());
562 						if (target) {
563 							that.floatTo(target);
564 						}
565 					}
566 				}
567 			});
568 			Aloha.bind('aloha-ready', function() {
569 				that.generateComponent();
570 				that.initialized = true;
571 			});
572 			
573 			if (typeof Aloha.settings.toolbar === 'object') {
574 				this.fromConfig = true;
575 			}
576 		},
577 
578 		/**
579 		 * jQuery reference to the extjs tabpanel
580 		 * @hide
581 		 */
582 		obj: null,
583 
584 		/**
585 		 * jQuery reference to the shadow obj
586 		 * @hide
587 		 */
588 		shadow: null,
589 
590 		/**
591 		 * jQuery reference to the panels body wrap div
592 		 * @hide
593 		 */
594 		panelBody: null,
595 		
596 		/**
597 		 * The panels width
598 		 * @hide
599 		 */
600 		width: 400,
601 
602 		/**
603 		 * initialize tabs and groups according to the current configuration
604 		 */
605 		initTabsAndGroups: function () {
606 			var that = this;
607 			
608 			// if there is no toolbar config tabs and groups have been initialized before
609 			if (!this.fromConfig) {
610 				return;
611 			}
612 			
613 			jQuery.each(Aloha.settings.toolbar.tabs, function (tab, groups) {
614 				// generate or retrieve tab
615 				var tabObject = that.tabMap[tab];
616 				if (typeof tabObject === 'undefined') {
617 					// the tab object does not yet exist, so create a new tab and add it to the list
618 					tabObject = new Tab(tab);
619 					that.tabs.push(tabObject);
620 					that.tabMap[tab] = tabObject;
621 				}
622 				
623 				// generate groups for current tab
624 				jQuery.each(groups, function (group, buttons) {
625 					var groupObject = tabObject.getGroup(group),
626 						i;
627 
628 					// now get all the buttons for that group
629 					jQuery.each(buttons, function (j, button) {
630 						if (jQuery.inArray(button, that.buttonsAdded) !== -1) {
631 							// buttons must not be added twice
632 							console.warn('Skipping button {' + button + '}. A button can\'t be added ' + 
633 								'to the floating menu twice. Config key: {Aloha.settings.toolbar.' + 
634 									tab + '.' + group + '}');
635 									return;
636 						}
637 						
638 						// now add the button to the group
639 						for (i = 0; i < that.allButtons.length; i++) {
640 							if (button === that.allButtons[i].button.name) {
641 								groupObject.addButton(that.allButtons[i]);
642 								// remember that we've added the button
643 								that.buttonsAdded.push(that.allButtons[i].button.name);
644 								break;
645 							}
646 						}
647 					});
648 				});
649 			});
650 		},
651 
652 		/**
653 		 * Generate the rendered component for the floatingmenu
654 		 * @hide
655 		 */
656 		generateComponent: function () {
657 			var that = this, pinTab;
658 
659 			// initialize tabs and groups first
660 			this.initTabsAndGroups();
661 
662 			// Initialize and configure the tooltips
663 			Ext.QuickTips.init();
664 			Ext.apply(Ext.QuickTips.getQuickTip(), {
665 				minWidth : 10
666 			});
667 
668 			
669 			
670 			if (this.extTabPanel) {
671 				// TODO dispose of the ext component
672 			} else {
673 				
674 				// Enable or disable the drag functionality
675 				var dragConfiguration = false;
676 
677 				if ( that.draggable ) {
678 					dragConfiguration = {
679 						insertProxy: false,
680 						onDrag : function(e) {
681 							var pel = this.proxy.getEl();
682 							this.x = pel.getLeft(true);
683 							this.y = pel.getTop(true);
684 							this.panel.shadow.hide();
685 						},
686 						endDrag : function(e) {
687 							var top = (that.pinned) ? this.y - jQuery(document).scrollTop() : this.y;
688 					
689 							that.left = this.x;
690 							that.top = top;
691 
692 							// store the last floating menu position when the floating menu was dragged around
693 							lastFloatingMenuPos.left = that.left;
694 							lastFloatingMenuPos.top = that.top;
695 
696 							this.panel.setPosition(this.x, top);
697 							that.refreshShadow();
698 							this.panel.shadow.show();
699 						}
700 					};
701 				}
702 				// generate the tabpanel object
703 				this.extTabPanel = new Ext.TabPanel({
704 					activeTab: 0,
705 					width: that.width, // 336px this fits the multisplit button and 6 small buttons placed in 3 cols
706 					plain: false,
707 					draggable: dragConfiguration,
708 					floating: {shadow: false},
709 					defaults: {
710 						autoScroll: true
711 					},
712 					layoutOnTabChange : true,
713 					shadow: false,
714 					cls: 'aloha-floatingmenu ext-root',
715 					listeners : {
716 						'tabchange' : {
717 							'fn' : function(tabPanel, tab) {
718 								if (tab.title != that.autoActivatedTab) {
719 									if (Aloha.Log.isDebugEnabled()) {
720 										Aloha.Log.debug(that, 'User selected tab ' + tab.title);
721 									}
722 									// remember the last user-selected tab
723 									that.userActivatedTab = tab.title;
724 								} else {
725 									if (Aloha.Log.isDebugEnabled()) {
726 										Aloha.Log.debug(that, 'Tab ' + tab.title + ' was activated automatically');
727 									}
728 								}
729 								that.autoActivatedTab = undefined;
730 						
731 								// ok, this is kind of a hack: when the tab changes, we check all buttons for multisplitbuttons (which have the method setActiveDOMElement).
732 								// if a DOM Element is queued to be set active, we try to do this now.
733 								// the reason for this is that the active DOM element can only be set when the multisplit button is currently visible.
734 								jQuery.each(that.allButtons, function(index, buttonInfo) {
735 									if (typeof buttonInfo.button !== 'undefined'
736 										&& typeof buttonInfo.button.extButton !== 'undefined'
737 										&& buttonInfo.button.extButton !== null
738 										&& typeof buttonInfo.button.extButton.setActiveDOMElement === 'function') {
739 										if (typeof buttonInfo.button.extButton.activeDOMElement !== 'undefined') {
740 											buttonInfo.button.extButton.setActiveDOMElement(buttonInfo.button.extButton.activeDOMElement);
741 										}
742 									}
743 								});
744 						
745 								// adapt the shadow
746 								if (that.extTabPanel.isVisible()) {
747 									that.extTabPanel.shadow.show();
748 									that.refreshShadow();
749 								}
750 							}
751 						}
752 					},
753 					enableTabScroll : true
754 				});
755 		
756 		
757 			}
758 
759 			// add the tabs
760 			jQuery.each(this.tabs, function(index, tab) {
761 				// let each tab generate its ext component and add them to the panel
762 				try {
763 					if (!tab.extPanel) {
764 						// if the tab itself was not generated, we do this and add it to the panel
765 						that.extTabPanel.add(tab.getExtComponent());
766 					} else {
767 						// otherwise, we will make sure that probably missing groups are generated, but don't add the tab to the menu (again)
768 						tab.getExtComponent();
769 					}
770 				} catch(e) {
771 					Aloha.Log.error(that,"Error while inserting tab: " + e);
772 				}
773 			});
774 
775 			// add the dropshadow
776 			if (!this.extTabPanel.shadow) {
777 				this.extTabPanel.shadow = jQuery('<div id="aloha-floatingmenu-shadow" class="aloha-shadow"> </div>').hide();
778 				jQuery('body').append(this.extTabPanel.shadow);
779 			}
780 
781 			// add an empty pin tab item, store reference
782 			pinTab = this.extTabPanel.add({
783 				title : ' '
784 			});
785 
786 			// finally render the panel to the body
787 			this.extTabPanel.render(document.body);
788 
789 			// finish the pin element after the FM has rendered (before there are noe html contents to be manipulated
790 			jQuery(pinTab.tabEl)
791 				.addClass('aloha-floatingmenu-pin')
792 				.html(' ')
793 				.mousedown(function (e) {
794 					that.togglePin();
795 					// Note: this event is deliberately stopped here, although normally,
796 					// we would set the flag GENTICS.Aloha.eventHandled instead.
797 					// But when the event bubbles up, no tab would be selected and
798 					// the floatingmenu would be rather thin.
799 					e.stopPropagation();
800 				});
801 
802 			// a reference to the panels body needed for shadow size & position
803 			this.panelBody = jQuery('div.aloha-floatingmenu div.x-tab-panel-bwrap');
804 
805 			// do the visibility
806 			this.doLayout();
807 
808 			// bind jQuery reference to extjs obj
809 			// this has to be done AFTER the tab panel has been rendered
810 			this.obj = jQuery(this.extTabPanel.getEl().dom);
811 
812 			if (jQuery.storage.get('Aloha.FloatingMenu.pinned') == 'true') {
813 				//this.togglePin();
814 
815 				this.top = parseInt(jQuery.storage.get('Aloha.FloatingMenu.top'),10);
816 				this.left = parseInt(jQuery.storage.get('Aloha.FloatingMenu.left'),10);
817 
818 				// do some positioning fixes
819 				this.fixPinnedPosition();
820 
821 				if (Aloha.Log.isInfoEnabled()) {
822 					Aloha.Log.info(this, 'restored FloatingMenu pinned position {' + this.left + ', ' + this.top + '}');
823 				}
824 
825 				this.refreshShadow();
826 			}
827 
828 			// set the user activated tab stored in a localStorage
829 			if (jQuery.storage.get('Aloha.FloatingMenu.activeTab')) {
830 				this.userActivatedTab = jQuery.storage.get('Aloha.FloatingMenu.activeTab');
831 			}
832 
833 			// for now, position the panel somewhere
834 			this.extTabPanel.setPosition(this.left, this.top);
835 
836 			// mark the event being handled by aloha, because we don't want to recognize
837 			// a click into the floatingmenu to be a click into nowhere (which would
838 			// deactivate the editables)
839 			this.obj.mousedown(function (e) {
840 				e.originalEvent.stopSelectionUpdate = true;
841 				Aloha.eventHandled = true;
842 				//e.stopSelectionUpdate = true;
843 			});
844 
845 			this.obj.mouseup(function (e) {
846 				e.originalEvent.stopSelectionUpdate = true;
847 				Aloha.eventHandled = false;
848 			});
849 
850 			jQuery( window ).scroll(function() {
851 				onWindowScroll( that );
852 			});
853 
854 			// don't display the drag handle bar / pin when floating menu is not draggable
855 			if ( !that.draggable ) {
856 				jQuery('.aloha-floatingmenu').hover( function() {
857 					jQuery(this).css({background: 'none'});
858 					jQuery('.aloha-floatingmenu-pin').hide();
859 				});
860 			}
861 
862 			// adjust float behaviour
863 			if (this.behaviour === 'float') {
864 				// listen to selectionChanged event
865 				Aloha.bind('aloha-selection-changed',function(event, rangeObject) {
866 					if (!that.pinned) {
867 						var pos = that.calcFloatTarget(rangeObject);
868 						if (pos) {
869 							that.floatTo(pos);
870 						}
871 					}
872 				});
873 			} else if (this.behaviour === 'append' ) {
874 				var p = jQuery( "#" + that.element );
875 				var position = p.offset();
876 
877 				if ( !position ) {
878 					Aloha.Log.warn(that, 'Invalid element HTML ID for floating menu: ' + that.element);
879 					return false;
880 				}
881 
882 				// set the position so that it does not float on the first editable activation
883 				this.floatTo( position );
884 				
885 				if ( this.pin ) {
886 					this.togglePin( true );
887 				}
888 
889 				Aloha.bind( 'aloha-editable-activated', function( event, data ) {
890 					if ( that.pinned ) {
891 						return;
892 					}
893 					that.floatTo( position );
894 				});
895 				
896 		    } else if ( this.behaviour === 'topalign' ) {
897 				// topalign will retain the user's pinned status
898 				// TODO maybe the pin should be hidden in that case?
899 				this.togglePin( false );
900 
901 				// Float the menu to the editable that is activated.
902 				Aloha.bind( 'aloha-editable-activated', function( event, data ) {
903 					if ( that.pinned ) {
904 						return;
905 					}
906 
907 					// FIXME: that.obj.height() does not return the correct
908 					//        height of the editable.  We need to fix this, and
909 					//        not hard-code the height as we currently do.
910 					var editable = data.editable.obj;
911 					var floatingmenuHeight = 90;
912 					var editablePos = editable.offset();
913 					var isFloatingmenuAboveViewport = ( (
914 						editablePos.top - floatingmenuHeight )
915 						    < jQuery( document ).scrollTop() );
916 
917 					if ( isFloatingmenuAboveViewport ) {
918 						// Since we don't have space to place the floatingmenu
919 						// above the editable, we want to place it over the
920 						// editable instead.  But if the editable is shorter
921 						// than the floatingmenu, it would be completely
922 						// covered by it, and so, in such cases, we position
923 						// the editable at the bottom of the short editable.
924 						editablePos.top = ( editable.height()
925 						         < floatingmenuHeight )
926 							? editablePos.top + editable.height()
927 							: jQuery( document ).scrollTop();
928 
929 						editablePos.top += that.marginTop;
930 					} else {
931 						editablePos.top -= floatingmenuHeight
932 						                 + that.topalignOffset;
933 					}
934 
935 					editablePos.left += that.horizontalOffset;
936 
937 					var HORIZONTAL_PADDING = 10;
938 					// Calculate by how much the floating menu is pocking
939 					// outside the width of the viewport.  A positive number
940 					// means that it is outside the viewport, negative means
941 					// it is within the viewport.
942 					var overhang = ( ( editablePos.left + that.width
943 						+ HORIZONTAL_PADDING ) - jQuery(window).width() );
944 
945 					if ( overhang > 0 ) {
946 						editablePos.left -= overhang;	
947 					}
948 
949 					that.floatTo( editablePos );
950 				});
951 			}
952 		},
953 
954 		/**
955 		 * Fix the position of the pinned floatingmenu to keep it visible
956 		 */
957 		fixPinnedPosition: function() {
958 			// if not pinned, do not fix the position
959 			if (!this.pinned) {
960 				return;
961 			}
962 
963 			// fix the position of the floatingmenu, to keep it visible
964 			if (this.top < 30) {
965 				// from top position, we want to have 30px margin
966 				this.top = 30;
967 			} else if (this.top > this.window.height() - this.extTabPanel.getHeight()) {
968 				this.top = this.window.height() - this.extTabPanel.getHeight();
969 			}
970 			if (this.left < 0) {
971 				this.left = 0;
972 			} else if (this.left > this.window.width() - this.extTabPanel.getWidth()) {
973 				this.left = this.window.width() - this.extTabPanel.getWidth();
974 			}
975 		},
976 
977 		/**
978 		 * reposition & resize the shadow
979 		 * the shadow must not be repositioned outside this method!
980 		 * position calculation is based on this.top and this.left coordinates
981 		 * @method
982 		 */
983 		refreshShadow: function (resize) {
984 			if (this.panelBody) {
985 				var props = {
986 					'top': this.top + 24, // 24px top offset to reflect tab bar height
987 					'left': this.left
988 				};
989 
990 				if(typeof resize === 'undefined' || !resize) {
991 					props.width = this.panelBody.width() + 'px';
992 					props.height = this.panelBody.height() + 'px';
993 				}
994 
995 				this.extTabPanel.shadow.css(props);
996 			}
997 		},
998 
999 		/**
1000 		 * toggles the pinned status of the floating menu
1001 		 * @method
1002 		 * @param {boolean} pinned set to true to activate pin, or set to false to deactivate pin. 
1003 		 *             leave undefined to toggle pin status automatically
1004 		 */
1005 		togglePin: function(pinned) {
1006 			var el = jQuery('.aloha-floatingmenu-pin');
1007 		       
1008 		    if (typeof pinned === 'boolean') {
1009 			this.pinned = !pinned;
1010 		    }
1011 		       
1012 			if (this.pinned) {
1013 				el.removeClass('aloha-floatingmenu-pinned');
1014 				this.top = this.obj.offset().top;
1015 
1016 				this.obj.removeClass('fixed').css({
1017 					'top': this.top
1018 				});
1019 
1020 				this.extTabPanel.shadow.removeClass('fixed');
1021 				this.refreshShadow();
1022 
1023 				this.pinned = false;
1024 			} else {
1025 				el.addClass('aloha-floatingmenu-pinned');
1026 				this.top = this.obj.offset().top - this.window.scrollTop();
1027 
1028 				this.obj.addClass('fixed').css({
1029 					'top': this.top // update position for fixed position
1030 				});
1031 
1032 				// do the same for the shadow
1033 				this.extTabPanel.shadow.addClass('fixed');//props.start
1034 				this.refreshShadow();
1035 
1036 				this.pinned = true;
1037 			}
1038 		},
1039 
1040 		/**
1041 		 * Create a new scopes
1042 		 * @method
1043 		 * @param {String} scope name of the new scope (should be namespaced for uniqueness)
1044 		 * @param {String} extendedScopes Array of scopes this scope extends. Can also be a single String if
1045 		 *            only one scope is extended, or omitted if the scope should extend
1046 		 *            the empty scope
1047 		 */
1048 		createScope: function(scope, extendedScopes) {
1049 			if (typeof extendedScopes === 'undefined') {
1050 				extendedScopes = ['Aloha.empty'];
1051 			} else if (typeof extendedScopes === 'string') {
1052 				extendedScopes = [extendedScopes];
1053 			}
1054 
1055 			// TODO check whether the extended scopes already exist
1056 
1057 			if (this.scopes[scope]) {
1058 				// TODO what if the scope already exists?
1059 			} else {
1060 				// generate the new scope
1061 				this.scopes[scope] = {'name' : scope, 'extendedScopes' : extendedScopes, 'buttons' : []};
1062 			}
1063 		},
1064 
1065 		/**
1066 		 * Adds a button to the floatingmenu
1067 		 * @method
1068 		 * @param {String} scope the scope for the button, should be generated before (either by core or the plugin)
1069 		 * @param {Button} button instance of Aloha.ui.button to add at the floatingmenu
1070 		 * @param {String} tab label of the tab to which the button is added
1071 		 * @param {int} group index of the button group in the tab, lowest index is left
1072 		 */
1073 		addButton: function(scope, button, tab, group) {
1074 			// check whether the scope exists
1075 			var
1076 				scopeObject = this.scopes[scope],
1077 				buttonInfo, tabObject, groupObject;
1078 		
1079 			if (!button.name) {
1080 				console.warn('Added button with iconClass {' + button.iconClass + '} which has no property "name"');
1081 			}
1082 	
1083 			if (typeof scopeObject === 'undefined') {
1084 				Aloha.Log.error("Can't add button to given scope since the scope has not yet been initialized.", scope);
1085 				return false;
1086 			}
1087 
1088 			// generate a buttonInfo object
1089 			buttonInfo = { 'button' : button, 'scopeVisible' : false };
1090 
1091 			// add the button to the list of all buttons
1092 			this.allButtons.push(buttonInfo);
1093 
1094 			// add the button to the scope
1095 			scopeObject.buttons.push(buttonInfo);
1096 
1097 			// if there is no toolbar config tabs and groups will be generated right away
1098 			if (!this.fromConfig) {
1099 				// get the tab object
1100 				tabObject = this.tabMap[tab];
1101 				if (typeof tabObject === 'undefined') {
1102 					// the tab object does not yet exist, so create a new tab and add it to the list
1103 					tabObject = new Tab(tab);
1104 					this.tabs.push(tabObject);
1105 					this.tabMap[tab] = tabObject;
1106 				}
1107 
1108 				// get the group
1109 				groupObject = tabObject.getGroup(group);
1110 
1111 				// now add the button to the group
1112 				groupObject.addButton(buttonInfo);
1113 			}
1114 
1115 			// finally, when the floatingmenu is already initialized, we need to create the ext component now
1116 			if (this.initialized) {
1117 				this.generateComponent();
1118 			}
1119 		},
1120 
1121 		/**
1122 		 * Recalculate the visibility of tabs, groups and buttons (depending on scope and button hiding)
1123 		 * @hide
1124 		 */
1125 		doLayout: function () {
1126 			if (Aloha.Log.isDebugEnabled()) {
1127 				Aloha.Log.debug(this, 'doLayout called for FloatingMenu, scope is ' + this.currentScope);
1128 			}
1129 
1130 			// if there's no floatingmenu don't do anything
1131 			if ( typeof this.extTabPanel === 'undefined' ) {
1132 				return false;
1133 			}
1134 
1135 			var that = this,
1136 				firstVisibleTab = false,
1137 				activeExtTab = this.extTabPanel.getActiveTab(),
1138 				activeTab = false,
1139 				floatingMenuVisible = false,
1140 				showUserActivatedTab = false,
1141 				pos;
1142 			
1143 			// let the tabs layout themselves
1144 			jQuery.each(this.tabs, function(index, tab) {
1145 				// remember the active tab
1146 				if (tab.extPanel == activeExtTab) {
1147 					activeTab = tab;
1148 				}
1149 
1150 				// remember whether the tab is currently visible
1151 				var tabVisible = tab.visible;
1152 
1153 				// let each tab generate its ext component and add them to the panel
1154 				if (tab.doLayout()) {
1155 					// found a visible tab, so the floatingmenu needs to be visible as well
1156 					floatingMenuVisible = true;
1157 
1158 					// make sure the tabstrip is visible
1159 					if (!tabVisible) {
1160 						if (Aloha.Log.isDebugEnabled()) {
1161 							Aloha.Log.debug(that, 'showing tab strip for tab ' + tab.label);
1162 						}
1163 						that.extTabPanel.unhideTabStripItem(tab.extPanel);
1164 					}
1165 
1166 					// remember the first visible tab
1167 					if (!firstVisibleTab) {
1168 						// this is the first visible tab (in case we need to switch to it)
1169 						firstVisibleTab = tab;
1170 					}
1171 
1172 					// check whether this visible tab is the last user activated tab and currently not active
1173 					if (that.userActivatedTab == tab.extPanel.title && tab.extPanel != activeExtTab) {
1174 						showUserActivatedTab = tab;
1175 					}
1176 				} else {
1177 					// make sure the tabstrip is hidden
1178 					if (tabVisible) {
1179 						if (Aloha.Log.isDebugEnabled()) {
1180 							Aloha.Log.debug(that, 'hiding tab strip for tab ' + tab.label);
1181 						}
1182 						that.extTabPanel.hideTabStripItem(tab.extPanel);
1183 					}
1184 				}
1185 			});
1186 
1187 			// check whether the last tab which was selected by the user is visible and not the active tab
1188 			if (showUserActivatedTab) {
1189 				if (Aloha.Log.isDebugEnabled()) {
1190 					Aloha.Log.debug(this, 'Setting active tab to ' + showUserActivatedTab.label);
1191 				}
1192 				this.extTabPanel.setActiveTab(showUserActivatedTab.extPanel);
1193 			} else if (typeof activeTab === 'object' && typeof firstVisibleTab === 'object') {
1194 				// now check the currently visible tab, whether it is visible and enabled
1195 				if (!activeTab.visible) {
1196 					if (Aloha.Log.isDebugEnabled()) {
1197 						Aloha.Log.debug(this, 'Setting active tab to ' + firstVisibleTab.label);
1198 					}
1199 					this.autoActivatedTab = firstVisibleTab.extPanel.title;
1200 					this.extTabPanel.setActiveTab(firstVisibleTab.extPanel);
1201 				}
1202 			}
1203 
1204 			// set visibility of floatingmenu
1205 			if (floatingMenuVisible && this.extTabPanel.hidden) {
1206 				// set the remembered position
1207 				this.extTabPanel.show();
1208 				this.refreshShadow();
1209 				this.extTabPanel.shadow.show();
1210 				this.extTabPanel.setPosition(this.left, this.top);
1211 			} else if (!floatingMenuVisible && !this.extTabPanel.hidden) {
1212 				// remember the current position
1213 				pos = this.extTabPanel.getPosition(true);
1214 				// restore previous position if the fm was pinned
1215 				this.left = pos[0] < 0 ? 100 : pos[0];
1216 				this.top = pos[1] < 0 ? 100 : pos[1];
1217 				this.extTabPanel.hide();
1218 				this.extTabPanel.shadow.hide();
1219 			} /*else {
1220 				var target = that.calcFloatTarget(Aloha.Selection.getRangeObject());
1221 				if (target) {
1222 					this.left = target.left;
1223 					this.top = target.top;
1224 					this.extTabPanel.show();
1225 					this.refreshShadow();
1226 					this.extTabPanel.shadow.show();
1227 					this.extTabPanel.setPosition(this.left, this.top);
1228 
1229 					that.floatTo(target);
1230 				}
1231 			}*/
1232 
1233 			// let the Ext object render itself again
1234 			this.extTabPanel.doLayout();
1235 		},
1236 
1237 		/**
1238 		 * Set the current scope
1239 		 * @method
1240 		 * @param {String} scope name of the new current scope
1241 		 */
1242 		setScope: function(scope) {
1243 			// get the scope object
1244 			var scopeObject = this.scopes[scope];
1245 
1246 			if (typeof scopeObject === 'undefined') {
1247 				// TODO log an error
1248 			} else if (this.currentScope != scope) {
1249 				this.currentScope = scope;
1250 
1251 				// first hide all buttons
1252 				jQuery.each(this.allButtons, function(index, buttonInfo) {
1253 					buttonInfo.scopeVisible = false;
1254 				});
1255 
1256 				// now set the buttons in the given scope to be visible
1257 				this.setButtonScopeVisibility(scopeObject);
1258 
1259 				// finally refresh the layout
1260 				this.doLayout();
1261 			}
1262 		},
1263 
1264 		/**
1265 		 * Set the scope visibility of the buttons for the given scope. This method will call itself for the motherscopes of the given scope.
1266 		 * @param scopeObject scope object
1267 		 * @hide
1268 		 */
1269 		setButtonScopeVisibility: function(scopeObject) {
1270 			var that = this;
1271 
1272 			// set all buttons in the given scope to be visible
1273 			jQuery.each(scopeObject.buttons, function(index, buttonInfo) {
1274 				buttonInfo.scopeVisible = true;
1275 			});
1276 
1277 			// now do the recursion for the motherscopes
1278 			jQuery.each(scopeObject.extendedScopes, function(index, scopeName) {
1279 				var motherScopeObject = that.scopes[scopeName];
1280 				if (typeof motherScopeObject === 'object') {
1281 					that.setButtonScopeVisibility(motherScopeObject);
1282 				}
1283 			});
1284 		},
1285 
1286 		/**
1287 		 * returns the next possible float target dom obj
1288 		 * the floating menu should only float to h1-h6, p, div, td and pre elements
1289 		 * if the current object is not valid, it's parentNode will be considered, until
1290 		 * the limit object is hit
1291 		 * @param obj the dom object to start from (commonly this would be the commonAncestorContainer)
1292 		 * @param limitObj the object that limits the range (this would be the editable)
1293 		 * @return dom object which qualifies as a float target
1294 		 * @hide
1295 		 */
1296 		nextFloatTargetObj: function (obj, limitObj) {
1297 			// if we've hit the limit object we don't care for it's type
1298 			if (!obj || obj == limitObj) {
1299 				return obj;
1300 			}
1301 
1302 			// fm will only float to h1-h6, p, div, td
1303 			switch (obj.nodeName.toLowerCase()) {
1304 				case 'h1':
1305 				case 'h2':
1306 				case 'h3':
1307 				case 'h4':
1308 				case 'h5':
1309 				case 'h6':
1310 				case 'p':
1311 				case 'div':
1312 				case 'td':
1313 				case 'pre':
1314 				case 'ul':
1315 				case 'ol':
1316 					return obj;
1317 				default:
1318 					return this.nextFloatTargetObj(obj.parentNode, limitObj);
1319 			}
1320 		},
1321 
1322 		/**
1323 		 * calculates the float target coordinates for a range
1324 		 * @param range the fm should float to
1325 		 * @return object containing left and top coordinates, like { left : 20, top : 43 }
1326 		 * @hide
1327 		 */
1328 		calcFloatTarget: function(range) {
1329 			var
1330 				i, documentWidth, editableLength, left, target,
1331 				targetObj, scrollTop, top;
1332 
1333 			// TODO in IE8 somteimes a broken range is handed to this function - investigate this
1334 			if (!Aloha.activeEditable || typeof range.getCommonAncestorContainer === 'undefined') {
1335 				return false;
1336 			}
1337 
1338 			// check if the designated editable is disabled
1339 			for ( i = 0, editableLength = Aloha.editables.length; i < editableLength; i++) {
1340 				if (Aloha.editables[i].obj.get(0) == range.limitObject &&
1341 						Aloha.editables[i].isDisabled()) {
1342 					return false;
1343 				}
1344 			}
1345 
1346 			target = this.nextFloatTargetObj(range.getCommonAncestorContainer(), range.limitObject);
1347 			if ( ! target ) {
1348 				return false;
1349 			}
1350 
1351 			targetObj = jQuery(target);
1352 			scrollTop = GENTICS.Utils.Position.Scroll.top;
1353 			if (!targetObj || !targetObj.offset()) {
1354 				return false;
1355 			}
1356 			top = targetObj.offset().top - this.obj.height() - 50; // 50px offset above the current obj to have some space above
1357 
1358 			// if the floating menu would be placed higher than the top of the screen...
1359 			if ( top < scrollTop) {
1360 				top += 80 + GENTICS.Utils.Position.ScrollCorrection.top;
1361 				// 80px if editable element is eg h1; 50px was fine for p;
1362 				// todo: maybe just use GENTICS.Utils.Position.ScrollCorrection.top with a better value?
1363 				// check where this is also used ...
1364 			}
1365 
1366 			// if the floating menu would float off the bottom of the screen
1367 			// we don't want it to move, so we'll return false
1368 			if (top > this.window.height() + this.window.scrollTop()) {
1369 				return false;
1370 			}
1371 
1372 			// check if the floating menu does not float off the right side
1373 			left = Aloha.activeEditable.obj.offset().left;
1374 			documentWidth = jQuery(document).width();
1375 			if ( documentWidth - this.width < left ) {
1376 					left = documentWidth - this.width - GENTICS.Utils.Position.ScrollCorrection.left;
1377 			}
1378 			
1379 			return {
1380 				left : left,
1381 				top : top
1382 			};
1383 		},
1384 
1385 		/**
1386 		 * float the fm to the desired position
1387 		 * the floating menu won't float if it is pinned
1388 		 * @method
1389 		 * @param {Object} coordinate object which has a left and top property
1390 		 */
1391 		floatTo: function(position) {
1392 			// no floating if the panel is pinned
1393 			if (this.pinned) {
1394 				return;
1395 			}
1396 
1397 			var floatingmenu = this,
1398 			    fmpos = this.obj.offset(),
1399 				lastLeft,
1400 				lastTop;
1401 			
1402 			if ( lastFloatingMenuPos.left === null ) {
1403 				lastLeft = fmpos.left;
1404 				lastTop = fmpos.top;
1405 			} else {
1406 				lastLeft = lastFloatingMenuPos.left;
1407 1408 				lastTop = lastFloatingMenuPos.top;
1409 			}
1410 
1411 			// Place the floatingmenu to the last place the user had seen it,
1412 			// then animate it into its new position.
1413 			if ( lastLeft != position.left || lastTop != position.top ) {
1414 				this.obj.offset({
1415 					left: lastLeft,
1416 					top: lastTop
1417 				});
1418 
1419 				this.obj.animate( {
1420 					top: position.top,
1421 					left: position.left
1422 				}, {
1423 					queue : false,
1424 					step : function( step, props ) {
1425 						// update position reference
1426 						if ( props.prop === 'top' ) {
1427 							floatingmenu.top = props.now;
1428 						} else if ( props.prop === 'left' ) {
1429 							floatingmenu.left = props.now;
1430 						}
1431 
1432 						floatingmenu.refreshShadow( false );
1433 					},
1434 					complete: function() {
1435 						// When the animation is over, remember the floatingmenu's
1436 						// final resting position.
1437 						lastFloatingMenuPos.left = floatingmenu.left;
1438 						lastFloatingMenuPos.top = floatingmenu.top;
1439 					}
1440 				});
1441 			}
1442 		},
1443 
1444 		/**
1445 		 * Hide the floatingmenu
1446 		 */
1447 		hide: function() {
1448 			if (this.obj) {
1449 				this.obj.hide();
1450 			}
1451 			if (this.shadow) {
1452 				this.shadow.hide();
1453 			}
1454 		},
1455 
1456 		/**
1457 		 * Activate the tab containing the button with given name.
1458 		 * If the button with given name is not found, nothing changes
1459 		 * @param name name of the button
1460 		 */
1461 		activateTabOfButton: function(name) {
1462 			var tabOfButton = null;
1463 
1464 			// find the tab containing the button
1465 			for (var t = 0; t < this.tabs.length && !tabOfButton; t++) {
1466 				var tab = this.tabs[t];
1467 				for (var g = 0; g < tab.groups.length && !tabOfButton; g++) {
1468 					var group = tab.groups[g];
1469 					for (var b = 0; b < group.buttons.length && !tabOfButton; b++) {
1470 						var button = group.buttons[b];
1471 						if (button.button.name == name) {
1472 							tabOfButton = tab;
1473 							break;
1474 						}
1475 					}
1476 				}
1477 			}
1478 
1479 			if (tabOfButton) {
1480 				this.userActivatedTab = tabOfButton.label;
1481 				this.doLayout();
1482 			}
1483 		}
1484 	});
1485 	
1486 	var menu =  new FloatingMenu();
1487 	menu.init();
1488 	
1489 	// set scope to empty if deactivated
1490 	Aloha.bind('aloha-editable-deactivated', function() {
1491 		menu.setScope('Aloha.empty');
1492 	});
1493 	
1494 	// set scope to empty if the user selectes a non contenteditable area
1495 	Aloha.bind('aloha-selection-changed', function() {
1496 		if ( !Aloha.Selection.isSelectionEditable() && !Aloha.Selection.isFloatingMenuVisible() ) {
1497 			menu.setScope('Aloha.empty');
1498 		}
1499 	});
1500 	
1501 	return menu;
1502 });
1503 
1504