/*!
* 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;
});