egroupware/api/js/framework/fw_desktop.js

609 lines
18 KiB
JavaScript

/**
* EGroupware desktop framework
*
* @package framework
* @author Hadi Nategh <hn@stylite.de>
* @author Andreas Stoeckel <as@stylite.de>
* @copyright EGroupware GmbH 2014-2021
* @description Create desktop framework
*/
/*egw:uses
vendor.bower-asset.jquery.dist.jquery;
framework.fw_base;
framework.fw_browser;
framework.fw_ui;
framework.fw_classes;
egw_inheritance.js;
*/
import './fw_base.js';
import './fw_browser.js';
import './fw_ui.js';
import './fw_classes.js';
import '../jsapi/egw_inheritance.js';
/**
*
* @param {DOMWindow} window
*/
(function(window)
{
"use strict";
/**
*
* @type @exp;fw_ui_sidemenu_entry@call;extend
*/
window.desktop_ui_sidemenu_entry = fw_ui_sidemenu_entry.extend(
{
/**
* Override fw_ui_sidemenu_entry class constructor
*
* @returns {undefined}
*/
init: function()
{
this._super.apply(this,arguments);
this.setBottomLine(this.parent.entries);
//Make the base Div sortable. Set all elements with the style "egw_fw_ui_sidemenu_entry_header"
//as handle
if(jQuery(this.elemDiv).data('uiSortable'))
{
jQuery(this.elemDiv).sortable("destroy");
}
jQuery(this.elemDiv).sortable({
handle: ".egw_fw_ui_sidemenu_entry_header",
distance: 15,
start: function(event, ui)
{
var parent = ui.item.context._parent;
parent.isDraged = true;
parent.parent.startDrag.call(parent.parent);
},
stop: function(event, ui)
{
var parent = ui.item.context._parent;
parent.parent.stopDrag.call(parent.parent);
parent.parent.refreshSort.call(parent.parent);
},
opacity: 0.7,
axis: 'y'
});
},
/**
* setBottomLine marks this element as the bottom element in the application list.
* This adds the egw_fw_ui_sidemenu_entry_content_bottom/egw_fw_ui_sidemenu_entry_header_bottom CSS classes
* which should care about adding an closing bottom line to the sidemenu. These classes are removed from
* all other entries in the side menu.
* @param {type} _entryList is a reference to the list which contains the sidemenu_entry entries.
*/
setBottomLine: function(_entryList)
{
//If this is the last tab in the tab list, the bottom line must be closed
for (var i = 0; i < _entryList.length; i++)
{
jQuery(_entryList[i].contentDiv).removeClass("egw_fw_ui_sidemenu_entry_content_bottom");
jQuery(_entryList[i].headerDiv).removeClass("egw_fw_ui_sidemenu_entry_header_bottom");
}
jQuery(this.contentDiv).addClass("egw_fw_ui_sidemenu_entry_content_bottom");
jQuery(this.headerDiv).addClass("egw_fw_ui_sidemenu_entry_header_bottom");
}
});
/**
*
* @type @exp;fw_ui_sidemenu@call;extend
*/
window.desktop_ui_sidemenu = fw_ui_sidemenu.extend(
{
init: function(_baseDiv, _sortCallback)
{
this._super.apply(this,arguments);
this.sortCallback = _sortCallback;
},
/**
*
* @returns {undefined}
*/
startDrag: function()
{
if (this.activeEntry)
{
jQuery(this.activeEntry.marker).show();
jQuery(this.elemDiv).sortable("refresh");
}
},
/**
*
* @returns {undefined}
*/
stopDrag: function()
{
if (this.activeEntry)
{
jQuery(this.activeEntry.marker).hide();
jQuery(this.elemDiv).sortable("refresh");
}
},
/**
* Called by the sidemenu elements whenever they were sorted. An array containing
* the sidemenu_entries ui-objects is generated and passed to the sort callback
*/
refreshSort: function()
{
//Step through all children of elemDiv and add all markers to the result array
var resultArray = new Array();
this._searchMarkers(resultArray, this.elemDiv.childNodes);
//Call the sort callback with the array containing the sidemenu_entries
this.sortCallback(resultArray);
},
/**
* Adds an entry to the sidemenu.
* @param {type} _name specifies the title of the new sidemenu entry
* @param {type} _icon specifies the icon displayed aside the title
* @param {type} _callback specifies the function which should be called when a callback is clicked
* @param {type} _tag extra data
* @param {type} _app application name
*
* @returns {desktop_ui_sidemenu_entry}
*/
addEntry: function(_name, _icon, _callback, _tag, _app)
{
//Create a new sidemenu entry and add it to the list
var entry = new desktop_ui_sidemenu_entry(this, this.baseDiv, this.elemDiv, _name, _icon,
_callback, _tag, _app);
this.entries[this.entries.length] = entry;
return entry;
}
});
/**
* jdots framework object defenition
* here we can add framework methods and also override fw_base methods if it is neccessary
* @type @exp;fw_base@call;extend
*/
window.fw_desktop = fw_base.extend({
/**
* jdots framework constructor
*
* @param {string} _sidemenuId sidebar menu div id
* @param {string} _tabsId tab area div id
* @param {string} _splitterId splitter div id
* @param {string} _webserverUrl specifies the egroupware root url
* @param {function} _sideboxSizeCallback
* @param {int} _sideboxStartSize sidebox start size
* @param {int} _sideboxMinSize sidebox minimum size
*/
init:function (_sidemenuId, _tabsId, _webserverUrl, _sideboxSizeCallback, _splitterId, _sideboxStartSize, _sideboxMinSize)
{
// call fw_base constructor, in order to build basic DOM elements
this._super.apply(this,arguments);
this.splitterDiv = document.getElementById(_splitterId);
if (this.sidemenuDiv && this.tabsDiv && this.splitterDiv)
{
//Wrap a scroll area handler around the applications
this.scrollAreaUi = new egw_fw_ui_scrollarea(this.sidemenuDiv);
// Create toggleSidebar menu
this.toggleSidebarUi = new egw_fw_ui_toggleSidebar('#egw_fw_basecontainer', this._toggleSidebarCallback,this);
//Create the sidemenu, the tabs area and the splitter
this.sidemenuUi = new desktop_ui_sidemenu(this.scrollAreaUi.contentDiv,
this.sortCallback);
this.tabsUi = new egw_fw_ui_tabs(this.tabsDiv);
this.splitterUi = new egw_fw_ui_splitter(this.splitterDiv,
EGW_SPLITTER_VERTICAL, this.splitterResize,
[
{
"size": _sideboxStartSize,
"minsize": _sideboxMinSize,
"maxsize": screen.availWidth - 50
}
], this);
var egw_script = document.getElementById('egw_script_id');
var apps = egw_script ? egw_script.getAttribute('data-navbar-apps') : null;
this.loadApplications(JSON.parse(apps));
}
_sideboxSizeCallback(_sideboxStartSize);
// warn user about using IE not compatibilities
// we need to wait until common translations are loaded
egw.langRequireApp(window, 'common', function()
{
if (navigator && navigator.userAgent.match(/Trident|msie/ig))
{
egw.message(egw.lang('Browser %1 %2 is not recommended. You may experience issues and not working features. Please use the latest version of Chrome, Firefox or Edge. Thank You!', 'IE',''), 'info', 'browser:ie:warning');
}
});
// initiate darkmode
let darkmode = egw.preference('darkmode', 'common');
if (darkmode == '2')
{
let prefes_color_scheme = this._get_prefers_color_scheme();
if (prefes_color_scheme)
{
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', event => {
this.toggle_darkmode(document.getElementById('topmenu_info_darkmode'), event.matches?false:true);
});
this.toggle_darkmode(document.getElementById('topmenu_info_darkmode'), prefes_color_scheme == 'light');
}
}
else if (egw.getSessionItem('api', 'darkmode') == '1')
{
this.toggle_darkmode(document.getElementById('topmenu_info_darkmode'), false);
}
else if(egw.getSessionItem('api', 'darkmode') == '0')
{
this.toggle_darkmode(document.getElementById('topmenu_info_darkmode'), true);
}
},
/**
*
* @param {array} apps
*/
loadApplications: function (apps)
{
var restore = this._super.apply(this, arguments);
//Generate an array with all tabs which shall be restored sorted in by
//their active state
//Fill in the sorted_restore array...
var sorted_restore = [];
for (this.appName in restore)
sorted_restore[sorted_restore.length] = restore[this.appName];
//...and sort it
sorted_restore.sort(function (a, b) {
return ((a.active < b.active) ? 1 : ((a.active == b.active) ? 0 : -1));
});
//Now actually restore the tabs by passing the application, the url, whether
//this is an legacyApp (null triggers the application default), whether the
//application is hidden (only the active tab is shown) and its position
//in the tab list.
for (var i = 0; i < sorted_restore.length; i++)
this.applicationTabNavigate(
sorted_restore[i].app, sorted_restore[i].url, i != 0,
sorted_restore[i].position, sorted_restore[i]['status']);
//Set the current state of the tabs and activate TabChangeNotification.
this.serializedTabState = egw.jsonEncode(this.assembleTabList());
this.notifyTabChangeEnabled = true;
this.scrollAreaUi.update();
// Disable loader, if present
jQuery('#egw_fw_loading').hide();
},
/**
*
* @param {type} _app
* @returns {undefined}
*/
setActiveApp: function(_app)
{
var result = this._super.apply(this, arguments);
this.notifyAppTab(_app.appName , 0);
if (_app == _app.parentFw.activeApp)
{
//Set the sidebox width if a application specific sidebox width is set
// do not trigger resize if the sidebar is already in toggle on mode and
// the next set state is the same
if (_app.sideboxWidth !== false && egw.preference('toggleSidebar',_app.internalName) == 'off')
{
this.sideboxSizeCallback(_app.sideboxWidth);
this.splitterUi.constraints[0].size = _app.sideboxWidth;
}
_app.parentFw.scrollAreaUi.update();
_app.parentFw.scrollAreaUi.setScrollPos(0);
}
//Resize the scroll area...
this.scrollAreaUi.update();
//...and scroll to the top
this.scrollAreaUi.setScrollPos(0);
// Handles toggleSidebar initialization
if (typeof framework != 'undefined')
{
framework.getToggleSidebarState();
framework.activeApp.browser.callResizeHandler();
}
return result;
},
/**
* Function called whenever the sidemenu entries are sorted
* @param {type} _entriesArray
*/
sortCallback: function(_entriesArray)
{
//Create an array with the names of the applications in their sort order
var name_array = [];
for (var i = 0; i < _entriesArray.length; i++)
{
name_array.push(_entriesArray[i].tag.appName);
}
//Send the sort order to the server via ajax
var req = egw.jsonq('EGroupware\\Api\\Framework\\Ajax::ajax_appsort', [name_array]);
},
/**
* Splitter resize callback
* @param {type} _width
* @param {string} _toggleMode if mode is "toggle" then resize happens without changing splitter preference
* @returns {undefined}
*/
splitterResize: function(_width, _toggleMode)
{
if (this.tag.activeApp)
{
if (_toggleMode !== "toggle")
{
if (!framework.isAnInternalApp(this.tag.activeApp)) egw.set_preference(this.tag.activeApp.internalName, 'jdotssideboxwidth', _width);
//If there are no global application width values, set the sidebox width of
//the application every time the splitter is resized
if (this.tag.activeApp.sideboxWidth !== false)
{
this.tag.activeApp.sideboxWidth = _width;
}
}
}
this.tag.sideboxSizeCallback(_width);
// Notify app about change
if(this.tag.activeApp && this.tag.activeApp.browser != null)
{
this.tag.activeApp.browser.callResizeHandler();
}
},
/**
*
*/
resizeHandler: function()
{
// Tabs overflow needs to be checked again
this.checkTabOverflow();
//Resize the browser area of the applications
for (var app in this.applications)
{
if (this.applications[app].browser != null)
{
this.applications[app].browser.resize();
}
}
//Update the scroll area
this.scrollAreaUi.update();
},
/**
* Sets the sidebox data of an application
* @param {object} _app the application whose sidebox content should be set.
* @param {object} _data an array/object containing the data of the sidebox content
* @param {string} _md5 an md5 hash of the sidebox menu content: Only if this hash differs between two setSidebox calles, the sidebox menu will be updated.
*/
setSidebox: function(_app, _data, _md5)
{
this._super.apply(this,arguments);
if (typeof _app == 'string') _app = this.getApplicationByName(_app);
//Set the sidebox width if a application specific sidebox width is set
if (_app && _app == _app.parentFw.activeApp && _app.sideboxWidth !== false )
{
this.splitterUi.constraints[0].size = _app.sideboxWidth;
}
this.getToggleSidebarState();
},
/**
*
* @param {app object} _app
* @param {int} _pos
* Checks whether the application already owns a tab and creates one if it doesn't exist
*/
createApplicationTab: function(_app, _pos)
{
this._super.apply(this, arguments);
},
/**
* Runs after et2 is loaded
*
*/
et2_loadingFinished: function() {
this.checkTabOverflow();
var $logout = jQuery('#topmenu_logout');
var self = this;
if (!$logout.hasClass('onLogout'))
{
$logout.on('click', function(e){
e.preventDefault();
self.callOnLogout(e);
window.framework.redirect(this.href);
});
$logout.addClass('onLogout');
}
},
/**
* Check to see if the tab header will overflow and want to wrap.
* Deal with it by setting some smaller widths on the tabs.
*/
checkTabOverflow: function()
{
var width = 0;
var outer_width = jQuery(this.tabsUi.contHeaderDiv).width();
var spans = jQuery(this.tabsUi.contHeaderDiv).children('span');
spans.css('max-width','');
spans.each(function() { width += jQuery(this).outerWidth(true);});
if(width > outer_width)
{
var max_width = Math.floor(outer_width / this.tabsUi.contHeaderDiv.childElementCount) -
(spans.outerWidth(true) - spans.width());
spans.css('max-width',max_width + 'px');
}
},
/**
* @param {function} _opened
* Sends sidemenu entry category open/close information to the server using an AJAX request
*/
categoryOpenCloseCallback: function(_opened)
{
if (!framework.isAnInternalApp(this.tag)) egw.set_preference(this.tag.internalName, 'jdots_sidebox_'+this.catName, _opened);
},
categoryAnimationCallback: function()
{
this.tag.parentFw.scrollAreaUi.update();
},
/**
* toggleSidebar callback function, handles preference and resize
* @param {string} _state state can be on/off
*/
_toggleSidebarCallback: function (_state)
{
var splitterWidth = egw.preference('jdotssideboxwidth',this.activeApp.internalName) || this.activeApp.sideboxWidth;
if (_state === "on")
{
this.splitterUi.resizeCallback(70,'toggle');
if (!framework.isAnInternalApp(this.activeApp)) egw.set_preference(this.activeApp.internalName, 'toggleSidebar', 'on');
}
else
{
this.splitterUi.resizeCallback(splitterWidth);
if (!framework.isAnInternalApp(this.activeApp)) egw.set_preference(this.activeApp.internalName, 'toggleSidebar', 'off');
}
},
/**
* function to get the stored toggleSidebar state and set the sidebar accordingly
*/
getToggleSidebarState: function()
{
var toggleSidebar = egw.preference('toggleSidebar',this.activeApp.internalName);
this.toggleSidebarUi.set_toggle(toggleSidebar?toggleSidebar:"off", this._toggleSidebarCallback, this);
},
toggle_avatar_menu: function ()
{
var $menu = jQuery('#egw_fw_topmenu');
var $body = jQuery('body');
if (!$menu.is(":visible"))
{
$body.on('click', function(e){
if (e.target.id != 'topmenu_info_user_avatar' && jQuery(e.target).parents('#topmenu_info_user_avatar').length < 1)
{
jQuery(this).off(e);
$menu.toggle();
}
});
}
else
{
$body.off('click');
}
$menu.toggle();
},
callOnLogout: function(e) {
var apps = Object.keys(framework.applications);
for(var i in apps)
{
if (app[apps[i]] && typeof app[apps[i]].onLogout === "function")
{
app[apps[i]].onLogout.call(e);
}
}
},
/**
* Notify tab
*
* @param {string} _appname
* @param {int} _value to set as notification, 0 will reset notification
*/
notifyAppTab: function(_appname, _value)
{
var tab = this.tabsUi.getTab(_appname);
// do not set tab's notification if it's the active tab
if (tab && (this.activeApp.appName != _appname || _value == 0))
{
this.tabsUi.getTab(_appname).setNotification(_value);
}
},
/**
* This method only used for status app when it tries to broadcast data to users
* avoiding throwing exceptions for users whom might have no status app access
*
* @param {type} _data
* @returns {undefined}
*/
execPushBroadcastAppStatus: function(_data)
{
if (app.status) app.status.mergeContent(_data, true);
},
/**
* Get color scheme
* @return {string|null} returns active color scheme mode or null in case browser not supporting it
* @private
*/
_get_prefers_color_scheme: function ()
{
if (window.matchMedia('(prefers-color-scheme: light)').matches)
{
return 'light';
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches)
{
return 'dark'
}
return null;
},
/**
*
* @param node
*/
toggle_darkmode: function(node, _state)
{
let state = (typeof _state != 'undefined') ? _state : node.firstElementChild.classList.contains('darkmode_on');
this._setDarkMode(state?'0':'1');
if (state == 1)
{
node.firstElementChild.classList.remove('darkmode_on');
node.firstElementChild.title = egw.lang('light mode');
}
else
{
node.firstElementChild.classList.add('darkmode_on');
node.firstElementChild.title = egw.lang('dark mode');
}
}
});
})(window);