/*! * This file is part of Aloha Editor Project http://aloha-editor.org * Copyright (c) 2010-2011 Gentics Software GmbH, aloha@gentics.com * Contributors http://aloha-editor.org/contribution.php * Licensed unter the terms of http://www.aloha-editor.org/license.html *//* * Aloha Editor is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version.* * * Aloha Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ define( ['aloha/core', 'aloha/jquery', 'aloha/ext', 'util/class', 'aloha/console', 'vendor/jquery.store'], function(Aloha, jQuery, Ext, Class, console) { var GENTICS = window.GENTICS; /** * Constructor for a floatingmenu tab * @namespace Aloha.FloatingMenu * @class Tab * @constructor * @param {String} label label of the tab */ var Tab = Class.extend({ _constructor: function(label) { this.label = label; this.groups = []; this.groupMap = {}; this.visible = true; }, /** * Get the group with given index. If it does not yet exist, create a new one * @method * @param {int} group group index of the group to get * @return group object */ getGroup: function(group) { var groupObject = this.groupMap[group]; if (typeof groupObject === 'undefined') { groupObject = new Group(); this.groupMap[group] = groupObject; this.groups.push(groupObject); // TODO resort the groups } return groupObject; }, /** * Get the EXT component representing the tab * @return EXT component (EXT.Panel) * @hide */ getExtComponent: function () { var that = this; if (typeof this.extPanel === 'undefined') { // generate the panel here this.extPanel = new Ext.Panel({ 'tbar' : [], 'title' : this.label, 'style': 'margin-top:0px', 'bodyStyle': 'display:none', 'autoScroll': true }); // add the groups jQuery.each(this.groups, function(index, group) { // let each group generate its ext component and add them to the panel that.extPanel.getTopToolbar().add(group.getExtComponent()); }); } return this.extPanel; }, /** * Recalculate the visibility of all groups within the tab * @hide */ doLayout: function() { var that = this; if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(this, 'doLayout called for tab ' + this.label); } this.visible = false; // check all groups in this tab jQuery.each(this.groups, function(index, group) { that.visible |= group.doLayout(); }); if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(this, 'tab ' + this.label + (this.visible ? ' is ' : ' is not ') + 'visible now'); } return this.visible; } }); /** * Constructor for a floatingmenu group * @namespace Aloha.FloatingMenu * @class Group * @constructor */ var Group = Class.extend({ _constructor: function() { this.buttons = []; this.fields = []; }, /** * Add a button to this group * @param {Button} buttonInfo to add to the group */ addButton: function(buttonInfo) { if (buttonInfo.button instanceof Aloha.ui.AttributeField) { if (this.fields.length < 2) { this.fields.push(buttonInfo); } else { throw new Error("Too much fields in this group"); } } else { // Every plugin API entryPoint (method) should be securised enough // to avoid Aloha to block at startup even // if a plugin is badly designed if (typeof buttonInfo.button !== "undefined"){ this.buttons.push(buttonInfo); } } }, /** * Get the EXT component representing the group (Ext.ButtonGroup) * @return the Ext.ButtonGroup * @hide */ getExtComponent: function () { var that = this, l, items = [], buttonCount = 0, columnCount = 0, len, idx, half; if (typeof this.extButtonGroup === 'undefined') { if (this.fields.length > 1) { columnCount = 1; } jQuery.each(this.buttons, function(index, button) { // count the number of buttons (large buttons count as 2) buttonCount += button.button.size == 'small' ? 1 : 2; }); columnCount = columnCount + Math.ceil(buttonCount / 2); len = this.buttons.length; idx = 0; half = Math.ceil(this.buttons.length / 2) - this.buttons.length % 2 ; if (this.fields.length > 0) { that.buttons.push(this.fields[0]); items.push(this.fields[0].button.getExtConfigProperties()); } while (--len >= half) { items.push(this.buttons[idx++].button.getExtConfigProperties()); } ++len; if (this.fields.length > 1) { that.buttons.push(this.fields[1]); items.push(this.fields[1].button.getExtConfigProperties()); } while (--len >=0) { items.push(this.buttons[idx++].button.getExtConfigProperties()); } this.extButtonGroup = new Ext.ButtonGroup({ 'columns' : columnCount, 'items': items }); // jQuery.each(this.fields, function(id, field){ // that.buttons.push(field); // }); // now find the Ext.Buttons and set to the GENTICS buttons jQuery.each(this.buttons, function(index, buttonInfo) { buttonInfo.button.extButton = that.extButtonGroup.findById(buttonInfo.button.id); // the following code is a work arround because ExtJS initializes later. // The ui wrapper store the information and here we use it... ugly. // if there are any listeners added before initializing the extButtons if ( buttonInfo.button.listenerQueue && buttonInfo.button.listenerQueue.length > 0 ) { while ( true ) { l = buttonInfo.button.listenerQueue.shift(); if ( !l ) {break;} buttonInfo.button.extButton.addListener(l.eventName, l.handler, l.scope, l.options); } } if (buttonInfo.button.extButton.setObjectTypeFilter) { if (buttonInfo.button.objectTypeFilter) { buttonInfo.button.extButton.noQuery = false; } if ( buttonInfo.button.objectTypeFilter == 'all' ) { buttonInfo.button.objectTypeFilter = null; } buttonInfo.button.extButton.setObjectTypeFilter(buttonInfo.button.objectTypeFilter); if ( buttonInfo.button.displayField) { buttonInfo.button.extButton.displayField = buttonInfo.button.displayField; } if ( buttonInfo.button.tpl ) { buttonInfo.button.extButton.tpl = buttonInfo.button.tpl; } } }); } return this.extButtonGroup; }, /** * Recalculate the visibility of the buttons and the group * @hide */ doLayout: function () { var groupVisible = false, that = this; jQuery.each(this.buttons, function(index, button) { if (typeof button.button !== "undefined") { var extButton = that.extButtonGroup.findById(button.button.id), buttonVisible = button.button.isVisible() && button.scopeVisible; if (!extButton) { return; } if (buttonVisible && extButton.hidden) { extButton.show(); } else if (!buttonVisible && extButton && !extButton.hidden) { extButton.hide(); } groupVisible |= buttonVisible; } }); if (groupVisible && this.extButtonGroup.hidden) { this.extButtonGroup.show(); } else if (!groupVisible && !this.extButtonGroup.hidden) { this.extButtonGroup.hide(); } return groupVisible; } }); /** * Aloha's Floating Menu * @namespace Aloha * @class FloatingMenu * @singleton */ var FloatingMenu = Class.extend({ /** * Define the default scopes * @property * @type Object */ scopes: { 'Aloha.empty' : { 'name' : 'Aloha.empty', 'extendedScopes' : [], 'buttons' : [] }, 'Aloha.global' : { 'name' : 'Aloha.global', 'extendedScopes' : ['Aloha.empty'], 'buttons' : [] }, 'Aloha.continuoustext' : { 'name' : 'Aloha.continuoustext', 'extendedScopes' : ['Aloha.global'], 'buttons' : [] } }, /** * Array of tabs within the floatingmenu * @hide */ tabs: [], /** * 'Map' of tabs (for easy access) * @hide */ tabMap: {}, /** * Flag to mark whether the floatingmenu is initialized * @hide */ initialized: false, /** * Array containing all buttons * @hide */ allButtons: [], /** * top part of the floatingmenu position * @hide */ top: 100, /** * left part of the floatingmenu position * @hide */ left: 100, /** * store pinned status - true, if the FloatingMenu is pinned * @property * @type boolean */ pinned: false, /** * just a reference to the jQuery(window) object, which is used quite often */ window: jQuery(window), /** * define floating menu float behaviour. meant to be adjusted via * GENTICS.Aloha.settings.floatingmenu.behaviour * set it to 'float' for standard behaviour, or 'topalign' for a fixed fm */ behaviour: 'float', /** * topalign offset to be used for topalign behavior */ topalignOffset: 90, /** * topalign offset to be used for topalign behavior */ horizontalOffset: 0, /** * will only be hounoured when behaviour is set to 'topalign'. Adds a margin, * so the floating menu is not directly attached to the top of the page */ marginTop: 0, /** * Define whether the floating menu shall be draggable or not via Aloha.settings.floatingmanu.draggable * Default is: true */ draggable: true, /** * A list of all buttons that have been added to the floatingmenu * This needs to be tracked, as adding buttons twice will break the fm */ buttonsAdded: [], /** * Will be initialized by checking Aloha.settings.toolbar, which will contain the config for * the floating menu. If there is no config, tabs and groups will be generated programmatically */ fromConfig: false, /** * Initialize the floatingmenu * @hide */ init: function() { // check for behaviour setting of the floating menu if (Aloha.settings.floatingmenu) { if (typeof Aloha.settings.floatingmenu.draggable === 'boolean') { this.draggable = Aloha.settings.floatingmenu.draggable; } if (typeof Aloha.settings.floatingmenu.behaviour === 'string') { this.behaviour = Aloha.settings.floatingmenu.behaviour; } if (typeof Aloha.settings.floatingmenu.topalignOffset !== 'undefined') { this.topalignOffset = Aloha.settings.floatingmenu.topalignOffset; } if (typeof Aloha.settings.floatingmenu.horizontalOffset !== 'undefined') { this.horizontalOffset = Aloha.settings.floatingmenu.horizontalOffset; } if (typeof Aloha.settings.floatingmenu.marginTop === 'number') { this.marginTop = Aloha.settings.floatingmenu.marginTop; } //We just check for undefined if (typeof Aloha.settings.floatingmenu.width !== 'undefined') { //Try to pars it try { var parsed = parseInt(Aloha.settings.floatingmenu.width); this.width = Aloha.settings.floatingmenu.width; } catch(e) { //do nothing. } } } jQuery.storage = new jQuery.store(); this.currentScope = 'Aloha.global'; var that = this; this.window.unload(function () { // store fm position if the panel is pinned to be able to restore it next time if (that.pinned) { jQuery.storage.set('Aloha.FloatingMenu.pinned', 'true'); jQuery.storage.set('Aloha.FloatingMenu.top', that.top); jQuery.storage.set('Aloha.FloatingMenu.left', that.left); if (Aloha.Log.isInfoEnabled()) { Aloha.Log.info(this, 'stored FloatingMenu pinned position {' + that.left + ', ' + that.top + '}'); } } else { // delete old localStorages jQuery.storage.del('Aloha.FloatingMenu.pinned'); jQuery.storage.del('Aloha.FloatingMenu.top'); jQuery.storage.del('Aloha.FloatingMenu.left'); } if (that.userActivatedTab) { jQuery.storage.set('Aloha.FloatingMenu.activeTab', that.userActivatedTab); } }).resize(function () { if (that.behaviour === 'float') { if (that.pinned) { that.fixPinnedPosition(); that.refreshShadow(); that.extTabPanel.setPosition(that.left, that.top); } else { var target = that.calcFloatTarget(Aloha.Selection.getRangeObject()); if (target) { that.floatTo(target); } } } }); Aloha.bind('aloha-ready', function() { that.generateComponent(); that.initialized = true; }); if (typeof Aloha.settings.toolbar === 'object') { this.fromConfig = true; } }, /** * jQuery reference to the extjs tabpanel * @hide */ obj: null, /** * jQuery reference to the shadow obj * @hide */ shadow: null, /** * jQuery reference to the panels body wrap div * @hide */ panelBody: null, /** * The panels width * @hide */ width: 400, /** * initialize tabs and groups according to the current configuration */ initTabsAndGroups: function () { var that = this; // if there is no toolbar config tabs and groups have been initialized before if (!this.fromConfig) { return; } jQuery.each(Aloha.settings.toolbar.tabs, function (tab, groups) { // generate or retrieve tab var tabObject = that.tabMap[tab]; if (typeof tabObject === 'undefined') { // the tab object does not yet exist, so create a new tab and add it to the list tabObject = new Tab(tab); that.tabs.push(tabObject); that.tabMap[tab] = tabObject; } // generate groups for current tab jQuery.each(groups, function (group, buttons) { var groupObject = tabObject.getGroup(group), i; // now get all the buttons for that group jQuery.each(buttons, function (j, button) { if (jQuery.inArray(button, that.buttonsAdded) !== -1) { // buttons must not be added twice console.warn('Skipping button {' + button + '}. A button can\'t be added ' + 'to the floating menu twice. Config key: {Aloha.settings.toolbar.' + tab + '.' + group + '}'); return; } // now add the button to the group for (i = 0; i < that.allButtons.length; i++) { if (button === that.allButtons[i].button.name) { groupObject.addButton(that.allButtons[i]); // remember that we've added the button that.buttonsAdded.push(that.allButtons[i].button.name); break; } } }); }); }); }, /** * Generate the rendered component for the floatingmenu * @hide */ generateComponent: function () { var that = this, pinTab; // initialize tabs and groups first this.initTabsAndGroups(); // Initialize and configure the tooltips Ext.QuickTips.init(); Ext.apply(Ext.QuickTips.getQuickTip(), { minWidth : 10 }); if (this.extTabPanel) { // TODO dispose of the ext component } else { // Enable or disable the drag functionality var dragConfiguration = false; if ( that.draggable ) { dragConfiguration = { insertProxy: false, onDrag : function(e) { var pel = this.proxy.getEl(); this.x = pel.getLeft(true); this.y = pel.getTop(true); this.panel.shadow.hide(); }, endDrag : function(e) { var top = (that.pinned) ? this.y - jQuery(document).scrollTop() : this.y; that.left = this.x; that.top = top; this.panel.setPosition(this.x, top); that.refreshShadow(); this.panel.shadow.show(); } }; } // generate the tabpanel object this.extTabPanel = new Ext.TabPanel({ activeTab: 0, width: that.width, // 336px this fits the multisplit button and 6 small buttons placed in 3 cols plain: false, draggable: dragConfiguration, floating: {shadow: false}, defaults: { autoScroll: true }, layoutOnTabChange : true, shadow: false, cls: 'aloha-floatingmenu ext-root', listeners : { 'tabchange' : { 'fn' : function(tabPanel, tab) { if (tab.title != that.autoActivatedTab) { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(that, 'User selected tab ' + tab.title); } // remember the last user-selected tab that.userActivatedTab = tab.title; } else { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(that, 'Tab ' + tab.title + ' was activated automatically'); } } that.autoActivatedTab = undefined; // ok, this is kind of a hack: when the tab changes, we check all buttons for multisplitbuttons (which have the method setActiveDOMElement). // if a DOM Element is queued to be set active, we try to do this now. // the reason for this is that the active DOM element can only be set when the multisplit button is currently visible. jQuery.each(that.allButtons, function(index, buttonInfo) { if (typeof buttonInfo.button !== 'undefined' && typeof buttonInfo.button.extButton !== 'undefined' && buttonInfo.button.extButton !== null && typeof buttonInfo.button.extButton.setActiveDOMElement === 'function') { if (typeof buttonInfo.button.extButton.activeDOMElement !== 'undefined') { buttonInfo.button.extButton.setActiveDOMElement(buttonInfo.button.extButton.activeDOMElement); } } }); // adapt the shadow that.extTabPanel.shadow.show(); that.refreshShadow(); } } }, enableTabScroll : true }); } // add the tabs jQuery.each(this.tabs, function(index, tab) { // let each tab generate its ext component and add them to the panel try { that.extTabPanel.add(tab.getExtComponent()); } catch(e) { Aloha.Log.error(that,"Error while inserting tab: " + e); } }); // add the dropshadow this.extTabPanel.shadow = jQuery('
 
'); jQuery('body').append(this.extTabPanel.shadow); // add an empty pin tab item, store reference pinTab = this.extTabPanel.add({ title : ' ' }); // finally render the panel to the body this.extTabPanel.render(document.body); // finish the pin element after the FM has rendered (before there are noe html contents to be manipulated jQuery(pinTab.tabEl) .addClass('aloha-floatingmenu-pin') .html(' ') .mousedown(function (e) { that.togglePin(); // Note: this event is deliberately stopped here, although normally, // we would set the flag GENTICS.Aloha.eventHandled instead. // But when the event bubbles up, no tab would be selected and // the floatingmenu would be rather thin. e.stopPropagation(); }); // a reference to the panels body needed for shadow size & position this.panelBody = jQuery('div.aloha-floatingmenu div.x-tab-panel-bwrap'); // do the visibility this.doLayout(); // bind jQuery reference to extjs obj // this has to be done AFTER the tab panel has been rendered this.obj = jQuery(this.extTabPanel.getEl().dom); if (jQuery.storage.get('Aloha.FloatingMenu.pinned') == 'true') { this.togglePin(); this.top = parseInt(jQuery.storage.get('Aloha.FloatingMenu.top'),10); this.left = parseInt(jQuery.storage.get('Aloha.FloatingMenu.left'),10); // do some positioning fixes this.fixPinnedPosition(); if (Aloha.Log.isInfoEnabled()) { Aloha.Log.info(this, 'restored FloatingMenu pinned position {' + this.left + ', ' + this.top + '}'); } this.refreshShadow(); } // set the user activated tab stored in a localStorage if (jQuery.storage.get('Aloha.FloatingMenu.activeTab')) { this.userActivatedTab = jQuery.storage.get('Aloha.FloatingMenu.activeTab'); } // for now, position the panel somewhere this.extTabPanel.setPosition(this.left, this.top); // mark the event being handled by aloha, because we don't want to recognize // a click into the floatingmenu to be a click into nowhere (which would // deactivate the editables) this.obj.mousedown(function (e) { e.originalEvent.stopSelectionUpdate = true; Aloha.eventHandled = true; // e.stopSelectionUpdate = true; }); this.obj.mouseup(function (e) { e.originalEvent.stopSelectionUpdate = true; Aloha.eventHandled = false; }); // adjust float behaviour if (this.behaviour === 'float') { // listen to selectionChanged event Aloha.bind('aloha-selection-changed',function(event, rangeObject) { if (!that.pinned) { var pos = that.calcFloatTarget(rangeObject); if (pos) { that.floatTo(pos); } } }); } else if (this.behaviour === 'topalign') { // topalign will retain the user's pinned status // TODO maybe the pin should be hidden in that case? this.togglePin(false); // float the fm to each editable that is activated Aloha.bind('aloha-editable-activated', function(event, data) { var p = data.editable.obj.offset(); p.top -= that.topalignOffset; p.left += that.horizontalOffset; if (p.top < jQuery(document).scrollTop()) { // scrollpos is below top of editable that.obj.css('top', jQuery(document).scrollTop() + that.marginTop); that.obj.css('left', p.left); that.togglePin(true); } else { // scroll pos is above top of editable that.floatTo(p); } }); // fm scroll behaviour jQuery(window).scroll(function () { if (!Aloha.activeEditable) { return; } var pos = Aloha.activeEditable.obj.offset(), fmHeight = that.obj.height(), scrollTop = jQuery(document).scrollTop(); if (scrollTop > (pos.top - fmHeight - 6 - that.marginTop)) { // scroll pos is lower than top of editable that.togglePin(true); that.obj.css('top', that.marginTop); } else if (scrollTop <= (pos.top - fmHeight - 6 - that.marginTop)) { // scroll pos is above top of editable if (that.behaviour === 'topalign') { pos.top = Aloha.activeEditable.obj.offset().top - that.topalignOffset; pos.left = Aloha.activeEditable.obj.offset().left + that.horizontalOffset; } else { pos.top -= fmHeight + 6; } that.togglePin(false); that.floatTo(pos); } else if (scrollTop > pos.top + Aloha.activeEditable.obj.height() - fmHeight) { // scroll pos is below editable that.togglePin(false); } }); } }, /** * Fix the position of the pinned floatingmenu to keep it visible */ fixPinnedPosition: function() { // if not pinned, do not fix the position if (!this.pinned) { return; } // fix the position of the floatingmenu, to keep it visible if (this.top < 30) { // from top position, we want to have 30px margin this.top = 30; } else if (this.top > this.window.height() - this.extTabPanel.getHeight()) { this.top = this.window.height() - this.extTabPanel.getHeight(); } if (this.left < 0) { this.left = 0; } else if (this.left > this.window.width() - this.extTabPanel.getWidth()) { this.left = this.window.width() - this.extTabPanel.getWidth(); } }, /** * reposition & resize the shadow * the shadow must not be repositioned outside this method! * position calculation is based on this.top and this.left coordinates * @method */ refreshShadow: function (resize) { if (this.panelBody) { var props = { 'top': this.top + 24, // 24px top offset to reflect tab bar height 'left': this.left }; if(typeof resize === 'undefined' || !resize) { props.width = this.panelBody.width() + 'px'; props.height = this.panelBody.height() + 'px'; } this.extTabPanel.shadow.css(props); } }, /** * toggles the pinned status of the floating menu * @method * @param {boolean} pinned set to true to activate pin, or set to false to deactivate pin. * leave undefined to toggle pin status automatically */ togglePin: function(pinned) { var el = jQuery('.aloha-floatingmenu-pin'); if (typeof pinned === 'boolean') { this.pinned = !pinned; } if (this.pinned) { el.removeClass('aloha-floatingmenu-pinned'); this.top = this.obj.offset().top; this.obj.removeClass('fixed').css({ 'top': this.top }); this.extTabPanel.shadow.removeClass('fixed'); this.refreshShadow(); this.pinned = false; } else { el.addClass('aloha-floatingmenu-pinned'); this.top = this.obj.offset().top - this.window.scrollTop(); this.obj.addClass('fixed').css({ 'top': this.top // update position for fixed position }); // do the same for the shadow this.extTabPanel.shadow.addClass('fixed');//props.start this.refreshShadow(); this.pinned = true; } }, /** * Create a new scopes * @method * @param {String} scope name of the new scope (should be namespaced for uniqueness) * @param {String} extendedScopes Array of scopes this scope extends. Can also be a single String if * only one scope is extended, or omitted if the scope should extend * the empty scope */ createScope: function(scope, extendedScopes) { if (typeof extendedScopes === 'undefined') { extendedScopes = ['Aloha.empty']; } else if (typeof extendedScopes === 'string') { extendedScopes = [extendedScopes]; } // TODO check whether the extended scopes already exist if (this.scopes[scope]) { // TODO what if the scope already exists? } else { // generate the new scope this.scopes[scope] = {'name' : scope, 'extendedScopes' : extendedScopes, 'buttons' : []}; } }, /** * Adds a button to the floatingmenu * @method * @param {String} scope the scope for the button, should be generated before (either by core or the plugin) * @param {Button} button instance of Aloha.ui.button to add at the floatingmenu * @param {String} tab label of the tab to which the button is added * @param {int} group index of the button group in the tab, lowest index is left */ addButton: function(scope, button, tab, group) { // check whether the scope exists var scopeObject = this.scopes[scope], buttonInfo, tabObject, groupObject; if (!button.name) { console.warn('Added button with iconClass {' + button.iconClass + '} which has no property "name"'); } if (typeof scopeObject === 'undefined') { Aloha.Log.error("Can't add button to given scope since the scope has not yet been initialized.", scope); return false; } // generate a buttonInfo object buttonInfo = { 'button' : button, 'scopeVisible' : false }; // add the button to the list of all buttons this.allButtons.push(buttonInfo); // add the button to the scope scopeObject.buttons.push(buttonInfo); // if there is no toolbar config tabs and groups will be generated right away if (!this.fromConfig) { // get the tab object tabObject = this.tabMap[tab]; if (typeof tabObject === 'undefined') { // the tab object does not yet exist, so create a new tab and add it to the list tabObject = new Tab(tab); this.tabs.push(tabObject); this.tabMap[tab] = tabObject; } // get the group groupObject = tabObject.getGroup(group); // now add the button to the group groupObject.addButton(buttonInfo); } // finally, when the floatingmenu is already initialized, we need to create the ext component now if (this.initialized) { this.generateComponent(); } }, /** * Recalculate the visibility of tabs, groups and buttons (depending on scope and button hiding) * @hide */ doLayout: function () { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(this, 'doLayout called for FloatingMenu, scope is ' + this.currentScope); } var that = this, firstVisibleTab = false, activeExtTab = this.extTabPanel.getActiveTab(), activeTab = false, floatingMenuVisible = false, showUserActivatedTab = false, pos; // let the tabs layout themselves jQuery.each(this.tabs, function(index, tab) { // remember the active tab if (tab.extPanel == activeExtTab) { activeTab = tab; } // remember whether the tab is currently visible var tabVisible = tab.visible; // let each tab generate its ext component and add them to the panel if (tab.doLayout()) { // found a visible tab, so the floatingmenu needs to be visible as well floatingMenuVisible = true; // make sure the tabstrip is visible if (!tabVisible) { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(that, 'showing tab strip for tab ' + tab.label); } that.extTabPanel.unhideTabStripItem(tab.extPanel); } // remember the first visible tab if (!firstVisibleTab) { // this is the first visible tab (in case we need to switch to it) firstVisibleTab = tab; } // check whether this visible tab is the last user activated tab and currently not active if (that.userActivatedTab == tab.extPanel.title && tab.extPanel != activeExtTab) { showUserActivatedTab = tab; } } else { // make sure the tabstrip is hidden if (tabVisible) { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(that, 'hiding tab strip for tab ' + tab.label); } that.extTabPanel.hideTabStripItem(tab.extPanel); } } }); // check whether the last tab which was selected by the user is visible and not the active tab if (showUserActivatedTab) { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(this, 'Setting active tab to ' + showUserActivatedTab.label); } this.extTabPanel.setActiveTab(showUserActivatedTab.extPanel); } else if (typeof activeTab === 'object' && typeof firstVisibleTab === 'object') { // now check the currently visible tab, whether it is visible and enabled if (!activeTab.visible) { if (Aloha.Log.isDebugEnabled()) { Aloha.Log.debug(this, 'Setting active tab to ' + firstVisibleTab.label); } this.autoActivatedTab = firstVisibleTab.extPanel.title; this.extTabPanel.setActiveTab(firstVisibleTab.extPanel); } } // set visibility of floatingmenu if (floatingMenuVisible && this.extTabPanel.hidden) { // set the remembered position this.extTabPanel.show(); this.refreshShadow(); this.extTabPanel.shadow.show(); this.extTabPanel.setPosition(this.left, this.top); } else if (!floatingMenuVisible && !this.extTabPanel.hidden) { // remember the current position pos = this.extTabPanel.getPosition(true); // restore previous position if the fm was pinned this.left = pos[0] < 0 ? 100 : pos[0]; this.top = pos[1] < 0 ? 100 : pos[1]; this.extTabPanel.hide(); this.extTabPanel.shadow.hide(); } /*else { var target = that.calcFloatTarget(Aloha.Selection.getRangeObject()); if (target) { this.left = target.left; this.top = target.top; this.extTabPanel.show(); this.refreshShadow(); this.extTabPanel.shadow.show(); this.extTabPanel.setPosition(this.left, this.top); that.floatTo(target); } }*/ // let the Ext object render itself again this.extTabPanel.doLayout(); }, /** * Set the current scope * @method * @param {String} scope name of the new current scope */ setScope: function(scope) { // get the scope object var scopeObject = this.scopes[scope]; if (typeof scopeObject === 'undefined') { // TODO log an error } else if (this.currentScope != scope) { this.currentScope = scope; // first hide all buttons jQuery.each(this.allButtons, function(index, buttonInfo) { buttonInfo.scopeVisible = false; }); // now set the buttons in the given scope to be visible this.setButtonScopeVisibility(scopeObject); // finally refresh the layout this.doLayout(); } }, /** * Set the scope visibility of the buttons for the given scope. This method will call itself for the motherscopes of the given scope. * @param scopeObject scope object * @hide */ setButtonScopeVisibility: function(scopeObject) { var that = this; // set all buttons in the given scope to be visible jQuery.each(scopeObject.buttons, function(index, buttonInfo) { buttonInfo.scopeVisible = true; }); // now do the recursion for the motherscopes jQuery.each(scopeObject.extendedScopes, function(index, scopeName) { var motherScopeObject = that.scopes[scopeName]; if (typeof motherScopeObject === 'object') { that.setButtonScopeVisibility(motherScopeObject); } }); }, /** * returns the next possible float target dom obj * the floating menu should only float to h1-h6, p, div, td and pre elements * if the current object is not valid, it's parentNode will be considered, until * the limit object is hit * @param obj the dom object to start from (commonly this would be the commonAncestorContainer) * @param limitObj the object that limits the range (this would be the editable) * @return dom object which qualifies as a float target * @hide */ nextFloatTargetObj: function (obj, limitObj) { // if we've hit the limit object we don't care for it's type if (!obj || obj == limitObj) { return obj; } // fm will only float to h1-h6, p, div, td switch (obj.nodeName.toLowerCase()) { case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': case 'p': case 'div': case 'td': case 'pre': case 'ul': case 'ol': return obj; default: return this.nextFloatTargetObj(obj.parentNode, limitObj); } }, /** * calculates the float target coordinates for a range * @param range the fm should float to * @return object containing left and top coordinates, like { left : 20, top : 43 } * @hide */ calcFloatTarget: function(range) { var i, documentWidth, editableLength, left, target, targetObj, scrollTop, top; // TODO in IE8 somteimes a broken range is handed to this function - investigate this if (!Aloha.activeEditable || typeof range.getCommonAncestorContainer === 'undefined') { return false; } // check if the designated editable is disabled for ( i = 0, editableLength = Aloha.editables.length; i < editableLength; i++) { if (Aloha.editables[i].obj.get(0) == range.limitObject && Aloha.editables[i].isDisabled()) { return false; } } target = this.nextFloatTargetObj(range.getCommonAncestorContainer(), range.limitObject); if ( ! target ) { return false; } targetObj = jQuery(target); scrollTop = GENTICS.Utils.Position.Scroll.top; if (!targetObj || !targetObj.offset()) { return false; } top = targetObj.offset().top - this.obj.height() - 50; // 50px offset above the current obj to have some space above // if the floating menu would be placed higher than the top of the screen... if ( top < scrollTop) { top += 50 + GENTICS.Utils.Position.ScrollCorrection.top; } // if the floating menu would float off the bottom of the screen // we don't want it to move, so we'll return false if (top > this.window.height() + this.window.scrollTop()) { return false; } // check if the floating menu does not float off the right side left = Aloha.activeEditable.obj.offset().left; documentWidth = jQuery(document).width(); if ( documentWidth - this.width < left ) { left = documentWidth - this.width - GENTICS.Utils.Position.ScrollCorrection.left; } return { left : left, top : top }; }, /** * float the fm to the desired position * the floating menu won't float if it is pinned * @method * @param {Object} coordinate object which has a left and top property */ floatTo: function(position) { // no floating if the panel is pinned if (this.pinned) { return; } var that = this, fmpos = this.obj.offset(); // move to the new position if (fmpos.left != position.left || fmpos.top != position.top) { this.obj.animate({ top: position.top, left: position.left }, { queue : false, step : function (step, props) { // update position reference if (props.prop == 'top') { that.top = props.now; } else if (props.prop == 'left') { that.left = props.now; } that.refreshShadow(false); } }); } }, /** * Hide the floatingmenu */ hide: function() { if (this.obj) { this.obj.hide(); } if (this.shadow) { this.shadow.hide(); } }, /** * Activate the tab containing the button with given name. * If the button with given name is not found, nothing changes * @param name name of the button */ activateTabOfButton: function(name) { var tabOfButton = null; // find the tab containing the button for (var t = 0; t < this.tabs.length && !tabOfButton; t++) { var tab = this.tabs[t]; for (var g = 0; g < tab.groups.length && !tabOfButton; g++) { var group = tab.groups[g]; for (var b = 0; b < group.buttons.length && !tabOfButton; b++) { var button = group.buttons[b]; if (button.button.name == name) { tabOfButton = tab; break; } } } } if (tabOfButton) { this.userActivatedTab = tabOfButton.label; this.doLayout(); } } }); var menu = new FloatingMenu(); menu.init(); // set scope to empty if deactivated Aloha.bind('aloha-editable-deactivated', function() { menu.setScope('Aloha.empty'); }); // set scope to empty if the user selectes a non contenteditable area Aloha.bind('aloha-selection-changed', function() { if ( !Aloha.Selection.isSelectionEditable() && !Aloha.Selection.isFloatingMenuVisible() ) { menu.setScope('Aloha.empty'); } }); return menu; });