improve sidebar

This commit is contained in:
srgooglo 2022-11-08 22:44:09 +00:00
parent e697bf43f3
commit 844ec95349
5 changed files with 325 additions and 386 deletions

View File

@ -1,59 +1,99 @@
import React from "react"
import { Layout, Menu, Avatar } from "antd"
import { Menu, Avatar } from "antd"
import { Translation } from "react-i18next"
import classnames from "classnames"
import config from "config"
import { Icons, createIconRender } from "components/Icons"
import { sidebarKeys as defaultSidebarItems } from "schemas/defaultSettings"
import sidebarItems from "schemas/routes.json"
import { Translation } from "react-i18next"
import { SidebarEditor } from "./components"
import "./index.less"
const { Sider } = Layout
const onClickHandlers = {
settings: (event) => {
window.app.openSettings()
},
}
const getSidebarComponents = () => {
const items = {}
sidebarItems.forEach((item, index) => {
items[item.id] = {
...item,
index,
content: (
<>
{createIconRender(item.icon)} {item.title}
</>
),
}
})
return items
}
const generateItems = () => {
const components = getSidebarComponents()
const itemsMap = []
const pathResolvers = {}
const keys = window.app?.settings.get("sidebarKeys") ?? defaultSidebarItems
// filter undefined components to avoid error
keys.filter((key) => {
if (typeof components[key] !== "undefined") {
return true
}
})
keys.forEach((key, index) => {
const component = components[key]
try {
// avoid if item is duplicated
if (itemsMap.includes(component)) {
return false
}
if (typeof component.path !== "undefined") {
pathResolvers[component.id] = component.path
}
itemsMap.push(component)
} catch (error) {
return console.log(error)
}
})
return {
itemsMap,
pathResolvers,
}
}
export default class Sidebar extends React.Component {
constructor(props) {
super(props)
this.controller = window.app["SidebarController"] = {
toggleVisibility: this.toggleVisibility,
toggleEdit: this.toggleEditMode,
toggleElevation: this.toggleElevation,
attachElement: this.attachElement,
toggleCollapse: this.toggleCollapse,
isVisible: () => this.state.visible,
isEditMode: () => this.state.visible,
isCollapsed: () => this.state.collapsed,
}
this.state = {
editMode: false,
visible: false,
loading: true,
collapsed: window.app.settings.get("collapseOnLooseFocus") ?? false,
pathResolve: {},
menus: {},
extraItems: {
bottom: [],
top: [],
},
elevated: false,
additionalElements: [],
collapsed: window.app.settings.get("collapseOnLooseFocus") ?? false,
pathResolvers: null,
menus: null,
}
window.app.eventBus.on("edit_sidebar", () => this.toggleEditMode())
window.app.eventBus.on("settingChanged.sidebar_collapse", (value) => {
this.toggleCollapse(value)
})
// handle sidedrawer open/close
window.app.eventBus.on("sidedrawer.hasDrawers", () => {
this.toggleElevation(true)
@ -66,112 +106,21 @@ export default class Sidebar extends React.Component {
collapseDebounce = null
componentDidMount = async () => {
await this.loadSidebarItems()
await this.loadItems()
setTimeout(() => {
this.controller.toggleVisibility(true)
}, 100)
}
getStoragedKeys = () => {
return window.app.settings.get("sidebarKeys")
}
attachElement = (element) => {
this.setState({
additionalElements: [...this.state.additionalElements, element],
})
}
appendItem = (item = {}) => {
const { position } = item
if (typeof position === "undefined" && typeof this.state.extraItems[position] === "undefined") {
console.error("Invalid position")
return false
}
const state = this.state.extraItems
state[position].push(item)
this.setState({ extraItems: state })
}
loadSidebarItems = () => {
const items = {}
const itemsMap = []
// parse all items from schema
sidebarItems.forEach((item, index) => {
items[item.id] = {
...item,
index,
content: (
<>
{createIconRender(item.icon)} {item.title}
</>
),
}
})
// filter undefined to avoid error
let keys = (this.getStoragedKeys() ?? defaultSidebarItems).filter((key) => {
if (typeof items[key] !== "undefined") {
return true
}
})
// short items
keys.forEach((id, index) => {
const item = items[id]
if (item.locked) {
if (item.index !== index) {
keys = keys.move(index, item.index)
//update index
window.app.settings.set("sidebarKeys", keys)
}
}
})
// set items from scoped keys
keys.forEach((key, index) => {
const item = items[key]
try {
// avoid if item is duplicated
if (itemsMap.includes(item)) {
return false
}
let valid = true
if (typeof item.requireState === "object") {
const { key, value } = item.requireState
//* TODO: check global state
}
// end validation
if (!valid) {
return false
}
if (typeof item.path !== "undefined") {
let resolvers = this.state.pathResolve ?? {}
resolvers[item.id] = item.path
this.setState({ pathResolve: resolvers })
}
itemsMap.push(item)
} catch (error) {
return console.log(error)
}
})
loadItems = async () => {
const generation = generateItems()
// update states
this.setState({ items, menus: itemsMap, loading: false })
await this.setState({
menus: generation.itemsMap,
pathResolvers: generation.pathResolvers,
})
}
renderMenuItems(items) {
@ -184,8 +133,7 @@ export default class Sidebar extends React.Component {
return items.map((item) => {
if (Array.isArray(item.children)) {
return (
<Menu.SubMenu
return <Menu.SubMenu
key={item.id}
icon={handleRenderIcon(item.icon)}
title={<span>
@ -197,16 +145,13 @@ export default class Sidebar extends React.Component {
>
{this.renderMenuItems(item.children)}
</Menu.SubMenu>
)
}
return (
<Menu.Item key={item.id} icon={handleRenderIcon(item.icon)} {...item.props}>
return <Menu.Item key={item.id} icon={handleRenderIcon(item.icon)} {...item.props}>
<Translation>
{t => t(item.title ?? item.id)}
</Translation>
</Menu.Item>
)
})
}
@ -223,29 +168,16 @@ export default class Sidebar extends React.Component {
if (typeof onClickHandlers[e.key] === "function") {
return onClickHandlers[e.key](e)
}
if (typeof this.state.pathResolve[e.key] !== "undefined") {
return window.app.setLocation(`/${this.state.pathResolve[e.key]}`, 150)
if (typeof this.state.pathResolvers === "object") {
if (typeof this.state.pathResolvers[e.key] !== "undefined") {
return window.app.setLocation(`/${this.state.pathResolvers[e.key]}`, 150)
}
}
return window.app.setLocation(`/${e.key}`, 150)
}
toggleEditMode = (to) => {
if (typeof to === "undefined") {
to = !this.state.editMode
}
if (to) {
window.app.eventBus.emit("clearAllOverlays")
} else {
if (this.itemsMap !== this.getStoragedKeys()) {
this.loadSidebarItems()
}
}
this.setState({ editMode: to, collapsed: false })
}
toggleCollapse = (to) => {
if (!this.state.editMode) {
this.setState({ collapsed: to ?? !this.state.collapsed })
@ -261,11 +193,12 @@ export default class Sidebar extends React.Component {
}
onMouseEnter = () => {
if (window.app.settings.is("collapseOnLooseFocus", false)) {
return false
}
if (!this.state.visible) return
if (window.app.settings.is("collapseOnLooseFocus", false)) return
clearTimeout(this.collapseDebounce)
this.collapseDebounce = null
if (this.state.collapsed) {
@ -274,49 +207,29 @@ export default class Sidebar extends React.Component {
}
handleMouseLeave = () => {
if (window.app.settings.is("collapseOnLooseFocus", false)) {
return false
}
if (!this.state.visible) return
if (window.app.settings.is("collapseOnLooseFocus", false)) return
if (!this.state.collapsed) {
this.collapseDebounce = setTimeout(() => { this.toggleCollapse(true) }, window.app.settings.get("autoCollapseDelay") ?? 500)
}
}
renderExtraItems = (position) => {
return this.state.extraItems[position].map((item = {}) => {
if (typeof item.icon !== "undefined") {
if (typeof item.props !== "object") {
item.props = Object()
}
item.props["icon"] = createIconRender(item.icon)
}
return <Menu.Item key={item.id} {...item.props}>{item.children}</Menu.Item>
})
}
render() {
if (this.state.loading) return null
const { user } = this.props
if (!this.state.menus) return null
return (
<Sider
<div
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.handleMouseLeave}
theme={this.props.theme}
width={this.state.editMode ? 400 : 200}
collapsed={this.state.editMode ? false : this.state.collapsed}
onCollapse={() => this.props.onCollapse()}
className={
classnames(
"sidebar",
"app_sidebar",
{
["edit_mode"]: this.state.editMode,
["collapsed"]: this.state.visible && this.state.collapsed,
["elevated"]: this.state.visible && this.state.elevated,
["hidden"]: !this.state.visible,
["elevated"]: this.state.elevated
}
)
}
@ -327,28 +240,14 @@ export default class Sidebar extends React.Component {
</div>
</div>
{this.state.editMode && (
<div style={{ height: "100%" }}>
<SidebarEditor />
</div>
)}
{!this.state.editMode && (
<div key="menu" className="app_sidebar_menu">
<Menu selectable={true} mode="inline" theme={this.props.theme} onClick={this.handleClick}>
<div key="menu" className="app_sidebar_menu_wrapper">
<Menu selectable={true} mode="inline" onClick={this.handleClick}>
{this.renderMenuItems(this.state.menus)}
{this.renderExtraItems("top")}
</Menu>
</div>
)}
{!this.state.editMode && <div key="additionalElements" className="additionalElements">
{this.state.additionalElements}
</div>}
{!this.state.editMode && (
<div key="bottom" className="app_sidebar_bottom">
<Menu selectable={false} mode="inline" theme={this.props.theme} onClick={this.handleClick}>
<div key="bottom" className={classnames("app_sidebar_menu_wrapper", "bottom")}>
<Menu selectable={false} mode="inline" onClick={this.handleClick}>
<Menu.Item key="search" icon={<Icons.Search />} overrideEvent="app.openSearcher" >
<Translation>
{(t) => t("Search")}
@ -369,28 +268,21 @@ export default class Sidebar extends React.Component {
{t => t("Settings")}
</Translation>
</Menu.Item>
{
user && <Menu.Item key="account">
<div className="user_avatar">
<Avatar shape="square" src={user?.avatar} />
</div>
app.userData && <Menu.Item key="account" className="user_avatar">
<Avatar shape="square" src={app.userData?.avatar} />
</Menu.Item>
}
{
!user && <Menu.Item key="login" icon={<Icons.LogIn />}>
!app.userData && <Menu.Item key="login" icon={<Icons.LogIn />}>
<Translation>
{t => t("Login")}
</Translation>
</Menu.Item>
}
{this.renderExtraItems("bottom")}
</Menu>
</div>
)}
</Sider>
</div>
)
}
}

View File

@ -1,18 +1,45 @@
@import "theme/vars.less";
// SIDEBAR
.ant-layout-sider {
.app_sidebar {
position: relative;
display: flex;
flex-direction: column;
left: 0;
top: 0;
z-index: 1000;
background: var(--sidebar-background-color) !important;
background-color: var(--sidebar-background-color) !important;
border-radius: 0 @app_sidebar_borderRadius @app_sidebar_borderRadius 0;
overflow: hidden;
border: 1px solid var(--sidebar-background-color);
width: @app_sidebar_collapsed_width;
min-width: @app_sidebar_width;
height: 100vh;
padding: 10px 0;
transition: all 150ms ease-in-out;
background-color: var(--sidebar-background-color);
border-radius: 0 @app_sidebar_borderRadius @app_sidebar_borderRadius 0;
border: 1px solid var(--sidebar-background-color);
&.collapsed {
width: 80px;
min-width: 80px;
.app_sidebar_menu_wrapper {
.ant-menu {
.ant-menu-item:not(.user_avatar) {
.ant-menu-title-content {
animation: disappear 0.3s ease-out forwards;
}
}
}
}
}
&.hidden {
flex: 0 !important;
min-width: 0 !important;
@ -23,85 +50,16 @@
&.elevated {
box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.1) !important;
}
}
.ant-menu-item {
color: var(--background-color-contrast);
h1,
h2,
h3,
h4,
h5,
h6,
span,
p {
color: var(--background-color-contrast);
}
}
.ant-menu,
.ant-menu ul {
background: transparent !important;
background-color: transparent !important;
border-right: 0 !important;
}
.sidebar .ant-layout-sider-children {
margin-top: 15px !important;
margin-bottom: 15px !important;
background: transparent !important;
background-color: transparent !important;
user-select: none;
--webkit-user-select: none;
transition: all 150ms ease-in-out;
height: 100%;
.app_sidebar_header {
display: flex;
flex-direction: column;
&.edit_mode .ant-layout-sider-children {
background: transparent !important;
background-color: transparent !important;
height: 17%;
margin-top: 15px !important;
.app_sidebar_menu_wrapper {
opacity: 0;
height: 0;
overflow: hidden;
}
}
}
.app_sidebar_menu_wrapper {
transition: all 450ms ease-in-out;
height: 100%;
width: 100%;
}
.app_sidebar_header {
background: transparent !important;
background-color: transparent !important;
user-select: none;
--webkit-user-select: none;
display: flex;
flex-direction: column;
height: 15%;
margin-top: 5%;
margin-bottom: 5%;
}
.app_sidebar_header_logo {
user-select: none;
--webkit-user-select: none;
padding: 10px 0;
.app_sidebar_header_logo {
display: flex;
align-items: center;
justify-content: center;
@ -119,42 +77,100 @@
max-width: 40px;
}
}
}
.app_sidebar_menu {
background: transparent !important;
background-color: transparent !important;
}
}
.app_sidebar_menu_wrapper {
height: 65%;
width: 100%;
overflow: overlay;
overflow-x: hidden;
}
.app_sidebar_bottom {
display: flex;
flex-direction: column;
align-items: center;
transition: all 150ms ease-in-out;
&.bottom {
position: absolute;
bottom: 0;
padding-bottom: 30px;
z-index: 50;
left: 0;
background: transparent !important;
background-color: transparent !important;
backdrop-filter: blur(10px);
--webkit-backdrop-filter: blur(10px);
width: 100%;
height: fit-content;
.ant-menu,
ul {
background: transparent !important;
background-color: transparent !important;
padding-bottom: 30px;
}
}
.user_avatar {
.ant-menu {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
transition: all 150ms ease-in-out;
.ant-menu-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
width: 90%;
padding: 0 10px !important;
margin: 5px 0 !important;
transition: all 150ms ease-in-out;
.ant-menu-title-content {
flex: 1;
}
&.user_avatar {
.ant-menu-title-content {
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
}
margin: 0;
padding: 0;
}
svg {
width: fit-content;
margin: 0 !important;
height: 16px;
}
}
}
}
}
@keyframes disappear {
0% {
opacity: 1;
width: 100%;
}
95% {
opacity: 0;
width: 0px;
margin: 0;
}
100% {
opacity: 0;
width: 0px;
margin: 0;
flex: 0;
}
}

View File

@ -172,6 +172,29 @@
}
// fix menu colors
.ant-menu,
.ant-menu ul {
background: transparent !important;
background-color: transparent !important;
border-right: 0 !important;
.ant-menu-item {
color: var(--background-color-contrast);
h1,
h2,
h3,
h4,
h5,
h6,
span,
p {
color: var(--background-color-contrast);
}
}
}
.ant-menu-item {
color: var(--text-color);
border-radius: 8px;

View File

@ -119,29 +119,25 @@ html {
}
}
.ant-layout,
.content_layout {
width: 100%;
height: 100%;
max-height: 100vh;
background-color: transparent;
}
.app_layout {
background-color: rgba(var(--layoutBackgroundColor), var(--backgroundColorTransparency)) !important;
backdrop-filter: blur(var(--backgroundBlur));
position: relative;
-webkit-overflow-scrolling: touch;
width: 100%;
height: 100%;
max-height: 100vh;
overflow: hidden;
display: flex;
flex-direction: row;
background-color: rgba(var(--layoutBackgroundColor), var(--backgroundColorTransparency)) !important;
backdrop-filter: blur(var(--backgroundBlur));
transition: all 150ms ease-in-out;
-webkit-overflow-scrolling: touch;
::-webkit-scrollbar {
display: block;
position: absolute;
@ -164,6 +160,15 @@ html {
}
}
.ant-layout,
.content_layout {
width: 100%;
height: 100%;
max-height: 100vh;
background-color: transparent;
}
.layout_page {
position: relative;
-webkit-overflow-scrolling: touch;

View File

@ -1,5 +1,8 @@
//* Now this only works as an fallback for unset dynamic theme values
@app_sidebar_collapsed_width: 80px;
@app_sidebar_width: 230px;
// borders & radius
@app_sidebar_borderRadius: 18px;