廓形仪rn版本-技术调研
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

339 lines
8.7 KiB

  1. import React, {
  2. useEffect,
  3. useState,
  4. createContext,
  5. useContext,
  6. useMemo,
  7. forwardRef,
  8. } from 'react';
  9. import type { VariantProps } from '@gluestack-ui/nativewind-utils';
  10. import { View, Dimensions, Platform, ViewProps } from 'react-native';
  11. import { gridStyle, gridItemStyle } from './styles';
  12. import { cssInterop } from 'nativewind';
  13. import {
  14. useBreakpointValue,
  15. getBreakPointValue,
  16. } from '@/components/ui/utils/use-break-point-value';
  17. const { width: DEVICE_WIDTH } = Dimensions.get('window');
  18. const GridContext = createContext<any>({});
  19. function arrangeChildrenIntoRows({
  20. childrenArray,
  21. colSpanArr,
  22. numColumns,
  23. }: {
  24. childrenArray: React.ReactNode[];
  25. colSpanArr: number[];
  26. numColumns: number;
  27. }) {
  28. let currentRow = 1;
  29. let currentRowTotalColSpan = 0;
  30. // store how many items in each row
  31. const rowItemsCount: {
  32. [key: number]: number[];
  33. } = {};
  34. for (let i = 0; i < childrenArray.length; i++) {
  35. const colSpan = colSpanArr[i];
  36. // if current row is full, go to next row
  37. if (currentRowTotalColSpan + colSpan > numColumns) {
  38. currentRow++;
  39. currentRowTotalColSpan = colSpan;
  40. } else {
  41. // if current row is not full, add colSpan to current row
  42. currentRowTotalColSpan += colSpan;
  43. }
  44. rowItemsCount[currentRow] = rowItemsCount[currentRow]
  45. ? [...rowItemsCount[currentRow], i]
  46. : [i];
  47. }
  48. return rowItemsCount;
  49. }
  50. function generateResponsiveNumColumns({ gridClass }: { gridClass: string }) {
  51. const gridClassNamePattern = /\b(?:\w+:)?grid-cols-?\d+\b/g;
  52. const numColumns = gridClass?.match(gridClassNamePattern);
  53. if (!numColumns) {
  54. return 12;
  55. }
  56. const regex = /^(?:(\w+):)?grid-cols-?(\d+)$/;
  57. const result: any = {};
  58. numColumns.forEach((classname) => {
  59. const match = classname.match(regex);
  60. if (match) {
  61. const prefix = match[1] || 'default';
  62. const value = parseInt(match[2], 10);
  63. result[prefix] = value;
  64. }
  65. });
  66. return result;
  67. }
  68. function generateResponsiveColSpans({
  69. gridItemClassName,
  70. }: {
  71. gridItemClassName: string;
  72. }) {
  73. const gridClassNamePattern = /\b(?:\w+:)?col-span-?\d+\b/g;
  74. const colSpan: any = gridItemClassName?.match(gridClassNamePattern);
  75. if (!colSpan) {
  76. return 1;
  77. }
  78. const regex = /^(?:(\w+):)?col-span-?(\d+)$/;
  79. const result: any = {};
  80. colSpan.forEach((classname: any) => {
  81. const match = classname.match(regex);
  82. if (match) {
  83. const prefix = match[1] || 'default';
  84. const value = parseInt(match[2], 10);
  85. result[prefix] = value;
  86. }
  87. });
  88. return result;
  89. }
  90. type IGridProps = ViewProps &
  91. VariantProps<typeof gridStyle> & {
  92. gap?: number;
  93. rowGap?: number;
  94. columnGap?: number;
  95. flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
  96. padding?: number;
  97. paddingLeft?: number;
  98. paddingRight?: number;
  99. paddingStart?: number;
  100. paddingEnd?: number;
  101. borderWidth?: number;
  102. borderLeftWidth?: number;
  103. borderRightWidth?: number;
  104. _extra: {
  105. className: string;
  106. };
  107. };
  108. const Grid = forwardRef<React.ComponentRef<typeof View>, IGridProps>(
  109. function Grid({ className, _extra, children, ...props }, ref) {
  110. const [calculatedWidth, setCalculatedWidth] = useState<number | null>(null);
  111. const gridClass = _extra?.className;
  112. const obj = generateResponsiveNumColumns({ gridClass });
  113. const responsiveNumColumns: any = useBreakpointValue(obj);
  114. const itemsPerRow = useMemo(() => {
  115. // get the colSpan of each child
  116. const colSpanArr = React.Children.map(children, (child: any) => {
  117. const gridItemClassName = child?.props?._extra?.className;
  118. const colSpan2 = getBreakPointValue(
  119. generateResponsiveColSpans({ gridItemClassName }),
  120. DEVICE_WIDTH
  121. );
  122. const colSpan = colSpan2 ? colSpan2 : 1;
  123. if (colSpan > responsiveNumColumns) {
  124. return responsiveNumColumns;
  125. }
  126. return colSpan;
  127. });
  128. const childrenArray = React.Children.toArray(children);
  129. const rowItemsCount = arrangeChildrenIntoRows({
  130. childrenArray,
  131. colSpanArr,
  132. numColumns: responsiveNumColumns,
  133. });
  134. return rowItemsCount;
  135. }, [responsiveNumColumns, children]);
  136. const childrenWithProps = React.Children.map(children, (child, index) => {
  137. if (React.isValidElement(child)) {
  138. return React.cloneElement(child, { key: index });
  139. }
  140. return child;
  141. });
  142. const gridClassMerged = `${Platform.select({
  143. web: gridClass ?? '',
  144. })}`;
  145. const contextValue = useMemo(() => {
  146. return {
  147. calculatedWidth,
  148. numColumns: responsiveNumColumns,
  149. itemsPerRow,
  150. flexDirection: props?.flexDirection || 'row',
  151. gap: props?.gap || 0,
  152. columnGap: props?.columnGap || 0,
  153. };
  154. }, [calculatedWidth, itemsPerRow, responsiveNumColumns, props]);
  155. const borderLeftWidth = props?.borderLeftWidth || props?.borderWidth || 0;
  156. const borderRightWidth = props?.borderRightWidth || props?.borderWidth || 0;
  157. const borderWidthToSubtract = borderLeftWidth + borderRightWidth;
  158. return (
  159. <GridContext.Provider value={contextValue}>
  160. <View
  161. ref={ref}
  162. className={gridStyle({
  163. class: className + ' ' + gridClassMerged,
  164. })}
  165. onLayout={(event) => {
  166. const paddingLeftToSubtract =
  167. props?.paddingStart || props?.paddingLeft || props?.padding || 0;
  168. const paddingRightToSubtract =
  169. props?.paddingEnd || props?.paddingRight || props?.padding || 0;
  170. const gridWidth =
  171. event.nativeEvent.layout.width -
  172. paddingLeftToSubtract -
  173. paddingRightToSubtract -
  174. borderWidthToSubtract;
  175. setCalculatedWidth(gridWidth);
  176. }}
  177. {...props}
  178. >
  179. {calculatedWidth && childrenWithProps}
  180. </View>
  181. </GridContext.Provider>
  182. );
  183. }
  184. );
  185. cssInterop(Grid, {
  186. className: {
  187. target: 'style',
  188. nativeStyleToProp: {
  189. gap: 'gap',
  190. rowGap: 'rowGap',
  191. columnGap: 'columnGap',
  192. flexDirection: 'flexDirection',
  193. padding: 'padding',
  194. paddingLeft: 'paddingLeft',
  195. paddingRight: 'paddingRight',
  196. paddingStart: 'paddingStart',
  197. paddingEnd: 'paddingEnd',
  198. borderWidth: 'borderWidth',
  199. borderLeftWidth: 'borderLeftWidth',
  200. borderRightWidth: 'borderRightWidth',
  201. },
  202. },
  203. });
  204. type IGridItemProps = ViewProps &
  205. VariantProps<typeof gridItemStyle> & {
  206. index?: number;
  207. _extra: {
  208. className: string;
  209. };
  210. };
  211. const GridItem = forwardRef<React.ComponentRef<typeof View>, IGridItemProps>(
  212. function GridItem({ className, _extra, ...props }, ref) {
  213. const [flexBasisValue, setFlexBasisValue] = useState<
  214. number | string | null
  215. >('auto');
  216. const {
  217. calculatedWidth,
  218. numColumns,
  219. itemsPerRow,
  220. flexDirection,
  221. gap,
  222. columnGap,
  223. } = useContext(GridContext);
  224. const gridItemClass = _extra?.className;
  225. const responsiveColSpan = (useBreakpointValue(
  226. generateResponsiveColSpans({ gridItemClassName: gridItemClass })
  227. ) ?? 1) as number;
  228. useEffect(() => {
  229. if (
  230. !flexDirection?.includes('column') &&
  231. calculatedWidth &&
  232. numColumns > 0 &&
  233. responsiveColSpan > 0
  234. ) {
  235. // find out in which row of itemsPerRow the current item's index is
  236. const row = Object.keys(itemsPerRow).find((key) => {
  237. return itemsPerRow[key].includes(props?.index);
  238. });
  239. const rowColsCount = itemsPerRow[row as string]?.length;
  240. const space = columnGap || gap || 0;
  241. const gutterOffset =
  242. space *
  243. (rowColsCount === 1 && responsiveColSpan < numColumns
  244. ? 2
  245. : rowColsCount - 1);
  246. const flexBasisVal =
  247. Math.min(
  248. (((calculatedWidth - gutterOffset) * responsiveColSpan) /
  249. numColumns /
  250. calculatedWidth) *
  251. 100,
  252. 100
  253. ) + '%';
  254. setFlexBasisValue(flexBasisVal);
  255. }
  256. // eslint-disable-next-line react-hooks/exhaustive-deps
  257. }, [
  258. calculatedWidth,
  259. responsiveColSpan,
  260. numColumns,
  261. columnGap,
  262. gap,
  263. flexDirection,
  264. ]);
  265. return (
  266. <View
  267. ref={ref}
  268. // @ts-expect-error : internal implementation for r-19/react-native-web
  269. gridItemClass={gridItemClass}
  270. className={gridItemStyle({
  271. class: className,
  272. })}
  273. {...props}
  274. style={[
  275. {
  276. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  277. flexBasis: flexBasisValue as any,
  278. },
  279. props.style,
  280. ]}
  281. />
  282. );
  283. }
  284. );
  285. Grid.displayName = 'Grid';
  286. GridItem.displayName = 'GridItem';
  287. export { Grid, GridItem };