index.tsx 11 KB

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