|
| 1 | +import { RefreshControl, StyleSheet, View } from 'react-native'; |
| 2 | +import { ScrollView } from 'react-native-gesture-handler'; |
| 3 | +import { Cell, Table, TableWrapper } from 'react-native-reanimated-table'; |
| 4 | +import colors from '../constants/colors'; |
| 5 | +import { useOrientation } from '../hooks/useOrientation'; |
| 6 | +import useSyncedScrollViews from '../hooks/useSyncedScrollViews'; |
| 7 | + |
| 8 | +export type CellType = Omit< |
| 9 | + React.ComponentProps<typeof Cell>, |
| 10 | + 'width' | 'height' | 'flex' |
| 11 | +>; |
| 12 | + |
| 13 | +export type DynamicCellType = CellType | ((opts: CellOpts) => CellType); |
| 14 | + |
| 15 | +export interface CellOpts { |
| 16 | + transposed: boolean; |
| 17 | +} |
| 18 | + |
| 19 | +export default function DoubleStickyTable({ |
| 20 | + table, |
| 21 | + heightArr, |
| 22 | + transposedHeightArr, |
| 23 | + widthArr, |
| 24 | + transposedWidthArr, |
| 25 | + ...rest |
| 26 | +}: Omit<React.ComponentProps<typeof DoubleStickyTableBase>, 'table'> & { |
| 27 | + table: DynamicCellType[][]; |
| 28 | + transposedHeightArr: number[]; |
| 29 | + transposedWidthArr: number[]; |
| 30 | +}) { |
| 31 | + const totalWidth = widthArr.reduce((a, b) => a + b, 0); |
| 32 | + const totalHeight = heightArr.reduce((a, b) => a + b, 0); |
| 33 | + |
| 34 | + const { isPortrait } = useOrientation(); |
| 35 | + |
| 36 | + if ( |
| 37 | + (totalWidth > totalHeight && isPortrait) || |
| 38 | + (totalWidth < totalHeight && !isPortrait) |
| 39 | + ) { |
| 40 | + return ( |
| 41 | + <DoubleStickyTableBase |
| 42 | + table={mapCells(transpose(table), (cell) => { |
| 43 | + if (typeof cell === 'function') return cell({ transposed: true }); |
| 44 | + return cell; |
| 45 | + })} |
| 46 | + heightArr={transposedHeightArr} |
| 47 | + widthArr={transposedWidthArr} |
| 48 | + {...rest} |
| 49 | + /> |
| 50 | + ); |
| 51 | + } |
| 52 | + |
| 53 | + return ( |
| 54 | + <DoubleStickyTableBase |
| 55 | + table={mapCells(table, (cell) => { |
| 56 | + if (typeof cell === 'function') return cell({ transposed: false }); |
| 57 | + return cell; |
| 58 | + })} |
| 59 | + heightArr={heightArr} |
| 60 | + widthArr={widthArr} |
| 61 | + {...rest} |
| 62 | + /> |
| 63 | + ); |
| 64 | +} |
| 65 | + |
| 66 | +function DoubleStickyTableBase({ |
| 67 | + table, |
| 68 | + heightArr, |
| 69 | + widthArr, |
| 70 | + refreshing, |
| 71 | + onRefresh, |
| 72 | +}: { |
| 73 | + table: CellType[][]; |
| 74 | + heightArr: number[]; |
| 75 | + widthArr: number[]; |
| 76 | + refreshing: React.ComponentProps<typeof RefreshControl>['refreshing']; |
| 77 | + onRefresh: React.ComponentProps<typeof RefreshControl>['onRefresh']; |
| 78 | +}) { |
| 79 | + const { topRowHeader, lowerRowHeaders, columnHeaders, columnContent } = |
| 80 | + sliceTable(table); |
| 81 | + |
| 82 | + const [headerHeight, ...contentHeight] = heightArr; |
| 83 | + const [headerWidth, ...contentWidth] = widthArr; |
| 84 | + |
| 85 | + const { LeaderScrollView, FollowerScrollView } = useSyncedScrollViews({ |
| 86 | + horizontal: true, |
| 87 | + }); |
| 88 | + |
| 89 | + return ( |
| 90 | + <ScrollView |
| 91 | + stickyHeaderIndices={[0]} |
| 92 | + style={styles.table} |
| 93 | + refreshControl={ |
| 94 | + <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> |
| 95 | + } |
| 96 | + > |
| 97 | + <View style={styles.rowSeparator}> |
| 98 | + <View style={{ flexDirection: 'row' }}> |
| 99 | + <Table style={styles.columnSeparator}> |
| 100 | + <Cell width={headerWidth} height={headerHeight} {...topRowHeader} /> |
| 101 | + </Table> |
| 102 | + <FollowerScrollView> |
| 103 | + <Table> |
| 104 | + <TableWrapper style={{ flexDirection: 'row' }}> |
| 105 | + {columnHeaders.map((columnHeader, index) => ( |
| 106 | + <Cell |
| 107 | + key={index} |
| 108 | + {...columnHeader} |
| 109 | + width={contentWidth[index]} |
| 110 | + height={headerHeight} |
| 111 | + /> |
| 112 | + ))} |
| 113 | + </TableWrapper> |
| 114 | + </Table> |
| 115 | + </FollowerScrollView> |
| 116 | + </View> |
| 117 | + </View> |
| 118 | + <View style={{ flexDirection: 'row' }}> |
| 119 | + <Table style={styles.columnSeparator}> |
| 120 | + {lowerRowHeaders.map((rowHeader, index) => ( |
| 121 | + <TableWrapper key={index}> |
| 122 | + <Cell |
| 123 | + width={headerWidth} |
| 124 | + height={contentHeight[index]} |
| 125 | + {...rowHeader} |
| 126 | + /> |
| 127 | + </TableWrapper> |
| 128 | + ))} |
| 129 | + </Table> |
| 130 | + <LeaderScrollView style={styles.rowSeparator}> |
| 131 | + <Table> |
| 132 | + {columnContent.map((contentRow, rowIndex) => ( |
| 133 | + <TableWrapper key={rowIndex} style={{ flexDirection: 'row' }}> |
| 134 | + {contentRow.map((contentCell, columnIndex) => ( |
| 135 | + <Cell |
| 136 | + key={columnIndex} |
| 137 | + {...contentCell} |
| 138 | + width={contentWidth[columnIndex]} |
| 139 | + height={contentHeight[rowIndex]} |
| 140 | + /> |
| 141 | + ))} |
| 142 | + </TableWrapper> |
| 143 | + ))} |
| 144 | + </Table> |
| 145 | + </LeaderScrollView> |
| 146 | + </View> |
| 147 | + </ScrollView> |
| 148 | + ); |
| 149 | +} |
| 150 | + |
| 151 | +function extractTableRowHeaders<T>(table: T[][]): { |
| 152 | + headers: T[]; |
| 153 | + content: T[][]; |
| 154 | +} { |
| 155 | + return { |
| 156 | + headers: table.map(([rowHeader]) => rowHeader), |
| 157 | + content: table.map(([, ...content]) => content), |
| 158 | + }; |
| 159 | +} |
| 160 | + |
| 161 | +function sliceTable<T>(table: T[][]) { |
| 162 | + const { headers: rowHeaders, content: rowContent } = |
| 163 | + extractTableRowHeaders(table); |
| 164 | + const [topRowHeader, ...lowerRowHeaders] = rowHeaders; |
| 165 | + const [columnHeaders, ...columnContent] = rowContent; |
| 166 | + |
| 167 | + return { |
| 168 | + topRowHeader, |
| 169 | + lowerRowHeaders, |
| 170 | + columnHeaders, |
| 171 | + columnContent, |
| 172 | + }; |
| 173 | +} |
| 174 | + |
| 175 | +function mapCells<A, B>( |
| 176 | + table: A[][], |
| 177 | + mapper: (cell: A, index: number) => B |
| 178 | +): B[][] { |
| 179 | + return table.map((row) => row.map((cell, index) => mapper(cell, index))); |
| 180 | +} |
| 181 | + |
| 182 | +function transpose<T>(data: T[][]): T[][] { |
| 183 | + return data[0].map((_, i) => data.map((row) => row[i])); |
| 184 | +} |
| 185 | + |
| 186 | +const styles = StyleSheet.create({ |
| 187 | + table: { |
| 188 | + backgroundColor: colors.white, |
| 189 | + }, |
| 190 | + columnSeparator: { |
| 191 | + borderColor: colors.primary, |
| 192 | + borderRightWidth: StyleSheet.hairlineWidth, |
| 193 | + }, |
| 194 | + rowSeparator: { |
| 195 | + backgroundColor: colors.white, |
| 196 | + borderColor: colors.primary, |
| 197 | + borderTopWidth: StyleSheet.hairlineWidth, |
| 198 | + }, |
| 199 | +}); |
0 commit comments