index.tsx 12 KB

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