UNPKG

mauerwerk

Version:

⚒ Animated masonry-like grid with enter/exit transitions and maximized cells

277 lines (266 loc) 7.51 kB
import React from 'react' import PropTypes from 'prop-types' import Measure from 'react-measure' import { Transition, Trail, animated, interpolate, config } from 'react-spring' const styles = { outer: { position: 'relative', width: '100%', height: '100%' }, inner: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden', minHeight: '100%', }, cell: { position: 'absolute', willChange: 'transform, width, height, opacity', }, } export class Grid extends React.PureComponent { static propTypes = { data: PropTypes.array, keys: PropTypes.func, occupySpace: PropTypes.bool, columns: PropTypes.number, margin: PropTypes.number, heights: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), lockScroll: PropTypes.bool, closeDelay: PropTypes.number, transitionMount: PropTypes.bool, } state = { width: 0, height: 0, heightOuter: 0, widthOuter: 0, open: undefined, lastOpen: undefined, } scrollOut = e => { if (!this.props.lockScroll) { this.state.open && this.toggle(undefined) this.clicked = false } } toggle = key => this.setState( state => ({ lastOpen: state.open, open: state.open === key ? undefined : key, }), () => (this.clicked = true) ) resize = (width, height, props) => this.setState({ [width]: props.client.width, [height]: props.client.height, }) resizeOuter = props => this.resize('widthOuter', 'heightOuter', props) resizeInner = props => this.resize('width', 'height', props) update = ({ key, x, y, width, height }) => { const open = this.state.open === key return { opacity: this.state.open && !open ? 0 : 1, x: open ? this.outerRef.scrollLeft : x, y: open ? this.outerRef.scrollTop : y, width: open ? this.state.width : width, height: open ? this.state.heightOuter : height, } } componentDidUpdate() { this.clicked = false } cell = ({ key, object }, i) => ({ lastOpen, open, opacity, x, y, width, height, }) => ( <animated.div style={{ ...styles.cell, opacity, width, height, zIndex: lastOpen === key || open === key ? 1000 : i, transform: interpolate( [x, y], (x, y) => `translate3d(${x}px,${y}px, 0)` ), }} children={this.props.children(object, open === key, () => this.toggle(key) )} /> ) render() { let { columns = 3, margin = 0, occupySpace = true, lockScroll = false, closeDelay = 0, transitionMount = true, children, impl, config, data, keys, heights, ...rest } = this.props let { lastOpen, open, height, width, widthOuter, heightOuter } = this.state let column = 0 let columnHeights = new Array(columns).fill(0) let displayData = data.map((child, i) => { let index = occupySpace ? columnHeights.indexOf(Math.min(...columnHeights)) : column++ % columns let cellWidth = width / columns - margin / (1 - 1 / (columns + 1)) let left = cellWidth * index let offset = (index + 1) * margin let top = columnHeights[index] + margin let cellHeight = typeof heights === 'function' ? heights(child) : heights || heightOuter - margin * 2 columnHeights[index] += cellHeight + margin return { x: margin ? left + offset : left, y: top, width: cellWidth, height: cellHeight, key: keys(child, i), object: child, } }) const overflow = lockScroll ? (open ? 'hidden' : 'auto') : 'auto' const totalHeight = Math.max(...columnHeights) + margin return ( <Measure client innerRef={r => (this.outerRef = r)} onResize={this.resizeOuter}> {({ measureRef }) => ( <div ref={measureRef} style={{ ...styles.outer, ...this.props.style, overflow }} {...rest} onScroll={this.scrollOut} onWheel={this.scrollOut} onTouchMove={this.scrollOut}> <Measure client innerRef={r => (this.innerRef = r)} onResize={this.resizeInner}> {({ measureRef }) => ( <div ref={measureRef} style={{ ...styles.inner, height: totalHeight }}> <Transition native delay={this.clicked && !open ? closeDelay : 0} items={displayData} keys={d => d.key} from={{ opacity: 0 }} leave={{ opacity: 0 }} enter={this.update} update={this.update} impl={impl} config={config} lastOpen={lastOpen} open={open} children={displayData.map(this.cell)} /> </div> )} </Measure> </div> )} </Measure> ) } } const wrap = (child, styles) => { styles = { willChange: Object.keys(styles).join(','), ...styles } if (!animated[child.type]) { // Wrap components into animated divs return <animated.div style={{ ...styles }}>{child}</animated.div> } else { // Otherwise inject styles into existing component-props const Component = animated[child.type] const props = { ...child.props, style: { ...child.props.style, ...styles, }, } return <Component {...props} /> } } // Wrapper around react-springs Trail component. // It will make each child (which must be a dom node) fade and trail in. export class Slug extends React.PureComponent { static propTypes = { from: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), to: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), } render() { const { children, from = { opacity: 0, transform: 'translate3d(0,40px,0)' }, to = { opacity: 1, transform: 'translate3d(0,0px,0)' }, ...rest } = this.props const result = React.Children.map(children, child => styles => wrap(child, styles) ) return ( <Trail native {...rest} keys={result.map((_, i) => i)} from={from} to={to} children={result} /> ) } } // Wrapper around react-springs Transition. // It will Transition the child node in and out depending on the "show" prop. export class Fade extends React.PureComponent { static propTypes = { children: PropTypes.node.isRequired, show: PropTypes.bool, from: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), enter: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), leave: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), } render() { const { children, show = true, from = { opacity: 0 }, enter = { opacity: 1 }, leave = { opacity: 0 }, ...rest } = this.props const result = styles => wrap(children, styles) return ( <Transition native keys={show.toString()} {...rest} from={from} enter={enter} leave={leave} children={show ? result : undefined} /> ) } }