index.tsx 11 KB

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