import { createStoreContext, ensure, react, syncFromProp, syncToProp, useStore } from '@o/use-store' import { Contents, gloss } from 'gloss' import React, { cloneElement, HTMLAttributes, isValidElement, memo, useCallback, useEffect, } from 'react' import { Responsive, WidthProvider } from 'react-grid-layout' import { isBrowser } from '../constants' import { isRightClick } from '../helpers/isRightClick' import { useDefaultProps } from '../hooks/useDefaultProps' import { useParentNodeSize } from '../hooks/useParentNodeSize' import { SurfaceProps } from '../Surface' import { Stack, StackProps } from '../View/Stack' const ResponsiveGridLayout = WidthProvider(Responsive) if (isBrowser) { // original require('../../react-grid-layout.css') // // our overrides require('../../react-grid-layout-styles.css') } type GridItemDimensions = { w?: number | 'auto' h?: number | 'auto' } type LayoutItem = { x: number y: number w: number h: number i: string } type GridLayout = { xxs?: LayoutItem[] xs?: LayoutItem[] sm?: LayoutItem[] md?: LayoutItem[] lg?: LayoutItem[] } type GridColumns = { xxs?: number xs?: number sm?: number md?: number lg?: number } type BaseGridLayoutProps = { /** Give an id to have the grid persist its state */ onChangeLayout?: (nextLayout: A) => any /** Pass in a layout you want to */ layout?: A /** Configure how many columns show per width */ columns?: GridColumns } type GridLayoutPropsItems = BaseGridLayoutProps & { items: any[] renderItem: (a: any, index: number) => React.ReactNode } type GridLayoutPropsChildren = BaseGridLayoutProps & { children?: React.ReactNode height?: number } export type GridLayoutProps = GridLayoutPropsChildren | GridLayoutPropsItems const defaultProps: Partial = { columns: { xxs: 1, xs: 2, sm: 3, md: 5, lg: 6, }, } type GridItems = { [key: string]: GridItemProps } class GridStore { props: GridLayoutProps items: GridItems = {} breakpoints = { lg: 1400, md: 1000, sm: 800, xs: 500, xxs: 0 } width = window.innerWidth enablePersist = false setItems = (items: GridItems) => { for (const key in items) { this.items[key] = items[key] } } getCol(width: number) { return Object.keys(this.breakpoints).reduce((a, key) => { const val = this.breakpoints[key] if (width > val && val > this.breakpoints[a]) { return key } return a }, 'xxs') } // @ts-ignore layout = syncFromProp(this.props, { key: 'layout', defaultKey: 'defaultLayout', defaultValue: {}, }) onChangeLayoutCb = syncToProp(this, 'layout', 'onChangeLayout') setLayout = (layout: any[], width?: number) => { if (!this.enablePersist) return const col = this.getCol(width) dualCompact(layout) this.layout = { // @ts-ignore ...this.layout, [col]: layout, } } mounted = false updateLayout = react( () => [this.items, this.props.columns], async ([items, columns], { sleep }) => { ensure('items', !!Object.keys(items).length) // react-grid had some bug if we listen to setLayout after sending // it sends the wrong dimensions back to us, so we use this to ignore until after this.enablePersist = false await sleep(50) // always re-calc from large and reset const layout = this.layout || {} this.layout = { lg: calculateLayout(items, columns, layout, 'lg'), md: calculateLayout(items, columns, layout, 'md'), sm: calculateLayout(items, columns, layout, 'sm'), xs: calculateLayout(items, columns, layout, 'xs'), xxs: calculateLayout(items, columns, layout, 'xxs'), } // bugfix react-grid-layout see https://github.com/STRML/react-grid-layout/issues/933 window.dispatchEvent(new Event('resize')) await sleep(20) this.mounted = true await sleep(200) this.enablePersist = true }, ) mountItem(props: GridItemProps) { this.items = { ...this.items, [props.id]: props } } unmountItem(props: GridItemProps) { delete this.items[props.id] this.items = { ...this.items } } } // this is the problem function, type is off const { useStore: useGridStore, ProvideStore } = createStoreContext(GridStore) export const GridLayout = memo(function GridLayout(directProps: GridLayoutProps) { const props = useDefaultProps(defaultProps, directProps) // TODO not great pattern here... maybe remove first option if ('items' in props) { return } return }) export const GridLayoutChildren = memo((props: GridLayoutPropsChildren) => { const { width, ref } = useParentNodeSize() const gridStore = useStore(GridStore, props) const childArr = Array.isArray(props.children) ? props.children : [props.children] const items = childArr .map((child, index) => { if (!isValidElement(child)) { throw new Error(`Invalid child: ${child}, must be a React Element`) } if (!child.key) { console.error( `Child without a key given to at index ${index}, must set a key.`, ) return null } // you can pass in a ... const isGridItem = child.type instanceof GridItem if (isGridItem) { return
{cloneElement(child, { id: `${child.key}` } as any)}
} // ... or a regular item like return ( {child} ) }) .filter(Boolean) let children = null if (!gridStore.layout) { children = {items} } else { children = ( gridStore.setLayout(next, width)} layouts={gridStore.layout} breakpoints={gridStore.breakpoints} width={width} height={props.height} compactType="vertical" measureBeforeMount={false} draggableHandle=".grid-draggable" cols={props.columns} > {items} ) } return ( {children} ) }) const GridWrapper = gloss(Stack, { width: '100%', flex: 1, opacity: 0, transition: 'all ease 300ms', overflow: 'auto', conditional: { mounted: { opacity: 1, background: 'transparent', }, }, }) const getSizes = items => { const sizes = {} for (const [index] of items.entries()) { sizes[index] = { w: 1, h: 1 } } return sizes } function GridLayoutObject(props: GridLayoutPropsItems) { const gridStore = useStore(GridStore, props) const items = props.items.map((item, idx) =>
{props.renderItem(item, idx)}
) const sizes = getSizes(props.items) useEffect(() => { console.log('set sizes') gridStore.setItems(sizes) }, [JSON.stringify(sizes)]) // @ts-ignore if (!gridStore.layout.lg) { return null } return ( {items} ) } export type GridItemProps = Omit, 'id'> & GridItemDimensions & { id: string } // warning: performance is sensitive here export function GridItem(props: GridItemProps) { const store = useGridStore() useEffect(() => { store.mountItem(props) return () => { store.unmountItem(props) } }, [props.h, props.w, props.id]) const onMouseDown = useCallback(e => isRightClick(e) && e.stopProgation(), []) return ( {forwardSurfaceProps(props.children, { flex: 1 })} ) } GridItem.defaultProps = { w: 'auto', h: 'auto', } const GridItemChrome = gloss({ flex: 1, }) GridItem.isGridItem = true // TODO this could be a pattern function forwardSurfaceProps(children: any, props: SurfaceProps) { if (children && children.type && children.type.accepts && children.type.accepts.surfaceProps) { return cloneElement(children, props) } return children } // compacts horizontal + vertical // see https://github.com/STRML/react-grid-layout/issues/157 function dualCompact(items: any[]) { const max_x = items.reduce(function(max_x, item) { return Math.max(max_x, item.x + item.w) }, 0) const max_y = items.reduce(function(max_y, item) { return Math.max(max_y, item.y + item.h) }, 0) const matrix = [] for (let y = 0; y < max_y; y++) { matrix.push(new Array(max_x).fill(undefined)) } //fill layout matrix items.forEach(function(item) { for (let i_y = item.y, i_y_max = item.y + item.h; i_y < i_y_max; i_y++) { for (let i_x = item.x, i_x_max = item.x + item.w; i_x < i_x_max; i_x++) { matrix[i_y][i_x] = item.i } } }) //compact vertical let compressed = 0 for (let y = 0; y < max_y; y++) { let is_empty_row = true for (let x = 0; x < max_x; x++) { if (matrix[y][x] !== undefined) { is_empty_row = false break } } if (is_empty_row) { const compressed_y = y - compressed items.forEach(function(item) { if (item.y > compressed_y) { item.y-- } }) compressed++ } } //compact horizontal compressed = 0 for (let x = 0; x < max_x; x++) { let is_empty_col = true for (let y = 0; y < max_y; y++) { if (matrix[y][x] !== undefined) { is_empty_col = false break } } if (is_empty_col) { const compressed_x = x - compressed items.forEach(function(item) { if (item.x > compressed_x) { item.x-- } }) compressed++ } } } const autoSize = (item: GridItemProps, cols: number, items: GridItems) => { const total = Object.keys(items).length let defaultSize = 1 if (total === 1) { defaultSize = cols } else if (total <= cols / 2) { const n = cols / total defaultSize = Math.floor(n % 1 === 0 ? n : n) } else { defaultSize = cols >= 4 ? 2 : 1 } const w = item.w === 'auto' ? defaultSize : item.w const h = item.h === 'auto' ? defaultSize : item.h return { w, h } } // attempts to position things nicely function calculateLayout( items: GridItems, gridCols: GridColumns, gridLayout: GridLayout, size: keyof GridLayout, ) { const cols = gridCols[size] const prevLayout = gridLayout[size] const layoutNeedsUpdate = () => { if (!prevLayout) return true const layoutIds = prevLayout.map(x => x.i) const itemIds = Object.keys(items) return itemIds.some(id => !layoutIds.some(y => id === y)) } if (layoutNeedsUpdate() === false) { return prevLayout } const layout: LayoutItem[] = [] let lastX = 0 let lastY = 0 for (const [idx, key] of Object.keys(items).entries()) { const item = items[key] const { w, h } = autoSize(item, cols, items) let x = lastX + idx === 0 ? 0 : w + lastX const shouldWrap = x + w > cols if (shouldWrap) { x = 0 } const y = x + w > cols ? lastY + 1 : lastY const next = { x, y, w, h, i: key, } lastY = y lastX = x layout.push(next) } return layout }