Sidebar.js 9.8 KB

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