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
339 lines
8.7 KiB
import React, {
|
|
useEffect,
|
|
useState,
|
|
createContext,
|
|
useContext,
|
|
useMemo,
|
|
forwardRef,
|
|
} from 'react';
|
|
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
|
|
import { View, Dimensions, Platform, ViewProps } from 'react-native';
|
|
import { gridStyle, gridItemStyle } from './styles';
|
|
import { cssInterop } from 'nativewind';
|
|
import {
|
|
useBreakpointValue,
|
|
getBreakPointValue,
|
|
} from '@/components/ui/utils/use-break-point-value';
|
|
|
|
const { width: DEVICE_WIDTH } = Dimensions.get('window');
|
|
|
|
const GridContext = createContext<any>({});
|
|
|
|
function arrangeChildrenIntoRows({
|
|
childrenArray,
|
|
colSpanArr,
|
|
numColumns,
|
|
}: {
|
|
childrenArray: React.ReactNode[];
|
|
colSpanArr: number[];
|
|
numColumns: number;
|
|
}) {
|
|
let currentRow = 1;
|
|
let currentRowTotalColSpan = 0;
|
|
|
|
// store how many items in each row
|
|
const rowItemsCount: {
|
|
[key: number]: number[];
|
|
} = {};
|
|
|
|
for (let i = 0; i < childrenArray.length; i++) {
|
|
const colSpan = colSpanArr[i];
|
|
|
|
// if current row is full, go to next row
|
|
if (currentRowTotalColSpan + colSpan > numColumns) {
|
|
currentRow++;
|
|
currentRowTotalColSpan = colSpan;
|
|
} else {
|
|
// if current row is not full, add colSpan to current row
|
|
currentRowTotalColSpan += colSpan;
|
|
}
|
|
|
|
rowItemsCount[currentRow] = rowItemsCount[currentRow]
|
|
? [...rowItemsCount[currentRow], i]
|
|
: [i];
|
|
}
|
|
|
|
return rowItemsCount;
|
|
}
|
|
|
|
function generateResponsiveNumColumns({ gridClass }: { gridClass: string }) {
|
|
const gridClassNamePattern = /\b(?:\w+:)?grid-cols-?\d+\b/g;
|
|
const numColumns = gridClass?.match(gridClassNamePattern);
|
|
|
|
if (!numColumns) {
|
|
return 12;
|
|
}
|
|
|
|
const regex = /^(?:(\w+):)?grid-cols-?(\d+)$/;
|
|
const result: any = {};
|
|
|
|
numColumns.forEach((classname) => {
|
|
const match = classname.match(regex);
|
|
if (match) {
|
|
const prefix = match[1] || 'default';
|
|
const value = parseInt(match[2], 10);
|
|
result[prefix] = value;
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function generateResponsiveColSpans({
|
|
gridItemClassName,
|
|
}: {
|
|
gridItemClassName: string;
|
|
}) {
|
|
const gridClassNamePattern = /\b(?:\w+:)?col-span-?\d+\b/g;
|
|
|
|
const colSpan: any = gridItemClassName?.match(gridClassNamePattern);
|
|
|
|
if (!colSpan) {
|
|
return 1;
|
|
}
|
|
|
|
const regex = /^(?:(\w+):)?col-span-?(\d+)$/;
|
|
const result: any = {};
|
|
|
|
colSpan.forEach((classname: any) => {
|
|
const match = classname.match(regex);
|
|
if (match) {
|
|
const prefix = match[1] || 'default';
|
|
const value = parseInt(match[2], 10);
|
|
result[prefix] = value;
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
type IGridProps = ViewProps &
|
|
VariantProps<typeof gridStyle> & {
|
|
gap?: number;
|
|
rowGap?: number;
|
|
columnGap?: number;
|
|
flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
|
|
padding?: number;
|
|
paddingLeft?: number;
|
|
paddingRight?: number;
|
|
paddingStart?: number;
|
|
paddingEnd?: number;
|
|
borderWidth?: number;
|
|
borderLeftWidth?: number;
|
|
borderRightWidth?: number;
|
|
_extra: {
|
|
className: string;
|
|
};
|
|
};
|
|
|
|
const Grid = forwardRef<React.ComponentRef<typeof View>, IGridProps>(
|
|
function Grid({ className, _extra, children, ...props }, ref) {
|
|
const [calculatedWidth, setCalculatedWidth] = useState<number | null>(null);
|
|
|
|
const gridClass = _extra?.className;
|
|
const obj = generateResponsiveNumColumns({ gridClass });
|
|
const responsiveNumColumns: any = useBreakpointValue(obj);
|
|
|
|
const itemsPerRow = useMemo(() => {
|
|
// get the colSpan of each child
|
|
const colSpanArr = React.Children.map(children, (child: any) => {
|
|
const gridItemClassName = child?.props?._extra?.className;
|
|
|
|
const colSpan2 = getBreakPointValue(
|
|
generateResponsiveColSpans({ gridItemClassName }),
|
|
DEVICE_WIDTH
|
|
);
|
|
const colSpan = colSpan2 ? colSpan2 : 1;
|
|
|
|
if (colSpan > responsiveNumColumns) {
|
|
return responsiveNumColumns;
|
|
}
|
|
|
|
return colSpan;
|
|
});
|
|
|
|
const childrenArray = React.Children.toArray(children);
|
|
|
|
const rowItemsCount = arrangeChildrenIntoRows({
|
|
childrenArray,
|
|
colSpanArr,
|
|
numColumns: responsiveNumColumns,
|
|
});
|
|
|
|
return rowItemsCount;
|
|
}, [responsiveNumColumns, children]);
|
|
|
|
const childrenWithProps = React.Children.map(children, (child, index) => {
|
|
if (React.isValidElement(child)) {
|
|
return React.cloneElement(child, { key: index });
|
|
}
|
|
|
|
return child;
|
|
});
|
|
|
|
const gridClassMerged = `${Platform.select({
|
|
web: gridClass ?? '',
|
|
})}`;
|
|
|
|
const contextValue = useMemo(() => {
|
|
return {
|
|
calculatedWidth,
|
|
numColumns: responsiveNumColumns,
|
|
itemsPerRow,
|
|
flexDirection: props?.flexDirection || 'row',
|
|
gap: props?.gap || 0,
|
|
columnGap: props?.columnGap || 0,
|
|
};
|
|
}, [calculatedWidth, itemsPerRow, responsiveNumColumns, props]);
|
|
|
|
const borderLeftWidth = props?.borderLeftWidth || props?.borderWidth || 0;
|
|
const borderRightWidth = props?.borderRightWidth || props?.borderWidth || 0;
|
|
const borderWidthToSubtract = borderLeftWidth + borderRightWidth;
|
|
|
|
return (
|
|
<GridContext.Provider value={contextValue}>
|
|
<View
|
|
ref={ref}
|
|
className={gridStyle({
|
|
class: className + ' ' + gridClassMerged,
|
|
})}
|
|
onLayout={(event) => {
|
|
const paddingLeftToSubtract =
|
|
props?.paddingStart || props?.paddingLeft || props?.padding || 0;
|
|
|
|
const paddingRightToSubtract =
|
|
props?.paddingEnd || props?.paddingRight || props?.padding || 0;
|
|
|
|
const gridWidth =
|
|
event.nativeEvent.layout.width -
|
|
paddingLeftToSubtract -
|
|
paddingRightToSubtract -
|
|
borderWidthToSubtract;
|
|
|
|
setCalculatedWidth(gridWidth);
|
|
}}
|
|
{...props}
|
|
>
|
|
{calculatedWidth && childrenWithProps}
|
|
</View>
|
|
</GridContext.Provider>
|
|
);
|
|
}
|
|
);
|
|
|
|
cssInterop(Grid, {
|
|
className: {
|
|
target: 'style',
|
|
nativeStyleToProp: {
|
|
gap: 'gap',
|
|
rowGap: 'rowGap',
|
|
columnGap: 'columnGap',
|
|
flexDirection: 'flexDirection',
|
|
padding: 'padding',
|
|
paddingLeft: 'paddingLeft',
|
|
paddingRight: 'paddingRight',
|
|
paddingStart: 'paddingStart',
|
|
paddingEnd: 'paddingEnd',
|
|
borderWidth: 'borderWidth',
|
|
borderLeftWidth: 'borderLeftWidth',
|
|
borderRightWidth: 'borderRightWidth',
|
|
},
|
|
},
|
|
});
|
|
|
|
type IGridItemProps = ViewProps &
|
|
VariantProps<typeof gridItemStyle> & {
|
|
index?: number;
|
|
_extra: {
|
|
className: string;
|
|
};
|
|
};
|
|
|
|
const GridItem = forwardRef<React.ComponentRef<typeof View>, IGridItemProps>(
|
|
function GridItem({ className, _extra, ...props }, ref) {
|
|
const [flexBasisValue, setFlexBasisValue] = useState<
|
|
number | string | null
|
|
>('auto');
|
|
|
|
const {
|
|
calculatedWidth,
|
|
numColumns,
|
|
itemsPerRow,
|
|
flexDirection,
|
|
gap,
|
|
columnGap,
|
|
} = useContext(GridContext);
|
|
|
|
const gridItemClass = _extra?.className;
|
|
const responsiveColSpan = (useBreakpointValue(
|
|
generateResponsiveColSpans({ gridItemClassName: gridItemClass })
|
|
) ?? 1) as number;
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!flexDirection?.includes('column') &&
|
|
calculatedWidth &&
|
|
numColumns > 0 &&
|
|
responsiveColSpan > 0
|
|
) {
|
|
// find out in which row of itemsPerRow the current item's index is
|
|
const row = Object.keys(itemsPerRow).find((key) => {
|
|
return itemsPerRow[key].includes(props?.index);
|
|
});
|
|
|
|
const rowColsCount = itemsPerRow[row as string]?.length;
|
|
|
|
const space = columnGap || gap || 0;
|
|
|
|
const gutterOffset =
|
|
space *
|
|
(rowColsCount === 1 && responsiveColSpan < numColumns
|
|
? 2
|
|
: rowColsCount - 1);
|
|
|
|
const flexBasisVal =
|
|
Math.min(
|
|
(((calculatedWidth - gutterOffset) * responsiveColSpan) /
|
|
numColumns /
|
|
calculatedWidth) *
|
|
100,
|
|
100
|
|
) + '%';
|
|
|
|
setFlexBasisValue(flexBasisVal);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [
|
|
calculatedWidth,
|
|
responsiveColSpan,
|
|
numColumns,
|
|
columnGap,
|
|
gap,
|
|
flexDirection,
|
|
]);
|
|
|
|
return (
|
|
<View
|
|
ref={ref}
|
|
// @ts-expect-error : internal implementation for r-19/react-native-web
|
|
gridItemClass={gridItemClass}
|
|
className={gridItemStyle({
|
|
class: className,
|
|
})}
|
|
{...props}
|
|
style={[
|
|
{
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
flexBasis: flexBasisValue as any,
|
|
},
|
|
props.style,
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
Grid.displayName = 'Grid';
|
|
GridItem.displayName = 'GridItem';
|
|
|
|
export { Grid, GridItem };
|