9
app/components/SidebarMenu/MenuContext.js
Executable file
9
app/components/SidebarMenu/MenuContext.js
Executable file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const MenuContext = React.createContext({
|
||||
entries: { },
|
||||
addEntry: () => { },
|
||||
removeEntry: () => { }
|
||||
});
|
||||
|
||||
export { MenuContext };
|
169
app/components/SidebarMenu/SidebarMenu.js
Executable file
169
app/components/SidebarMenu/SidebarMenu.js
Executable file
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import _ from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { withPageConfig } from './../Layout/withPageConfig'
|
||||
import Common from './../../common';
|
||||
import { MenuContext } from './MenuContext';
|
||||
|
||||
class SidebarMenu extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
currentUrl: PropTypes.string,
|
||||
slim: PropTypes.bool,
|
||||
location: PropTypes.object,
|
||||
pageConfig: PropTypes.object,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
containerRef = React.createRef();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
entries: this.entries = { }
|
||||
};
|
||||
}
|
||||
|
||||
addEntry(entry) {
|
||||
this.setState({
|
||||
entries: this.entries = {
|
||||
...this.entries,
|
||||
[entry.id]: {
|
||||
open: false,
|
||||
active: false,
|
||||
...entry
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateEntry(id, stateMods) {
|
||||
this.setState({
|
||||
entries: this.entries = {
|
||||
...this.state.entries,
|
||||
[id]: {
|
||||
...this.state.entries[id],
|
||||
...stateMods
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeEntry(id) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { [id]: toRemove, ...rest } = this.state.entries;
|
||||
this.setState({ entries: this.entries = rest });
|
||||
}
|
||||
|
||||
setActiveEntries(openActive = false) {
|
||||
const activeId = (childEntry, entries, previous = []) => {
|
||||
if (childEntry.parentId) {
|
||||
const parentEntry = entries[childEntry.parentId];
|
||||
const activeIds = [...previous, parentEntry.id];
|
||||
return activeId(parentEntry, entries, activeIds);
|
||||
}
|
||||
return previous;
|
||||
}
|
||||
|
||||
const activeChild = _.find(this.state.entries, (entry) => {
|
||||
const { pathname } = this.props.location;
|
||||
|
||||
const noTailSlashLocation = pathname[pathname.length - 1] === '/' && pathname.length > 1 ?
|
||||
pathname.replace(/\/$/, '') : pathname;
|
||||
|
||||
return entry.exact ?
|
||||
entry.url === noTailSlashLocation :
|
||||
_.includes(noTailSlashLocation, entry.url)
|
||||
});
|
||||
|
||||
if (activeChild) {
|
||||
const activeEntries = [...activeId(activeChild, this.entries), activeChild.id];
|
||||
|
||||
this.setState({
|
||||
entries: this.entries = _.mapValues(this.entries, (entry) => {
|
||||
const isActive = _.includes(activeEntries, entry.id);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
active: isActive,
|
||||
open: openActive ? (!entry.url && isActive) : entry.open
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.sidebarAnimation = new Common.SideMenuAnimate();
|
||||
this.sidebarAnimation.assignParentElement(
|
||||
this.containerRef.current
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
this.setActiveEntries(true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location.pathname !== prevProps.location.pathname) {
|
||||
this.setActiveEntries();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.sidebarAnimation) {
|
||||
this.sidebarAnimation.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const isSlim = this.props.slim || (
|
||||
this.props.pageConfig.sidebarSlim &&
|
||||
this.props.pageConfig.sidebarCollapsed && (
|
||||
this.props.pageConfig.screenSize === 'lg' ||
|
||||
this.props.pageConfig.screenSize === 'xl'
|
||||
)
|
||||
);
|
||||
const sidebarMenuClass = classNames('sidebar-menu', {
|
||||
'sidebar-menu--slim': isSlim,
|
||||
'sidebar-menu--disabled': this.props.disabled
|
||||
});
|
||||
|
||||
return (
|
||||
<MenuContext.Provider
|
||||
value={{
|
||||
entries: this.state.entries,
|
||||
addEntry: this.addEntry.bind(this),
|
||||
updateEntry: this.updateEntry.bind(this),
|
||||
removeEntry: this.removeEntry.bind(this)
|
||||
}}
|
||||
>
|
||||
<ul className={ sidebarMenuClass } ref={ this.containerRef }>
|
||||
{
|
||||
React.Children.map(this.props.children, (child) =>
|
||||
<MenuContext.Consumer>
|
||||
{
|
||||
(ctx) => React.cloneElement(child, {
|
||||
...ctx,
|
||||
currentUrl: this.props.location.pathname,
|
||||
slim: isSlim
|
||||
})
|
||||
}
|
||||
</MenuContext.Consumer>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</MenuContext.Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const RouterSidebarMenu = withPageConfig(withRouter(SidebarMenu));
|
||||
|
||||
export {
|
||||
RouterSidebarMenu as SidebarMenu
|
||||
};
|
173
app/components/SidebarMenu/SidebarMenuItem.js
Executable file
173
app/components/SidebarMenu/SidebarMenuItem.js
Executable file
@@ -0,0 +1,173 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
import { MenuContext } from './MenuContext';
|
||||
|
||||
/**
|
||||
* Renders a collapse trigger or a ReactRouter Link
|
||||
*/
|
||||
const SidebarMenuItemLink = (props) => (
|
||||
(props.to || props.href) ? (
|
||||
props.to ? (
|
||||
<Link to={ props.to } className={`${props.classBase}__entry__link`}>
|
||||
{ props.children }
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
href={ props.href }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`${props.classBase}__entry__link`}
|
||||
>
|
||||
{ props.children }
|
||||
</a>
|
||||
)
|
||||
|
||||
) : (
|
||||
<a
|
||||
href="javascript:;"
|
||||
className={`${props.classBase}__entry__link`}
|
||||
onClick={ () => props.onToggle() }
|
||||
>
|
||||
{ props.children }
|
||||
</a>
|
||||
)
|
||||
)
|
||||
SidebarMenuItemLink.propTypes = {
|
||||
to: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
onToggle: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
classBase: PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* The main menu entry component
|
||||
*/
|
||||
export class SidebarMenuItem extends React.Component {
|
||||
static propTypes = {
|
||||
// MenuContext props
|
||||
addEntry: PropTypes.func,
|
||||
updateEntry: PropTypes.func,
|
||||
removeEntry: PropTypes.func,
|
||||
entries: PropTypes.object,
|
||||
// Provided props
|
||||
parentId: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
isSubNode: PropTypes.bool,
|
||||
currentUrl: PropTypes.string,
|
||||
slim: PropTypes.bool,
|
||||
// User props
|
||||
icon: PropTypes.node,
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
]),
|
||||
to: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
exact: PropTypes.bool,
|
||||
noCaret: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
exact: true
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.id = uuid();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const entry = {
|
||||
id: this.id,
|
||||
parentId: this.props.parentId,
|
||||
exact: !!this.props.exact
|
||||
};
|
||||
|
||||
if (this.props.to) {
|
||||
entry.url = this.props.to;
|
||||
}
|
||||
|
||||
this.props.addEntry(entry);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.removeEntry(this.id);
|
||||
}
|
||||
|
||||
getEntry() {
|
||||
return this.props.entries[this.id];
|
||||
}
|
||||
|
||||
toggleNode() {
|
||||
const entry = this.getEntry();
|
||||
|
||||
this.props.updateEntry(this.id, { open: !entry.open });
|
||||
}
|
||||
|
||||
render() {
|
||||
const entry = this.getEntry();
|
||||
const classBase = this.props.isSubNode ? "sidebar-submenu" : "sidebar-menu";
|
||||
const itemClass = classNames(`${classBase}__entry`, {
|
||||
[`${classBase}__entry--nested`]: !!this.props.children,
|
||||
'open': entry && entry.open,
|
||||
'active': entry && entry.active
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classNames(itemClass, {
|
||||
'sidebar-menu__entry--no-caret': this.props.noCaret,
|
||||
})}
|
||||
>
|
||||
<SidebarMenuItemLink
|
||||
to={ this.props.to || null }
|
||||
href={ this.props.href || null }
|
||||
onToggle={ this.toggleNode.bind(this) }
|
||||
classBase={ classBase }
|
||||
>
|
||||
{
|
||||
this.props.icon && React.cloneElement(this.props.icon, {
|
||||
className: classNames(
|
||||
this.props.icon.props.className,
|
||||
`${classBase}__entry__icon`
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
typeof this.props.title === 'string' ?
|
||||
<span>{ this.props.title }</span> :
|
||||
this.props.title
|
||||
}
|
||||
</SidebarMenuItemLink>
|
||||
{
|
||||
this.props.children && (
|
||||
<ul className="sidebar-submenu">
|
||||
{
|
||||
React.Children.map(this.props.children, (child) => (
|
||||
<MenuContext.Consumer>
|
||||
{
|
||||
(ctx) => React.cloneElement(child, {
|
||||
isSubNode: true,
|
||||
parentId: this.id,
|
||||
currentUrl: this.props.currentUrl,
|
||||
slim: this.props.slim,
|
||||
...ctx
|
||||
})
|
||||
}
|
||||
</MenuContext.Consumer>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
6
app/components/SidebarMenu/index.js
Executable file
6
app/components/SidebarMenu/index.js
Executable file
@@ -0,0 +1,6 @@
|
||||
import { SidebarMenu } from './SidebarMenu';
|
||||
import { SidebarMenuItem } from './SidebarMenuItem';
|
||||
|
||||
SidebarMenu.Item = SidebarMenuItem;
|
||||
|
||||
export default SidebarMenu;
|
Reference in New Issue
Block a user