index.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. import {
  2. useImperativeHandle,
  3. useLayoutEffect,
  4. forwardRef,
  5. useEffect,
  6. useState,
  7. Fragment,
  8. useRef
  9. } from "react";
  10. import {
  11. Platform,
  12. Animated,
  13. Easing,
  14. View
  15. } from "react-native";
  16. import {
  17. type EmbeddedMenuButton as EmbeddedMenuButtonType
  18. } from "./components/menuButton/type";
  19. import {
  20. type IEmbeddedMenuRef
  21. } from "./type";
  22. import type IEmbeddedMenuProps from "./type";
  23. import stylesheet, {
  24. useStyles
  25. } from "./stylesheet";
  26. import MenuButton from "./components/menuButton";
  27. import {
  28. NCoreUIKitEmbeddedMenu,
  29. NCoreUIKitTheme
  30. } from "../../core/hooks";
  31. import type {
  32. RefForwardingComponent
  33. } from "../../types";
  34. import {
  35. PanelLeftClose,
  36. PanelLeftOpen
  37. } from "lucide-react-native";
  38. import {
  39. SafeAreaView
  40. } from "react-native-safe-area-context";
  41. import Button from "../button";
  42. import Seperator from "../seperator";
  43. import SiteLogo from "../siteLogo";
  44. const Menu: RefForwardingComponent<IEmbeddedMenuRef, IEmbeddedMenuProps> = ({
  45. isWorkWithSafeAreaView = true,
  46. isWorkWithAnimation = true,
  47. renderHeader: RenderHeader,
  48. renderFooter: RenderFooter,
  49. close: closeAction,
  50. maxWidth = 300,
  51. minWidth = 75,
  52. siteLogoProps,
  53. customTheme,
  54. navigation,
  55. isCollapse,
  56. onClosed,
  57. onOpened,
  58. buttons,
  59. onClose,
  60. onOpen,
  61. style,
  62. id,
  63. ...props
  64. }, ref) => {
  65. const {
  66. colors,
  67. spaces
  68. } = NCoreUIKitTheme.useContext(customTheme);
  69. const [
  70. isActive,
  71. setIsActive
  72. ] = useState(false);
  73. const {
  74. headerContentContainer: headerContentContainerDynamicStyle,
  75. headerContainer: headerContainerDynamicStyle,
  76. collapseButton: collapseButtonDynamicStyle,
  77. seperator: seperatorDynamicStyle,
  78. container: containerDynamicStyle,
  79. siteLogo: siteLogoDynamicStyle
  80. } = useStyles({
  81. isCollapse: isActive,
  82. colors,
  83. spaces
  84. });
  85. const animatedX = useRef(new Animated.Value(!isCollapse ? minWidth : maxWidth)).current;
  86. useImperativeHandle(
  87. ref,
  88. () => ({
  89. close,
  90. open
  91. }),
  92. []
  93. );
  94. useEffect(() => {
  95. if(isCollapse !== undefined) setIsActive(isCollapse);
  96. }, [isCollapse]);
  97. useLayoutEffect(() => {
  98. if(isActive) {
  99. if(isWorkWithAnimation) {
  100. openAnimation();
  101. } else {
  102. if(onOpen) onOpen({
  103. id
  104. });
  105. animatedX.setValue(maxWidth);
  106. if(onOpened) onOpened({
  107. id
  108. });
  109. }
  110. } else {
  111. if(isWorkWithAnimation) {
  112. closeAnimation();
  113. } else {
  114. if(onClose) onClose({
  115. id
  116. });
  117. animatedX.setValue(minWidth);
  118. if(onClosed) onClosed({
  119. id
  120. });
  121. }
  122. }
  123. }, [
  124. isActive
  125. ]);
  126. const open = () => {
  127. setIsActive(true);
  128. };
  129. const close = () => {
  130. setIsActive(false);
  131. };
  132. const openAnimation = () => {
  133. if(onOpen) onOpen({
  134. id
  135. });
  136. Animated.timing(animatedX, {
  137. useNativeDriver: Platform.OS !== "web",
  138. easing: Easing.linear,
  139. toValue: maxWidth,
  140. duration: 300
  141. }).start(({
  142. finished
  143. }) => {
  144. if(finished) {
  145. if(onOpened) onOpened({
  146. id
  147. });
  148. }
  149. });
  150. };
  151. const closeAnimation = () => {
  152. if(onClose) onClose({
  153. id
  154. });
  155. Animated.timing(animatedX, {
  156. useNativeDriver: Platform.OS !== "web",
  157. easing: Easing.linear,
  158. toValue: minWidth,
  159. duration: 300
  160. }).start(({
  161. finished
  162. }) => {
  163. if(finished) {
  164. if(onClosed) onClosed({
  165. id
  166. });
  167. }
  168. });
  169. };
  170. const updateMenuButtonCollabs = ({
  171. depthMap,
  172. status
  173. }: {
  174. depthMap: Array<number>;
  175. status: boolean;
  176. }) => {
  177. const state = NCoreUIKitEmbeddedMenu.get({
  178. id
  179. });
  180. if(!state) {
  181. return;
  182. }
  183. const recursivelyCatch = (item: Array<EmbeddedMenuButtonType>, index = 0): Array<EmbeddedMenuButtonType> => {
  184. const tIndex = depthMap[index];
  185. return item.map((c_item, c_index) => {
  186. if(c_index !== tIndex) {
  187. return c_item;
  188. }
  189. if(index === depthMap.length - 1) {
  190. return {
  191. ...c_item,
  192. isCollapse: status
  193. };
  194. }
  195. return {
  196. ...c_item,
  197. subButtons: recursivelyCatch(c_item.subButtons as Array<EmbeddedMenuButtonType>, index + 1)
  198. };
  199. });
  200. };
  201. const newButtons = recursivelyCatch(state.buttons);
  202. state.buttons = newButtons;
  203. NCoreUIKitEmbeddedMenu.update({
  204. menuData: state,
  205. id: id
  206. });
  207. };
  208. const renderToggleCollapseForCollabsed = () => {
  209. if(isActive) {
  210. return null;
  211. }
  212. return <Button
  213. isCustomPadding={true}
  214. spreadBehaviour="free"
  215. type="neutral"
  216. size="medium"
  217. icon={({
  218. color,
  219. size
  220. }) => {
  221. return <PanelLeftOpen
  222. color={colors.content.icon[color]}
  223. size={size * 2}
  224. />;
  225. }}
  226. style={{
  227. ...collapseButtonDynamicStyle
  228. }}
  229. onPress={() => {
  230. setIsActive(!isActive);
  231. }}
  232. />;
  233. };
  234. const renderToggleCollapseForExpended = () => {
  235. if(!isActive) {
  236. return null;
  237. }
  238. return <Button
  239. isCustomPadding={true}
  240. spreadBehaviour="free"
  241. type="neutral"
  242. size="medium"
  243. icon={({
  244. color,
  245. size
  246. }) => {
  247. return <PanelLeftClose
  248. color={colors.content.icon[color]}
  249. size={size * 2}
  250. />;
  251. }}
  252. style={{
  253. ...collapseButtonDynamicStyle
  254. }}
  255. onPress={() => {
  256. setIsActive(!isActive);
  257. }}
  258. />;
  259. };
  260. const renderHeader = () => {
  261. if(RenderHeader) {
  262. return <RenderHeader
  263. isCollapse={isActive}
  264. maxWidth={maxWidth}
  265. minWidth={minWidth}
  266. />;
  267. }
  268. return <View
  269. style={[
  270. stylesheet.headerContainer,
  271. headerContainerDynamicStyle
  272. ]}
  273. >
  274. {renderToggleCollapseForCollabsed()}
  275. <View
  276. style={[
  277. stylesheet.headerContentContainer,
  278. headerContentContainerDynamicStyle
  279. ]}
  280. >
  281. <SiteLogo
  282. {...siteLogoProps}
  283. title={isActive ? siteLogoProps?.title : undefined}
  284. size={isActive ? siteLogoProps?.size : "small"}
  285. style={[
  286. siteLogoProps?.style,
  287. siteLogoDynamicStyle
  288. ]}
  289. />
  290. {renderToggleCollapseForExpended()}
  291. </View>
  292. <Seperator
  293. style={[
  294. stylesheet.seperator,
  295. seperatorDynamicStyle
  296. ]}
  297. />
  298. </View>;
  299. };
  300. const renderButtons = () => {
  301. if(!buttons || !buttons.length) {
  302. return null;
  303. }
  304. return buttons.map((buttonItem, buttonIndex) => {
  305. return <MenuButton
  306. key={`embedded-menu-button-${id}-${buttonIndex}`}
  307. updateMenuButtonCollabs={updateMenuButtonCollabs}
  308. closeAction={closeAction}
  309. buttonIndex={buttonIndex}
  310. isMenuCollabs={isActive}
  311. depthMap={[buttonIndex]}
  312. buttonItem={buttonItem}
  313. navigation={navigation}
  314. id={id}
  315. />;
  316. });
  317. };
  318. const renderFooter = () => {
  319. if(!RenderFooter) {
  320. return null;
  321. }
  322. return <RenderFooter
  323. isCollapse={isActive}
  324. maxWidth={maxWidth}
  325. minWidth={minWidth}
  326. />;
  327. };
  328. const renderSafeAreaContext = () => {
  329. if(!isWorkWithSafeAreaView) {
  330. return <Fragment>
  331. {renderHeader()}
  332. <View
  333. style={[
  334. stylesheet.content
  335. ]}
  336. >
  337. {renderButtons()}
  338. </View>
  339. {renderFooter()}
  340. </Fragment>;
  341. }
  342. return <SafeAreaView
  343. style={[
  344. stylesheet.safeAreaView
  345. ]}
  346. >
  347. {renderHeader()}
  348. <View
  349. style={[
  350. stylesheet.content
  351. ]}
  352. >
  353. {renderButtons()}
  354. </View>
  355. {renderFooter()}
  356. </SafeAreaView>;
  357. };
  358. const renderContent = () => {
  359. return <Animated.View
  360. {...props}
  361. style={[
  362. style,
  363. stylesheet.container,
  364. containerDynamicStyle,
  365. {
  366. width: animatedX
  367. }
  368. ]}
  369. >
  370. {renderSafeAreaContext()}
  371. </Animated.View>;
  372. };
  373. return renderContent();
  374. };
  375. export default forwardRef(Menu);