Sidebar.js 10 KB

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