Sidebar.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import React, { Component } from 'react';
  2. import PropTypes from 'prop-types';
  3. import { withTranslation, Trans } from '@/components/Common/Translate';
  4. import Link from 'next/link';
  5. import Router, { withRouter } from 'next/router';
  6. import { Collapse, Badge } from 'reactstrap';
  7. import { connect } from 'react-redux';
  8. import { bindActionCreators } from 'redux';
  9. import * as actions from '../../store/actions/actions';
  10. import SidebarUserBlock from './SidebarUserBlock';
  11. import Menu from './Menu.js';
  12. // Helper to check for parrent of an given elements
  13. const parents = (element, selector) => {
  14. if (typeof selector !== 'string') {
  15. return null;
  16. }
  17. const parents = [];
  18. let ancestor = element.parentNode;
  19. while (
  20. ancestor &&
  21. ancestor.nodeType === Node.ELEMENT_NODE &&
  22. ancestor.nodeType !== 3 /*NODE_TEXT*/
  23. ) {
  24. if (ancestor.matches(selector)) {
  25. parents.push(ancestor);
  26. }
  27. ancestor = ancestor.parentNode;
  28. }
  29. return parents;
  30. };
  31. // Helper to get outerHeight of a dom element
  32. const outerHeight = (elem, includeMargin) => {
  33. const style = getComputedStyle(elem);
  34. const margins = includeMargin
  35. ? parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10)
  36. : 0;
  37. return elem.offsetHeight + margins;
  38. };
  39. /**
  40. Component to display headings on sidebar
  41. */
  42. const SidebarItemHeader = ({ item }) => (
  43. <li className="nav-heading">
  44. <span>
  45. <Trans i18nKey={item.translate}>{item.heading}</Trans>
  46. </span>
  47. </li>
  48. );
  49. /**
  50. Normal items for the sidebar
  51. */
  52. const SidebarItem = ({ item, isActive, className, onMouseEnter }) => (
  53. <li className={isActive ? 'active' : ''} onMouseEnter={onMouseEnter}>
  54. <Link href={item.path} as={item.as}>
  55. <a title={item.name}>
  56. {item.label && (
  57. <Badge tag="div" className="float-right" color={item.label.color}>
  58. {item.label.value}
  59. </Badge>
  60. )}
  61. {item.icon && <em className={item.icon} />}
  62. <span>
  63. <Trans i18nKey={item.translate}>{item.name}</Trans>
  64. </span>
  65. </a>
  66. </Link>
  67. </li>
  68. );
  69. /**
  70. Build a sub menu with items inside and attach collapse behavior
  71. */
  72. const SidebarSubItem = ({ item, isActive, handler, children, isOpen, onMouseEnter }) => (
  73. <li className={isActive ? 'active' : ''}>
  74. <div className="nav-item" onClick={handler} onMouseEnter={onMouseEnter}>
  75. {item.label && (
  76. <Badge tag="div" className="float-right" color={item.label.color}>
  77. {item.label.value}
  78. </Badge>
  79. )}
  80. {item.icon && <em className={item.icon} />}
  81. <span>
  82. <Trans i18nKey={item.translate}>{item.name}</Trans>
  83. </span>
  84. </div>
  85. <Collapse isOpen={isOpen}>
  86. <ul id={item.path} className="sidebar-nav sidebar-subnav">
  87. {children}
  88. </ul>
  89. </Collapse>
  90. </li>
  91. );
  92. /**
  93. Component used to display a header on menu when using collapsed/hover mode
  94. */
  95. const SidebarSubHeader = ({ item }) => <li className="sidebar-subnav-header">{item.name}</li>;
  96. const SidebarBackdrop = ({ closeFloatingNav }) => (
  97. <div className="sidebar-backdrop" onClick={closeFloatingNav} />
  98. );
  99. const FloatingNav = ({ item, target, routeActive, isFixed, closeFloatingNav }) => {
  100. let asideContainer = document.querySelector('.aside-container');
  101. let asideInner = asideContainer.firstElementChild; /*('.aside-inner')*/
  102. let sidebar = asideInner.firstElementChild; /*('.sidebar')*/
  103. let mar =
  104. parseInt(getComputedStyle(asideInner)['padding-top'], 0) +
  105. parseInt(getComputedStyle(asideContainer)['padding-top'], 0);
  106. let itemTop = target.parentElement.offsetTop + mar - sidebar.scrollTop;
  107. let vwHeight = document.body.clientHeight;
  108. const setPositionStyle = el => {
  109. if (!el) return;
  110. el.style.position = isFixed ? 'fixed' : 'absolute';
  111. el.style.top = itemTop + 'px';
  112. el.style.bottom = outerHeight(el, true) + itemTop > vwHeight ? 0 : 'auto';
  113. };
  114. return (
  115. <ul
  116. id={item.path}
  117. ref={setPositionStyle}
  118. className="sidebar-nav sidebar-subnav nav-floating"
  119. onMouseLeave={closeFloatingNav}
  120. >
  121. <SidebarSubHeader item={item} />
  122. {item.submenu.map((subitem, i) => (
  123. <SidebarItem key={i} item={subitem} isActive={routeActive(subitem.path)} />
  124. ))}
  125. </ul>
  126. );
  127. };
  128. /**
  129. The main sidebar component
  130. */
  131. class Sidebar extends Component {
  132. state = {
  133. collapse: {},
  134. showSidebarBackdrop: false,
  135. currentFloatingItem: null,
  136. currentFloatingItemTarget: null,
  137. pathname: this.props.router.pathname
  138. };
  139. componentDidMount() {
  140. // prepare the flags to handle menu collapsed states
  141. this.buildCollapseList();
  142. // Listen for routes changes in order to hide the sidebar on mobile
  143. Router.events.on('routeChangeStart', this.handleRouteChange);
  144. Router.events.on('routeChangeComplete', this.handleRouteComplete);
  145. // Attach event listener to automatically close sidebar when click outside
  146. document.addEventListener('click', this.closeSidebarOnExternalClicks);
  147. }
  148. handleRouteComplete = (pathname) => {
  149. this.setState({
  150. pathname
  151. })
  152. }
  153. handleRouteChange = () => {
  154. this.closeFloatingNav();
  155. this.closeSidebar();
  156. };
  157. componentWillUnmount() {
  158. document.removeEventListener('click', this.closeSidebarOnExternalClicks);
  159. Router.events.off('routeChangeStart', this.handleRouteChange);
  160. Router.events.off('routeChangeComplete', this.handleRouteComplete);
  161. }
  162. closeSidebar = () => {
  163. this.props.actions.toggleSetting('asideToggled');
  164. };
  165. closeSidebarOnExternalClicks = e => {
  166. // don't check if sidebar not visible
  167. if (!this.props.settings.asideToggled) return;
  168. if (
  169. !parents(e.target, '.aside-container').length && // if not child of sidebar
  170. !parents(e.target, '.topnavbar-wrapper').length && // if not child of header
  171. !e.target.matches('#user-block-toggle') && // user block toggle anchor
  172. !e.target.parentElement.matches('#user-block-toggle') // user block toggle icon
  173. ) {
  174. this.closeSidebar();
  175. }
  176. };
  177. /** prepare initial state of collapse menus.*/
  178. buildCollapseList = () => {
  179. let collapse = {};
  180. Menu.filter(({ heading }) => !heading).forEach(({ name, path, submenu }) => {
  181. collapse[name] = this.routeActive(submenu ? submenu.map(({ path }) => path) : path);
  182. });
  183. this.setState({ collapse });
  184. };
  185. routeActive = paths => {
  186. const currpath = this.state.pathname;
  187. paths = Array.isArray(paths) ? paths : [paths];
  188. return paths.some(p => (p === '/' ? currpath === p : currpath.indexOf(p) > -1));
  189. };
  190. toggleItemCollapse = stateName => () => {
  191. for (let c in this.state.collapse) {
  192. if (this.state.collapse[c] === true && c !== stateName)
  193. this.setState({
  194. collapse: {
  195. [c]: false
  196. }
  197. });
  198. }
  199. this.setState({
  200. collapse: {
  201. [stateName]: !this.state.collapse[stateName]
  202. }
  203. });
  204. };
  205. getSubRoutes = item => item.submenu.map(({ path }) => path);
  206. /** map menu config to string to determine which element to render */
  207. itemType = item => {
  208. if (item.heading) return 'heading';
  209. if (!item.submenu) return 'menu';
  210. if (item.submenu) return 'submenu';
  211. };
  212. shouldUseFloatingNav = () => {
  213. return (
  214. this.props.settings.isCollapsed ||
  215. this.props.settings.isCollapsedText ||
  216. this.props.settings.asideHover
  217. );
  218. };
  219. showFloatingNav = item => e => {
  220. if (this.shouldUseFloatingNav())
  221. this.setState({
  222. currentFloatingItem: item,
  223. currentFloatingItemTarget: e.currentTarget,
  224. showSidebarBackdrop: true
  225. });
  226. };
  227. closeFloatingNav = () => {
  228. this.setState({
  229. currentFloatingItem: null,
  230. currentFloatingItemTarget: null,
  231. showSidebarBackdrop: false
  232. });
  233. };
  234. render() {
  235. return (
  236. <>
  237. <aside className="aside-container">
  238. {/* START Sidebar (left) */}
  239. <div className="aside-inner">
  240. <nav
  241. className={
  242. 'sidebar ' +
  243. (this.props.settings.asideScrollbar ? 'show-scrollbar' : '')
  244. }
  245. >
  246. {/* START sidebar nav */}
  247. <ul className="sidebar-nav">
  248. {/* START user info */}
  249. <li className="has-user-block">
  250. <SidebarUserBlock />
  251. </li>
  252. {/* END user info */}
  253. {/* Iterates over all sidebar items */}
  254. {Menu.map((item, i) => {
  255. // heading
  256. if (this.itemType(item) === 'heading')
  257. return <SidebarItemHeader item={item} key={i} />;
  258. else {
  259. if (this.itemType(item) === 'menu')
  260. return (
  261. <SidebarItem
  262. isActive={this.routeActive(item.path)}
  263. item={item}
  264. key={i}
  265. onMouseEnter={this.closeFloatingNav}
  266. />
  267. );
  268. if (this.itemType(item) === 'submenu')
  269. return [
  270. <SidebarSubItem
  271. item={item}
  272. isOpen={this.state.collapse[item.name]}
  273. handler={this.toggleItemCollapse(item.name)}
  274. isActive={this.routeActive(
  275. this.getSubRoutes(item)
  276. )}
  277. key={i}
  278. onMouseEnter={this.showFloatingNav(item)}
  279. >
  280. <SidebarSubHeader item={item} key={i} />
  281. {item.submenu.map((subitem, i) => (
  282. <SidebarItem
  283. key={i}
  284. item={subitem}
  285. isActive={this.routeActive(
  286. subitem.path
  287. )}
  288. />
  289. ))}
  290. </SidebarSubItem>
  291. ];
  292. }
  293. return null; // unrecognized item
  294. })}
  295. </ul>
  296. {/* END sidebar nav */}
  297. </nav>
  298. </div>
  299. {/* END Sidebar (left) */}
  300. {this.state.currentFloatingItem && this.state.currentFloatingItem.submenu && (
  301. <FloatingNav
  302. item={this.state.currentFloatingItem}
  303. target={this.state.currentFloatingItemTarget}
  304. routeActive={this.routeActive}
  305. isFixed={this.props.settings.isFixed}
  306. closeFloatingNav={this.closeFloatingNav}
  307. />
  308. )}
  309. </aside>
  310. {this.state.showSidebarBackdrop && (
  311. <SidebarBackdrop closeFloatingNav={this.closeFloatingNav} />
  312. )}
  313. </>
  314. );
  315. }
  316. }
  317. Sidebar.propTypes = {
  318. actions: PropTypes.object,
  319. settings: PropTypes.object
  320. };
  321. const mapStateToProps = state => ({ settings: state.settings });
  322. const mapDispatchToProps = dispatch => ({
  323. actions: bindActionCreators(actions, dispatch)
  324. });
  325. export default connect(
  326. mapStateToProps,
  327. mapDispatchToProps
  328. )(withRouter(withTranslation(Sidebar)));