packages/storybook/stories/example-table.stories.tsx (291 lines of code) (raw):

/* eslint-disable @typescript-eslint/no-unused-vars */ import { DebugFlags, Log, Point, Rect, ScrollAxis, StyleProps, SyntheticEvent, SyntheticPointerEvent } from '@canvas-ui/core' import { Canvas, Chunk, Flex, ScrollView, Text, useCanvasState, View } from '@canvas-ui/react' import { assert } from '@canvas-ui/assert' import React, { createContext, FC, useContext, useEffect, useMemo, useRef, useState } from 'react' import { Movie, MovieFields, MovieRepo } from './movie.repo' import { StoryObj } from '@storybook/react/*' type ScrollSync = { vertical: VerticalScrollSync horizontal: HorizontalScrollSync } const ScrollSyncContext = createContext<ScrollSync | null>(null) const useScrollSyncContext = () => { const context = useContext(ScrollSyncContext) if (!context) { throw '' } return context } abstract class AbstractScrollSync { private nodes: ScrollView[] = [] private handleScrollChange = (event: SyntheticEvent<ScrollView>) => { const source = event.target assert(source) const { scrollOffset } = source for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i] if (node !== source) { this.sync(node, scrollOffset) } } } protected abstract sync(target: ScrollView, scrollOffset: Point): void add(node: ScrollView, passive?: boolean) { if (!passive) { node.addEventListener('scroll', this.handleScrollChange) } this.nodes.push(node) } remove(node: ScrollView) { const pos = this.nodes.indexOf(node) assert(pos !== -1, '找不到节点') this.nodes.splice(pos, 1) node.removeEventListener('scroll', this.handleScrollChange) } } class VerticalScrollSync extends AbstractScrollSync { protected sync(target: ScrollView, scrollOffset: Point) { target._setScrollOffset(Point.fromXY(target.scrollLeft, scrollOffset.y)) } } class HorizontalScrollSync extends AbstractScrollSync { protected sync(target: ScrollView, scrollOffset: Point) { target._setScrollOffset(Point.fromXY(scrollOffset.x, target.scrollLeft)) } } export const TableTest: StoryObj<React.FC> = () => { useEffect(() => { // DebugFlags.set(DebugFlags.LayerBounds | DebugFlags.RasterCacheWaterMark) Log.disableAll = true return () => { DebugFlags.set(0) Log.disableAll = false } }, []) const [movies, setMovies] = useState<Movie[]>([]) const [movieRepo] = useState(() => new MovieRepo()) useEffect(() => { movieRepo.fetch(12000).then(setMovies) }, [movieRepo]) const scrollSyncValue = useMemo(() => { return { vertical: new VerticalScrollSync(), horizontal: new HorizontalScrollSync(), } }, []) const deleteFirstRow = () => { setMovies(prev => { const next = prev.slice() next.shift() return next }) } const deleteLastRow = () => { setMovies(prev => { const next = prev.slice() next.pop() return next }) } return ( <> <button onClick={deleteFirstRow}>Delete first row (slow)</button> <button onClick={deleteLastRow}>Delete last row</button> <div style={{ width: '100%', height: '100%' }}> <Canvas> <ScrollSyncContext.Provider value={scrollSyncValue}> <View> <View> <RightHead /> </View> <View style={{ top: 40 }}> <Left rows={movies} /> <Right rows={movies} /> </View> </View> </ScrollSyncContext.Provider> </Canvas> </div> </> ) } type LeftProps = { rows: Movie[] } const Left: FC<LeftProps> = ({ rows: data }) => { const { height } = useCanvasState() const scrollSync = useScrollSyncContext() const ref = React.useRef<ScrollView>(null) const setRef = (node: ScrollView) => { if (ref.current) { scrollSync.vertical.remove(ref.current) } ref.current = node if (ref.current) { scrollSync.vertical.add(ref.current) } } const content = data.map((it, index) => { return ( <LeftRow seq={index} key={index} row={it} /> ) }) return ( <ScrollView ref={setRef} scrollAxis={ScrollAxis.Vertical} style={{ width: 348, height: height - 40 }}> <Flex style={{ flexDirection: 'column' }}> {content} </Flex> </ScrollView> ) } type LeftRowProps = { seq: number row: Movie } const LeftRow: FC<LeftRowProps> = ({ seq, row, }) => { const style: StyleProps = { borderWidth: 1, borderColor: '#EFEFEF', backgroundColor: 'white', width: 348, height: 30, justifyContent: 'flex-start', marginTop: -1, } return ( <Flex style={style}> <Text style={{ width: 48 }}> {seq} </Text> <Text style={{ maxLines: 1, width: 100 }}> {row.电影ID} </Text> <Text style={{ maxLines: 1, width: 100 }}> {row.电影名} </Text> <Text style={{ maxLines: 1, width: 100 }}> {row.电影英文名} </Text> </Flex> ) } type RightProps = { rows: Movie[] } const Right: FC<RightProps> = ({ rows }) => { const { width, height } = useCanvasState() const scrollSync = useScrollSyncContext() const ref = React.useRef<ScrollView>(null) const setRef = (node: ScrollView) => { if (ref.current) { scrollSync.vertical.remove(ref.current) scrollSync.horizontal.remove(ref.current) } ref.current = node if (ref.current) { scrollSync.vertical.add(ref.current) scrollSync.horizontal.add(ref.current) } } const content = rows.map((it, index) => { return ( <RightRow key={index} seq={index} row={it} /> ) }) const isOffstage = (viewport: Rect, childBounds: Rect) => { return childBounds.bottom < viewport.top || childBounds.top > viewport.bottom } return ( <ScrollView ref={setRef} offset={Point.fromXY(348, 0)} style={{ width: width - 348, height: height - 40 }}> <Chunk isOffstage={isOffstage} style={{ width: (MovieFields.length - 4) * 150, height: -1 + rows.length * (30 - 1) }}> {content} </Chunk> </ScrollView> ) } type RightRowProps = { seq: number row: Movie } const RightRow: FC<RightRowProps> = ({ seq, row, }) => { const ref = useRef<Flex | null>(null) const [lastPointerEvent, setLastPointerEvent] = useState<SyntheticPointerEvent<Flex> | null>(null) const style = useMemo<StyleProps>(() => { return { borderWidth: 1, borderColor: '#EFEFEF', backgroundColor: lastPointerEvent?.type === 'pointerover' ? 'rgba(255,165,0,0.2)' : 'white', height: 30, justifyContent: 'flex-start', top: -1 + seq * (30 - 1), } }, [seq, lastPointerEvent]) const columns = MovieFields.slice(4).map(key => { return ( <Text key={key} style={{ width: 150, maxLines: 1 }}> {row[key] || ''} </Text> ) }) return ( <Flex ref={ref} style={style} onPointerOver={setLastPointerEvent} onPointerOut={setLastPointerEvent}> {columns} </Flex> ) } const RightHead: FC = () => { const { width } = useCanvasState() const scrollSync = useScrollSyncContext() const ref = React.useRef<ScrollView>(null) const setRef = (node: ScrollView) => { if (ref.current) { scrollSync.horizontal.remove(ref.current) } ref.current = node if (ref.current) { scrollSync.horizontal.add(ref.current) } } const cells = MovieFields.slice(4).map(it => { return ( <Text key={it} style={{ width: 150, maxLines: 1 }}> {it} </Text> ) }) const style: StyleProps = { borderWidth: 1, borderColor: '#EFEFEF', backgroundColor: 'white', height: 30, justifyContent: 'flex-start', } return ( <ScrollView ref={setRef} scrollAxis={ScrollAxis.Horizontal} offset={Point.fromXY(348, 0)} style={{ width: width - 348, height: 30 }}> <Flex repaintBoundary={true} style={style}> {cells} </Flex> </ScrollView> ) } TableTest.storyName = 'Table' export default { title: 'example/Table', component: TableTest, decorators: [(Story: React.ComponentType) => <div style={{ backgroundColor: '#efefef', width: '100%', height: '100vh' }}><Story /></div>], }