| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- import React, { Component } from 'react';
- import PropTypes from 'prop-types';
- import { withTranslation, Trans } from '@/components/Common/Translate';
- import Link from 'next/link';
- import Router, { withRouter } from 'next/router';
- import { Collapse, Badge } from 'reactstrap';
- import { connect } from 'react-redux';
- import { bindActionCreators } from 'redux';
- import * as actions from '../../store/actions/actions';
- import SidebarUserBlock from './SidebarUserBlock';
- import Menu from './Menu.js';
- // Helper to check for parrent of an given elements
- const parents = (element, selector) => {
- if (typeof selector !== 'string') {
- return null;
- }
- const parents = [];
- let ancestor = element.parentNode;
- while (
- ancestor &&
- ancestor.nodeType === Node.ELEMENT_NODE &&
- ancestor.nodeType !== 3 /*NODE_TEXT*/
- ) {
- if (ancestor.matches(selector)) {
- parents.push(ancestor);
- }
- ancestor = ancestor.parentNode;
- }
- return parents;
- };
- // Helper to get outerHeight of a dom element
- const outerHeight = (elem, includeMargin) => {
- const style = getComputedStyle(elem);
- const margins = includeMargin
- ? parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10)
- : 0;
- return elem.offsetHeight + margins;
- };
- /**
- Component to display headings on sidebar
- */
- const SidebarItemHeader = ({ item }) => (
- <li className="nav-heading">
- <span>
- <Trans i18nKey={item.translate}>{item.heading}</Trans>
- </span>
- </li>
- );
- /**
- Normal items for the sidebar
- */
- const SidebarItem = ({ item, isActive, className, onMouseEnter }) => (
- <li className={isActive ? 'active' : ''} onMouseEnter={onMouseEnter}>
- <Link href={item.path} as={item.as}>
- <a title={item.name}>
- {item.label && (
- <Badge tag="div" className="float-right" color={item.label.color}>
- {item.label.value}
- </Badge>
- )}
- {item.icon && <em className={item.icon} />}
- <span>
- <Trans i18nKey={item.translate}>{item.name}</Trans>
- </span>
- </a>
- </Link>
- </li>
- );
- /**
- Build a sub menu with items inside and attach collapse behavior
- */
- const SidebarSubItem = ({ item, isActive, handler, children, isOpen, onMouseEnter }) => (
- <li className={isActive ? 'active' : ''}>
- <div className="nav-item" onClick={handler} onMouseEnter={onMouseEnter}>
- {item.label && (
- <Badge tag="div" className="float-right" color={item.label.color}>
- {item.label.value}
- </Badge>
- )}
- {item.icon && <em className={item.icon} />}
- <span>
- <Trans i18nKey={item.translate}>{item.name}</Trans>
- </span>
- </div>
- <Collapse isOpen={isOpen}>
- <ul id={item.path} className="sidebar-nav sidebar-subnav">
- {children}
- </ul>
- </Collapse>
- </li>
- );
- /**
- Component used to display a header on menu when using collapsed/hover mode
- */
- const SidebarSubHeader = ({ item }) => <li className="sidebar-subnav-header">{item.name}</li>;
- const SidebarBackdrop = ({ closeFloatingNav }) => (
- <div className="sidebar-backdrop" onClick={closeFloatingNav} />
- );
- const FloatingNav = ({ item, target, routeActive, isFixed, closeFloatingNav }) => {
- let asideContainer = document.querySelector('.aside-container');
- let asideInner = asideContainer.firstElementChild; /*('.aside-inner')*/
- let sidebar = asideInner.firstElementChild; /*('.sidebar')*/
- let mar =
- parseInt(getComputedStyle(asideInner)['padding-top'], 0) +
- parseInt(getComputedStyle(asideContainer)['padding-top'], 0);
- let itemTop = target.parentElement.offsetTop + mar - sidebar.scrollTop;
- let vwHeight = document.body.clientHeight;
- const setPositionStyle = el => {
- if (!el) return;
- el.style.position = isFixed ? 'fixed' : 'absolute';
- el.style.top = itemTop + 'px';
- el.style.bottom = outerHeight(el, true) + itemTop > vwHeight ? 0 : 'auto';
- };
- return (
- <ul
- id={item.path}
- ref={setPositionStyle}
- className="sidebar-nav sidebar-subnav nav-floating"
- onMouseLeave={closeFloatingNav}
- >
- <SidebarSubHeader item={item} />
- {item.submenu.map((subitem, i) => (
- <SidebarItem key={i} item={subitem} isActive={routeActive(subitem.path)} />
- ))}
- </ul>
- );
- };
- /**
- The main sidebar component
- */
- class Sidebar extends Component {
- state = {
- collapse: {},
- showSidebarBackdrop: false,
- currentFloatingItem: null,
- currentFloatingItemTarget: null,
- pathname: this.props.router.pathname
- };
- componentDidMount() {
- // prepare the flags to handle menu collapsed states
- this.buildCollapseList();
- // Listen for routes changes in order to hide the sidebar on mobile
- Router.events.on('routeChangeStart', this.handleRouteChange);
- Router.events.on('routeChangeComplete', this.handleRouteComplete);
- // Attach event listener to automatically close sidebar when click outside
- document.addEventListener('click', this.closeSidebarOnExternalClicks);
- }
- handleRouteComplete = (pathname) => {
- this.setState({
- pathname
- })
- }
- handleRouteChange = () => {
- this.closeFloatingNav();
- this.closeSidebar();
- };
- componentWillUnmount() {
- document.removeEventListener('click', this.closeSidebarOnExternalClicks);
- Router.events.off('routeChangeStart', this.handleRouteChange);
- Router.events.off('routeChangeComplete', this.handleRouteComplete);
- }
- closeSidebar = () => {
- this.props.actions.toggleSetting('asideToggled');
- };
- closeSidebarOnExternalClicks = e => {
- // don't check if sidebar not visible
- if (!this.props.settings.asideToggled) return;
- if (
- !parents(e.target, '.aside-container').length && // if not child of sidebar
- !parents(e.target, '.topnavbar-wrapper').length && // if not child of header
- !e.target.matches('#user-block-toggle') && // user block toggle anchor
- !e.target.parentElement.matches('#user-block-toggle') // user block toggle icon
- ) {
- this.closeSidebar();
- }
- };
- /** prepare initial state of collapse menus.*/
- buildCollapseList = () => {
- let collapse = {};
- Menu.filter(({ heading }) => !heading).forEach(({ name, path, submenu }) => {
- collapse[name] = this.routeActive(submenu ? submenu.map(({ path }) => path) : path);
- });
- this.setState({ collapse });
- };
- routeActive = paths => {
- const currpath = this.state.pathname;
- paths = Array.isArray(paths) ? paths : [paths];
- return paths.some(p => (p === '/' ? currpath === p : currpath.indexOf(p) > -1));
- };
- toggleItemCollapse = stateName => () => {
- for (let c in this.state.collapse) {
- if (this.state.collapse[c] === true && c !== stateName)
- this.setState({
- collapse: {
- [c]: false
- }
- });
- }
- this.setState({
- collapse: {
- [stateName]: !this.state.collapse[stateName]
- }
- });
- };
- getSubRoutes = item => item.submenu.map(({ path }) => path);
- /** map menu config to string to determine which element to render */
- itemType = item => {
- if (item.heading) return 'heading';
- if (!item.submenu) return 'menu';
- if (item.submenu) return 'submenu';
- };
- shouldUseFloatingNav = () => {
- return (
- this.props.settings.isCollapsed ||
- this.props.settings.isCollapsedText ||
- this.props.settings.asideHover
- );
- };
- showFloatingNav = item => e => {
- if (this.shouldUseFloatingNav())
- this.setState({
- currentFloatingItem: item,
- currentFloatingItemTarget: e.currentTarget,
- showSidebarBackdrop: true
- });
- };
- closeFloatingNav = () => {
- this.setState({
- currentFloatingItem: null,
- currentFloatingItemTarget: null,
- showSidebarBackdrop: false
- });
- };
- render() {
- return (
- <>
- <aside className="aside-container">
- {/* START Sidebar (left) */}
- <div className="aside-inner">
- <nav
- className={
- 'sidebar ' +
- (this.props.settings.asideScrollbar ? 'show-scrollbar' : '')
- }
- >
- {/* START sidebar nav */}
- <ul className="sidebar-nav">
- {/* START user info */}
- <li className="has-user-block">
- <SidebarUserBlock />
- </li>
- {/* END user info */}
- {/* Iterates over all sidebar items */}
- {Menu.map((item, i) => {
- // heading
- if (this.itemType(item) === 'heading')
- return <SidebarItemHeader item={item} key={i} />;
- else {
- if (this.itemType(item) === 'menu')
- return (
- <SidebarItem
- isActive={this.routeActive(item.path)}
- item={item}
- key={i}
- onMouseEnter={this.closeFloatingNav}
- />
- );
- if (this.itemType(item) === 'submenu')
- return [
- <SidebarSubItem
- item={item}
- isOpen={this.state.collapse[item.name]}
- handler={this.toggleItemCollapse(item.name)}
- isActive={this.routeActive(
- this.getSubRoutes(item)
- )}
- key={i}
- onMouseEnter={this.showFloatingNav(item)}
- >
- <SidebarSubHeader item={item} key={i} />
- {item.submenu.map((subitem, i) => (
- <SidebarItem
- key={i}
- item={subitem}
- isActive={this.routeActive(
- subitem.path
- )}
- />
- ))}
- </SidebarSubItem>
- ];
- }
- return null; // unrecognized item
- })}
- </ul>
- {/* END sidebar nav */}
- </nav>
- </div>
- {/* END Sidebar (left) */}
- {this.state.currentFloatingItem && this.state.currentFloatingItem.submenu && (
- <FloatingNav
- item={this.state.currentFloatingItem}
- target={this.state.currentFloatingItemTarget}
- routeActive={this.routeActive}
- isFixed={this.props.settings.isFixed}
- closeFloatingNav={this.closeFloatingNav}
- />
- )}
- </aside>
- {this.state.showSidebarBackdrop && (
- <SidebarBackdrop closeFloatingNav={this.closeFloatingNav} />
- )}
- </>
- );
- }
- }
- Sidebar.propTypes = {
- actions: PropTypes.object,
- settings: PropTypes.object
- };
- const mapStateToProps = state => ({ settings: state.settings });
- const mapDispatchToProps = dispatch => ({
- actions: bindActionCreators(actions, dispatch)
- });
- export default connect(
- mapStateToProps,
- mapDispatchToProps
- )(withRouter(withTranslation(Sidebar)));
|