UNPKG

@fto-consult/expo-ui

Version:

Bibliothèque de composants UI Expo,react-native

305 lines (294 loc) • 13.7 kB
// Copyright 2023 @fto-consult/Boris Fouomene. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. import {Virtuoso,VirtuosoGrid,TableVirtuoso} from "react-virtuoso/dist/index.mjs"; import React from "$react"; import PropTypes from "prop-types"; import {defaultObj,classNames,defaultNumber,isObj,isDOMElement,isNumber,uniqid,isNonNullString,defaultStr} from "$cutils"; import { View } from "react-native"; import {useList} from "../hooks"; import theme,{grid} from "$theme"; import Dimensions from "$cdimensions"; import { StyleSheet } from "react-native"; import {isMobileNative} from "$cplatform"; import {addClassName,removeClassName} from "$cutils/dom"; import { useGetNumColumns } from "../hooks"; const propTypes = { ...defaultObj(Virtuoso.propTypes), items : PropTypes.array.isRequired, alignToBottom : PropTypes.bool, /**Called with true / false when the list has reached the bottom / gets scrolled up. Can be used to load newer items, like tail -f. */ atBottomStateChange : PropTypes.func, /***alias : onEndReachedThreshold */ atBottomThreshold : PropTypes.number, atTopStateChange : PropTypes.func, atTopThreshold : PropTypes.number, defaultItemHeight : PropTypes.number, /***(index: number) => void; Gets called when the user scrolls to the end of the list. Receives the last item index as an argument. Can be used to implement endless scrolling. */ endReached : PropTypes.func, firstItemIndex : PropTypes.number, /**(isScrolling: boolean) => void */ isScrolling : PropTypes.func, }; /***@see : https://virtuoso.dev/virtuoso-api-reference/ */ const VirtuosoListComponent = React.forwardRef(({onRender,id,tableHeadId,fixedHeaderContent,numColumns:cNumCol,rowProps,renderTable,listClassName,components,itemProps,windowWidth,responsive,testID,renderItem,onEndReached,onLayout,onContentSizeChange,onScroll,isScrolling,estimatedItemSize,onEndReachedThreshold,containerProps,style,autoSizedStyle,...props},ref)=>{ if(renderTable){ responsive = false; } const {numColumns} = useGetNumColumns({responsive,numColumns:cNumCol,windowWidth}) const Component = React.useMemo(()=>renderTable ? TableVirtuoso : responsive?VirtuosoGrid:Virtuoso,[responsive,renderTable]); const context = useList(props); itemProps = defaultObj(itemProps); const items = context.items; renderTable ? rowProps = defaultObj(rowProps) : null; const r2 = {}; if(renderTable){ r2.fixedHeaderContent = fixedHeaderContent; } Object.map(Component.propTypes,(_,i)=>{ if(i in props){ r2[i] = props[i]; } }); containerProps = defaultObj(containerProps); const idRef = React.useRef(defaultStr(id,uniqid("virtuoso-list-id"))); id = idRef.current; const containerId = `${id}-container`; const headId = React.useRef(defaultStr(tableHeadId,`${id}-table-head`)).current; testID = defaultStr(testID,containerProps.testID,"RN_VirtuosoListComponent"); const listId = `${id}-list`; const listRef = React.useRef(null); const sizeRef = React.useRef({width:0,height:0}); const listSize = sizeRef.current; const isValid = ()=> listRef.current; const listStyle = {height:'100%',width:"100%",overflowX:renderTable?"auto":"hidden",maxWidth:"100%"}; if(renderTable){ listStyle.borderCollapse ="collapse"; } r2["data-test-id"] = testID+"_ListContent"; if(isObj(estimatedItemSize)){ if(isNumber(estimatedItemSize.width)){ listStyle.width = estimatedItemSize.width+"px"; } if(isNumber(estimatedItemSize.height)){ listStyle.height = estimatedItemSize.height+"px"; } } React.setRef(ref,{ scrollToEnd : ()=>{ return isValid() && listRef.current.scrollToIndex && listRef.current.scrollToIndex({index:"LAST"}); }, scrollToTop : ()=>{ return isValid() && listRef.current.scrollToIndex && listRef.current.scrollToIndex({index:0}); }, scrollToIndex : (opts)=>{ opts = defaultObj(opts); opts.index = defaultNumber(opts.index); return isValid() && listRef.current.scrollToIndex && listRef.current.scrollToIndex(opts); }, scrollToItem : ()=>null, scrollToOffset : (opts)=>{ opts = defaultObj(opts); opts.top = defaultNumber(opts.top,opts.offset); return isValid() && listRef.current.scrollTo && listRef.current.scrollTo(opts); }, scrollToLeft : ()=>{ return isValid() && listRef.current.scrollTo && listRef.current.scrollTo({left:0}); }, }); const checkSize = ()=>{ const element = document.getElementById(listId); if(!element) return; const target = element.firstChild?.firstChild; if(isDOMElement(target) && onContentSizeChange){ const {nativeEvent:{contentSize}} = normalizeEvent({target}); setTimeout(()=>{ target.style.paddingBottom = "50px"; if(contentSize.width !== listSize.width || contentSize.height != listSize.height){ sizeRef.current = contentSize; onContentSizeChange(contentSize.width,contentSize.height); } },100) } } const scrolledTopRef = React.useRef(0); const updateTableHeadCss = ()=>{ const newScrollTop = scrolledTopRef.current; const head = document.querySelector(`#${headId}`); if(!head || typeof newScrollTop !='number') return; const scrolled = newScrollTop > 50 head.style.background = !scrolled ? "transparent":theme.isDark()? theme.Colors.lighten(theme.surfaceBackgroundColor):theme.Colors.darken(theme.surfaceBackgroundColor); head.style.border = !scrolled ? "none" : `1px solid ${theme.colors.divider}` } React.useEffect(()=>{ const handleScroll = (e)=>{ if(!isDOMElement(e?.target)) return; const target = e?.target; const container = document.querySelector(`#${containerId}`); if(!container) return; if(container !== target && !container.contains(target)) return; if(!target.hasAttribute("data-virtuoso-scroller")) return; scrolledTopRef.current = typeof target?.scrollTop =="number"? target.scrollTop : undefined; updateTableHeadCss(); } window.addEventListener('scroll', handleScroll,true); return ()=>{ React.setRef(ref,null); window.removeEventListener('scroll', handleScroll,true); } },[]); React.useOnRender((...args)=>{ updateTableHeadCss(); if(onRender && onRender(...args)); },Math.max(Array.isArray(items) && items.length/10 || 0,500)); const listP = responsive ? { listClassName : classNames(listClassName,"rn-virtuoso-list",responsive && gridClassName) } : { atBottomThreshold : typeof onEndReachedThreshold =='number'? onEndReachedThreshold : undefined, totalListHeightChanged : (height)=>{ checkSize(); }, defaultItemHeight : typeof estimatedItemSize=='number' && estimatedItemSize || undefined, }; const getItemData = (index)=>{ if(typeof index =='number'){ return context.items[index] || null; } return null; } components = defaultObj(components); return <View {...containerProps} {...props} id={containerId} className={classNames("virtuoso-list-container",renderTable&& "virtuoso-list-container-render-table")} style={[{flex:1},containerProps.style,style,autoSizedStyle,{minWidth:'100%',height:'100%',maxWidth:'100%'}]} onLayout={onLayout} testID={testID}> <Component {...r2} {...listP} style = {listStyle} ref = {listRef} data = {items} id = {listId} useWindowScroll = {false} totalCount = {items.length} itemContent = {(index)=>{ return renderItem({index,numColumns,item:items[index],items}) }} atBottomStateChange = {()=>{ if(typeof onEndReached =='function'){ onEndReached(); } }} onScroll={(e) => onScroll && onScroll(normalizeEvent(e))} isScrolling = {(isC,)=>{ if(typeof isScrolling =='function'){ return isScrolling(isC); } if(!renderTable) return; }} components = {{ Item : renderTable ? undefined : responsive ? function(props){return <ItemContainer {...props} getItemData={getItemData} style={[itemProps.style,props.style]} numColumns={numColumns}/>} : undefined, ...(renderTable ? { TableRow: TableRowComponent, }:{}), ...components, ...(renderTable ? { TableHead : React.forwardRef((props,ref)=>{ const restProps = {id:headId,className:classNames(props.className,"virtuoso-list-render-table-thead")}; const Thead = React.useMemo(()=>React.isComponent(components.TableHead)? components.TableHead : "thead",[]); return <Thead ref={ref} {...props} {...restProps}/> }) } : {}) }} /> </View> }); VirtuosoListComponent.propTypes = { ...propTypes, tableHeadId : PropTypes.string,//l'id du header de la table virtuoso fixedHeaderContent : PropTypes.func,//la fonction rendant le contenu fixe du tableau renderTable : PropTypes.bool,//si le composant Table sera rendu numColumns : PropTypes.number, rowProps : PropTypes.object,//les props du TableRow, lorsque le rendu est de type table items : PropTypes.oneOfType([ PropTypes.object, PropTypes.array, PropTypes.func, ]) }; VirtuosoListComponent.displayName = "VirtuosoListComponent"; export default VirtuosoListComponent; const normalizeEvent = (e)=>{ return { nativeEvent: { contentOffset: { get x() { return e.target.scrollLeft; }, get y() { return e.target.scrollTop; } }, contentSize: { get height() { return e.target.scrollHeight; }, get width() { return e.target.scrollWidth; } }, layoutMeasurement: { get height() { return e.target.offsetHeight; }, get width() { return e.target.offsetWidth; } } }, timeStamp: Date.now() }; } function ItemContainer({numColumns,getItemData,responsive,windowWidth,...props}){ const dataIntex = "index" in props ? props.index : "data-index" in props ? props["data-index"] : "" const item = getItemData(dataIntex); const width = React.useMemo(()=>{ if(item?.isSectionListHeader) return "100%"; if(!numColumns || numColumns <= 1) return undefined; if(typeof windowWidth =='number' && windowWidth <=600) return "100%"; if(Dimensions.isMobileMedia()){ return "100%"; } return (100/numColumns)+"%"; },[windowWidth,numColumns,item?.isSectionListHeader]); const style = width && {width} || grid.col(windowWidth); const dataItemIndex = "data-item-index" in props ? props["data-item-index"] : ""; if(isObj(style)){ style.paddingRight = style.paddingLeft = style.paddingHorizontal = undefined; } return <View testID={`RN_VirtosoGridItem_${dataIntex}-${dataItemIndex}`} {...props} style = {[style,props.style]} /> } const ResponsiveVirtuosoListItemContainer = React.forwardRef((props,ref)=>{ return <View ref={ref} testID={"RN_ResponsiveVirtuosoListItemContainer"} {...props} style={[responsiveListStyle,props.style,]}/> }); export const gridClassName = "rn-virtuoso-responsive-list"; const responsiveListStyle = [theme.styles.row,theme.styles.row,theme.styles.flexWrap,theme.styles.justifyContentStart,theme.styles.alignItemsStart] ResponsiveVirtuosoListItemContainer.displayName = "ResponsiveVirtuosoListItemContainer"; if(typeof document !=='undefined' && document && document?.createElement){ const gridDomId = "dynamic-virtuoso-grid-styles"; let style = document.getElementById(gridDomId); if(!style){ style = document.createElement("style"); } style.id = gridDomId; style.textContent = ` .${gridClassName}{display:flex;flex-direction:row;align-items:flex-start;flex-wrap:wrap;justify-content:flex-start;}; `; document.body.appendChild(style); } export const TableRowComponent = ({testID,style,children,isSectionListHeader,rowData,...props}) => { const index = props['data-index']; const isOdd = typeof index =='number' ? index%2 > 0 : false; testID = defaultStr(testID,"_VirtuosoTableRow_"+index); return <tr data-test-id={testID} {...props} children={children} style={StyleSheet.flatten(style)} className={classNames(props.className,"virtuoso-table-row",`table-row-${isOdd?"odd":"even"}`)}/> };