diff --git a/actions/windows.js b/actions/windows.js
index d12ae14b4..e2b00e063 100644
--- a/actions/windows.js
+++ b/actions/windows.js
@@ -17,6 +17,7 @@ export const CLOSE_ALL_WINDOWS = 'CLOSE_ALL_WINDOWS';
export const REGISTER_WINDOW = 'REGISTER_WINDOW';
export const UNREGISTER_WINDOW = 'UNREGISTER_WINDOW';
export const RAISE_WINDOW = 'RAISE_WINDOW';
+export const SET_MENU_MARGIN = 'SET_MENU_MARGIN';
export const SET_SPLIT_SCREEN = 'SET_SPLIT_SCREEN';
export const NotificationType = {
@@ -86,3 +87,11 @@ export function setSplitScreen(windowId, side, size) {
size: size
};
}
+
+export function setMenuMargin(right, left) {
+ return {
+ type: SET_MENU_MARGIN,
+ right: right,
+ left: left
+ };
+}
diff --git a/components/AppMenu.jsx b/components/AppMenu.jsx
index 718ed1621..285f6bb73 100644
--- a/components/AppMenu.jsx
+++ b/components/AppMenu.jsx
@@ -13,6 +13,7 @@ import mousetrap from 'mousetrap';
import {remove as removeDiacritics} from 'diacritics';
import isEmpty from 'lodash.isempty';
import classnames from 'classnames';
+import {setMenuMargin} from '../actions/windows';
import {setCurrentTask} from '../actions/task';
import InputContainer from '../components/InputContainer';
import LocaleUtils from '../utils/LocaleUtils';
@@ -32,10 +33,12 @@ class AppMenu extends React.Component {
currentTaskBlocked: PropTypes.bool,
currentTheme: PropTypes.object,
keepMenuOpen: PropTypes.bool,
+ menuCompact: PropTypes.bool,
menuItems: PropTypes.array,
onMenuToggled: PropTypes.func,
openExternalUrl: PropTypes.func,
setCurrentTask: PropTypes.func,
+ setMenuMargin: PropTypes.func,
showFilterField: PropTypes.bool,
showOnStartup: PropTypes.bool
};
@@ -168,14 +171,16 @@ class AppMenu extends React.Component {
if (!this.state.menuVisible && this.props.appMenuClearsTask) {
this.props.setCurrentTask(null);
}
- if (!this.state.menuVisible) {
- document.addEventListener('click', this.checkCloseMenu);
- document.addEventListener('keydown', this.onKeyPress, true);
- document.addEventListener('mousemove', this.onMouseMove, true);
- } else {
- document.removeEventListener('click', this.checkCloseMenu);
- document.removeEventListener('keydown', this.onKeyPress, true);
- document.removeEventListener('mousemove', this.onMouseMove, true);
+ if (!this.props.keepMenuOpen) {
+ if (!this.state.menuVisible) {
+ document.addEventListener('click', this.checkCloseMenu);
+ document.addEventListener('keydown', this.onKeyPress, true);
+ document.addEventListener('mousemove', this.onMouseMove, true);
+ } else {
+ document.removeEventListener('click', this.checkCloseMenu);
+ document.removeEventListener('keydown', this.onKeyPress, true);
+ document.removeEventListener('mousemove', this.onMouseMove, true);
+ }
}
this.props.onMenuToggled(!this.state.menuVisible);
this.setState((state) => ({menuVisible: !state.menuVisible, submenusVisible: [], filter: ""}));
@@ -262,12 +267,12 @@ class AppMenu extends React.Component {
}
};
render() {
- let className = "";
- if (this.props.currentTaskBlocked) {
- className = "appmenu-blocked";
- } else if (this.state.menuVisible) {
- className = "appmenu-visible";
- }
+ const visible = !this.props.currentTaskBlocked && this.state.menuVisible;
+ const className = classnames({
+ "appmenu-blocked": this.props.currentTaskBlocked,
+ "appmenu-visible": visible,
+ "appmenu-compact": this.props.menuCompact
+ });
const filter = removeDiacritics(this.state.filter.toLowerCase());
return (
{ this.menuEl = el; MiscUtils.setupKillTouchEvents(el); }}
@@ -275,7 +280,7 @@ class AppMenu extends React.Component {
{this.props.buttonContents}
-
+
{this.props.showFilterField ? (
-
@@ -302,6 +307,12 @@ class AppMenu extends React.Component {
mousetrap(el).bind(this.props.appMenuShortcut, this.toggleMenu);
}
};
+ storeRightMargin = (el) => {
+ if (this.props.menuCompact && el?.clientWidth > 0) {
+ const rightmargin = el.clientWidth - MiscUtils.convertEmToPx(11.5);
+ this.props.setMenuMargin(rightmargin, 0);
+ }
+ };
itemAllowed = (item) => {
if (!ThemeUtils.themFlagsAllowed(this.props.currentTheme, item.themeFlagWhitelist, item. themeFlagBlacklist)) {
return false;
@@ -323,5 +334,6 @@ export default connect((state) => ({
currentTaskBlocked: state.task.blocked,
currentTheme: state.theme.current || {}
}), {
- setCurrentTask: setCurrentTask
+ setCurrentTask: setCurrentTask,
+ setMenuMargin: setMenuMargin
})(AppMenu);
diff --git a/components/ResizeableWindow.jsx b/components/ResizeableWindow.jsx
index b40f79002..8c75bf8d5 100644
--- a/components/ResizeableWindow.jsx
+++ b/components/ResizeableWindow.jsx
@@ -41,6 +41,7 @@ class ResizeableWindow extends React.Component {
maxHeight: PropTypes.number,
maxWidth: PropTypes.number,
maximizeable: PropTypes.bool,
+ menuMargins: PropTypes.object,
minHeight: PropTypes.number,
minWidth: PropTypes.number,
minimizeable: PropTypes.bool,
@@ -117,7 +118,7 @@ class ResizeableWindow extends React.Component {
componentWillUnmount() {
this.props.unregisterWindow(this.id);
if (this.props.splitScreenWhenDocked) {
- this.props.setSplitScreen(this.id, null);
+ this.props.setSplitScreen(this.id, null, null);
}
}
componentDidUpdate(prevProps, prevState) {
@@ -135,7 +136,7 @@ class ResizeableWindow extends React.Component {
(!this.props.visible && prevProps.visible) ||
(this.state.geometry.docked === false && prevState.geometry.docked !== false)
) {
- this.props.setSplitScreen(this.id, null);
+ this.props.setSplitScreen(this.id, null, null);
} else if (this.props.visible && this.state.geometry.docked) {
const dockSide = this.props.dockable === true ? "left" : this.props.dockable;
const dockSize = ["left", "right"].includes(dockSide) ? this.state.geometry.width : this.state.geometry.height;
@@ -169,7 +170,10 @@ class ResizeableWindow extends React.Component {
"resizeable-window-body-scrollable": this.props.scrollable,
"resizeable-window-body-nonscrollable": !this.props.scrollable
});
- const style = {display: this.props.visible ? 'initial' : 'none'};
+ const style = {
+ display: this.props.visible ? 'initial' : 'none',
+ right: 'calc(0.25em + ' + this.props.menuMargins.right + 'px)'
+ };
const maximized = this.state.geometry.maximized ? true : false;
const minimized = this.state.geometry.minimized ? true : false;
const zIndex = this.props.baseZIndex + this.props.windowStacking.findIndex(item => item === this.id);
@@ -327,7 +331,8 @@ class ResizeableWindow extends React.Component {
export default connect((state) => ({
windowStacking: state.windows.stacking,
topbarHeight: state.map.topbarHeight,
- bottombarHeight: state.map.bottombarHeight
+ bottombarHeight: state.map.bottombarHeight,
+ menuMargins: state.windows.menuMargins
}), {
raiseWindow: raiseWindow,
registerWindow: registerWindow,
diff --git a/components/SideBar.jsx b/components/SideBar.jsx
index 908469f58..a8ef661b4 100644
--- a/components/SideBar.jsx
+++ b/components/SideBar.jsx
@@ -26,6 +26,7 @@ class SideBar extends React.Component {
heightResizeable: PropTypes.bool,
icon: PropTypes.string,
id: PropTypes.string.isRequired,
+ menuMargins: PropTypes.object,
minWidth: PropTypes.string,
onHide: PropTypes.func,
onShow: PropTypes.func,
@@ -80,6 +81,7 @@ class SideBar extends React.Component {
const render = visible || this.state.render || this.props.renderWhenHidden;
const style = {
width: this.props.width,
+ right: visible ? 'calc(0.25em + ' + this.props.menuMargins.right + 'px)' : 0,
minWidth: this.props.minWidth,
zIndex: visible ? 5 : 4
};
@@ -166,7 +168,8 @@ class SideBar extends React.Component {
}
const selector = (state) => ({
- currentTask: state.task
+ currentTask: state.task,
+ menuMargins: state.windows.menuMargins
});
export default connect(selector, {
diff --git a/components/style/AppMenu.css b/components/style/AppMenu.css
index 1641ff85d..a951b10c4 100644
--- a/components/style/AppMenu.css
+++ b/components/style/AppMenu.css
@@ -14,6 +14,10 @@ div.AppMenu.appmenu-visible {
overflow: visible;
}
+div.AppMenu.appmenu-visible.appmenu-compact {
+ background: none;
+}
+
div.AppMenu .appmenu-label {
font-weight: bold;
transition: color 0.25s;
@@ -53,6 +57,20 @@ div.AppMenu div.appmenu-menu-container {
max-height: calc(var(--vh, 1vh) * 100 - 5.8em);
}
+div.AppMenu.appmenu-compact div.appmenu-menu-container {
+ right: -11.5em;
+ width: 15em;
+ height: calc(100vh - 86px);
+ transition: transform 0.25s, opacity 0.25s, right 0.5s;
+ background: var(--app-menu-bg-color);
+ box-shadow: 0px 0px 4px rgba(136, 136, 136, 0.5);
+ top: 3.7em;
+}
+
+div.AppMenu.appmenu-compact div.appmenu-menu-container:hover {
+ right: 0;
+}
+
div.AppMenu ul.appmenu-menu {
text-align: left;
padding: 0;
diff --git a/components/style/ResizeableWindow.css b/components/style/ResizeableWindow.css
index 8740282c8..0b4d1dec3 100644
--- a/components/style/ResizeableWindow.css
+++ b/components/style/ResizeableWindow.css
@@ -6,6 +6,7 @@ div.resizeable-window-container {
right: 0;
pointer-events: none;
}
+
div.resizeable-window {
pointer-events: auto;
background-color: var(--container-bg-color);
diff --git a/doc/plugins.md b/doc/plugins.md
index 5f7a21c52..c71492f1c 100644
--- a/doc/plugins.md
+++ b/doc/plugins.md
@@ -763,6 +763,7 @@ Top bar, containing the logo, searchbar, task buttons and app menu.
| Property | Type | Description | Default value |
|----------|------|-------------|---------------|
| appMenuClearsTask | `bool` | Whether opening the app menu clears the active task. | `undefined` |
+| appMenuCompact | `bool` | Whether show an appMenu compact (menu visible on icons hover) - Only available for desktop client. | `undefined` |
| appMenuFilterField | `bool` | Whether to display the filter field in the app menu. | `undefined` |
| appMenuShortcut | `string` | The shortcut for tiggering the app menu, i.e. alt+shift+m. | `undefined` |
| appMenuVisibleOnStartup | `bool` | Whether to open the app menu on application startup. | `undefined` |
diff --git a/plugins/LoginUser.jsx b/plugins/LoginUser.jsx
index 9013949c2..e36e1744e 100644
--- a/plugins/LoginUser.jsx
+++ b/plugins/LoginUser.jsx
@@ -7,6 +7,8 @@
*/
import React from 'react';
+import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
import Icon from '../components/Icon';
import ConfigUtils from '../utils/ConfigUtils';
import './style/LoginUser.css';
@@ -15,17 +17,29 @@ import './style/LoginUser.css';
/**
* Displays the currently logged in user.
*/
-export default class LoginUser extends React.Component {
+class LoginUser extends React.Component {
+ static propTypes = {
+ mapMargins: PropTypes.object
+ };
render() {
const username = ConfigUtils.getConfigProp("username");
+ const right = this.props.mapMargins.right;
+ const style = {
+ right: 'calc(0.25em + ' + right + 'px)'
+ };
if (!username) {
return null;
}
return (
-
+
{username}
);
}
}
+
+export default connect((state) => ({
+ mapMargins: state.windows.mapMargins
+}))(LoginUser);
+
diff --git a/plugins/TopBar.jsx b/plugins/TopBar.jsx
index db4428c05..9f169357e 100644
--- a/plugins/TopBar.jsx
+++ b/plugins/TopBar.jsx
@@ -28,6 +28,8 @@ class TopBar extends React.Component {
static propTypes = {
/** Whether opening the app menu clears the active task. */
appMenuClearsTask: PropTypes.bool,
+ /** Whether show an appMenu compact (menu visible on icons hover) - Only available for desktop client. */
+ appMenuCompact: PropTypes.bool,
/** Whether to display the filter field in the app menu. */
appMenuFilterField: PropTypes.bool,
/** The shortcut for tiggering the app menu, i.e. alt+shift+m. */
@@ -99,7 +101,7 @@ class TopBar extends React.Component {
let logo;
const assetsPath = ConfigUtils.getAssetsPath();
const tooltip = LocaleUtils.tr("appmenu.menulabel");
- if (this.props.mobile) {
+ if (this.props.mobile || this.props.appMenuCompact) {
buttonContents = (
@@ -128,6 +130,12 @@ class TopBar extends React.Component {
const searchOptions = {...this.props.searchOptions};
searchOptions.minScaleDenom = searchOptions.minScaleDenom || searchOptions.minScale;
delete searchOptions.minScale;
+ // Menu compact only available for desktop client
+ const menuCompact = !this.props.mobile ? this.props.appMenuCompact : false;
+ // Keep menu open when appMenu is in compact mode (Visible on Hover)
+ const keepMenuOpen = menuCompact;
+ // Menu should be visible on startup when appMenu is in compact mode (Visible on Hover)
+ const showOnStartup = this.props.appMenuVisibleOnStartup || menuCompact;
return (
this.props.toggleFullscreen(false)}
@@ -150,10 +158,12 @@ class TopBar extends React.Component {
appMenuClearsTask={this.props.appMenuClearsTask}
appMenuShortcut={this.props.appMenuShortcut}
buttonContents={buttonContents}
+ keepMenuOpen={keepMenuOpen}
+ menuCompact={menuCompact}
menuItems={this.props.menuItems}
openExternalUrl={this.openUrl}
showFilterField={this.props.appMenuFilterField}
- showOnStartup={this.props.appMenuVisibleOnStartup} />
+ showOnStartup={showOnStartup} />
) : null}
{this.props.components.FullscreenSwitcher ? (
diff --git a/reducers/windows.js b/reducers/windows.js
index 87a2d37f7..8544fa02e 100644
--- a/reducers/windows.js
+++ b/reducers/windows.js
@@ -14,7 +14,8 @@ import {
REGISTER_WINDOW,
UNREGISTER_WINDOW,
RAISE_WINDOW,
- SET_SPLIT_SCREEN
+ SET_SPLIT_SCREEN,
+ SET_MENU_MARGIN
} from '../actions/windows';
const defaultState = {
@@ -23,6 +24,9 @@ const defaultState = {
mapMargins: {
left: 0, top: 0, right: 0, bottom: 0
},
+ menuMargins: {
+ left: 0, right: 0
+ },
entries: {}
};
@@ -95,9 +99,9 @@ export default function windows(state = defaultState, action) {
}
const splitWindows = Object.values(newSplitScreen);
const mapMargins = {
- right: splitWindows.filter(entry => entry.side === 'right').reduce((res, e) => Math.max(e.size, res), 0),
+ right: splitWindows.filter(entry => entry.side === 'right').reduce((res, e) => Math.max(e.size, res), 0) + state.menuMargins.right,
bottom: splitWindows.filter(entry => entry.side === 'bottom').reduce((res, e) => Math.max(e.size, res), 0),
- left: splitWindows.filter(entry => entry.side === 'left').reduce((res, e) => Math.max(e.size, res), 0),
+ left: splitWindows.filter(entry => entry.side === 'left').reduce((res, e) => Math.max(e.size, res), 0) + state.menuMargins.left,
top: splitWindows.filter(entry => entry.side === 'top').reduce((res, e) => Math.max(e.size, res), 0)
};
return {
@@ -106,6 +110,19 @@ export default function windows(state = defaultState, action) {
mapMargins: mapMargins
};
}
+ case SET_MENU_MARGIN: {
+ const menuMargins = {
+ right: action.right,
+ left: action.left
+ };
+ const mapMargins = {
+ right: state.mapMargins.right + action.right,
+ bottom: state.mapMargins.bottom,
+ left: state.mapMargins.left + action.left,
+ top: state.mapMargins.top
+ };
+ return {...state, menuMargins: menuMargins, mapMargins: mapMargins};
+ }
default:
return state;
}
diff --git a/utils/MiscUtils.js b/utils/MiscUtils.js
index 983c7d260..d7f5c8838 100644
--- a/utils/MiscUtils.js
+++ b/utils/MiscUtils.js
@@ -110,6 +110,10 @@ const MiscUtils = {
return 'https:' + url.substr(5);
}
return url;
+ },
+ convertEmToPx(emsize) {
+ const defaultfontsize = getComputedStyle(document.documentElement).fontSize;
+ return emsize * parseFloat(defaultfontsize);
}
};