import * as anime from "animejs" import * as _ from "lodash" import * as React from "react" import * as interact from "interactjs" import { getElementRect } from "../../utilities/dom"; export interface ISheetProps { height: number, isOpen?: boolean onOpen?: () => void; onClose?: () => void; className?: string; onOpenStart?: () => void; onOpenEnd?: () => void; onCloseStart?: () => void; onCloseEnd?: () => void; } interface ISheetState { isOpen: boolean } const defaultSheetState: ISheetState = { isOpen: false, } export class Sheet extends React.Component { public state: ISheetState = defaultSheetState public ref: React.RefObject constructor(props: ISheetProps) { super(props) this.ref = React.createRef() } public componentDidMount() { this.initializeInteractions() if (this.props.isOpen) { this.open(); } } public shouldComponentUpdate(nextProps: ISheetProps, nextState: ISheetState) { // Only block update if the 'isOpen' flag has been flipped return this.state.isOpen === nextState.isOpen; } public componentDidUpdate(prevProps: ISheetProps) { if (this.props.isOpen && !this.state.isOpen) { this.open() } else if (!this.props.isOpen && this.state.isOpen) { this.close() } } public open = () => { this.withSheet((sheetNode, parentNode) => { anime.remove(sheetNode); parentNode.style.height = this.props.height + "px"; anime({ targets: sheetNode, height: [0, this.props.height], elasticity: (el, i, l) => 200 + i * 200, ...(this.props.onOpenStart && { begin: this.props.onOpenStart }), ...(this.props.onOpenEnd && { complete: this.props.onOpenEnd }), }) }); this.setState({ isOpen: true, }, () => { if (_.isFunction(this.props.onOpen)) { this.props.onOpen() } }); } public close = () => { this.withSheet((sheetNode, parentNode) => { anime.remove(sheetNode); parentNode.style.height = "0px"; anime({ targets: sheetNode, // TODO: Is this the sheets current height or its set height? height: [sheetNode.style.height, 0], elasticity: 0, ...(this.props.onCloseStart && { begin: this.props.onCloseStart }), ...(this.props.onCloseEnd && { complete: this.props.onCloseEnd }), }) }); this.setState({ isOpen: false, }, () => { if (_.isFunction(this.props.onClose)) { this.props.onClose() } }) } public render() { return (
{this.props.children}
) } private initializeInteractions = () => { this.withSheet(sheetNode => { interact(sheetNode) .resizable({ // resize top only edges: { top: '.sheet--resizable' }, // keep the edges inside the parent // @ts-ignore restrictEdges: { outer: 'parent', endOnly: true, }, // @ts-ignore snap: { targets: [ // @ts-ignore () => { return this.withSheet((sheetNode) => { const rect = getElementRect(sheetNode.parentElement); return { y: rect.bottom, range: 100 }; }); }, // @ts-ignore () => { return this.withSheet((sheetNode) => { const rect = getElementRect(sheetNode.parentElement); // @ts-ignore return { y: rect.top, range: Infinity }; }); }, ], // @ts-ignore endOnly: true, }, inertia: { resistance: 10, // the lambda in exponential decay minSpeed: 100, // target speed must be above this for inertia to start endSpeed: 10, // the speed at which inertia is slow enough to stop allowResume: true, // allow resuming an action in inertia phase smoothEndDuration: 300, // animate to snap/restrict endOnly if there's no inertia }, ignoreFrom: ".sheet--not-resizable", }) .on("resizemove", function(event: interact.InteractEvent) { // @ts-ignore event.target.style.height = event.rect.height + "px" }) .on("resizeend", (event: interact.InteractEvent) => { // Guard against no snap // @ts-ignore if (_.isNil(event.snap)) return; const isOpen = this.withSheet((sheetNode) => { const rect = getElementRect(sheetNode.parentElement); // @ts-ignore return rect.top === event.snap.y; }); if (isOpen !== this.state.isOpen) { if (!isOpen) { this.close(); } } else { if (isOpen && _.isFunction(this.props.onOpen)) { this.props.onOpen(); } else if (_.isFunction(this.props.onClose)) { this.props.onClose(); } } }) }) } /** * Give the sheet node */ private withSheet( found: (node: HTMLDivElement, parent?: HTMLElement) => T, notFound?: () => T ): T { const sheetNode = this.ref.current if (sheetNode) { return found(sheetNode, sheetNode.parentElement) } else if (notFound) { return notFound() } return null } }