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