import { CoordinateTransform, NewBBox, NewBBoxClass } from './NewBBox'; import React, { useRef, useState, useCallback, useImperativeHandle, isValidElement, ComponentType, forwardRef, PropsWithChildren, useContext, useEffect, useId, } from 'react'; import { isEqual, isNaN } from 'lodash'; import ReactIs from 'react-is'; import { flattenChildren } from './flatten-children'; // TODO: we need to change this code so that children accumulate the coordinate transformation from // the root component down to them. This is necessary so that references can resolve correct positions. // To implement these we need to: // 1. Change the coordinate transformation to be a stack of transformations. // The function we need to update is `placeUnlessDefined`. // Incorrect. this function is no longer used. try again. // 2. Change the `measure` function to return a coordinate transformation. // Ok. But how do we accumulate the coordinate transformation? // 3. Change the `measure` function to take a coordinate transformation. // Ok. But how do we accumulate the coordinate transformation? // 4. Change the `measure` function to take a coordinate transformation and return a coordinate // transformation. export type Measurable = { domRef: SVGElement | null; name: string; measure(constraints: Constraints, isRef?: boolean): NewBBoxClass; transformStack: CoordinateTransform[] | undefined; }; export type MeasureResult = Partial; export type Measure = (measurables: Array, constraints: Constraints) => MeasureResult; export type BBox = { x: number; y: number; width: number; height: number; }; export type BBoxWithChildren = Partial>; export type NewBBoxWithChildren = Partial>; // export type Constraints = { // minWidth: number; // maxWidth: number; // minHeight: number; // maxHeight: number; // }; export type Constraints = { width?: number; height?: number; }; export type Placeable = { place: (point: { x?: number; y?: number }) => void; placeUnlessDefined: (point: { x?: number; y?: number }) => void; measuredWidth: number; measuredHeight: number; }; export type NewPlaceable = { top?: number; left?: number; right?: number; bottom?: number; width?: number; height?: number; }; // TODO: this is almost correct except that the index is going to be wrong if visit nested children // such as in the contextprovider case. // I think the most robust way to do this is to create new Bluefish components that wrap the // fragment and contextprovider components. This way we can use the Bluefish component's index // to determine the order of the children. // this currently works if a context provider is the only child of a component // but it doesn't work if there are multiple children and one of them is a context // provider. const processChildren = ( children: React.ReactNode, callbackRef: (child: any, index: number) => (node: any) => void, ): any => { return React.Children.map(flattenChildren(children), (child, index) => { if (ReactIs.isContextProvider(child)) { // TODO: try to push this into the flattenChildren function. the difference between dealing // with Context and with Fragment is that we still want to render ContextProviders. return React.cloneElement(child, { children: processChildren(child.props.children, callbackRef) }); } else if (isValidElement(child)) { return React.cloneElement(child as React.ReactElement, { // store a pointer to every child's ref in an array // also pass through outer refs // see: https://github.com/facebook/react/issues/8873 // ref: (node: any) => { // childrenRef.current[index] = node; // // console.log('setting child ref', index, node, node.name); // if (node !== null && 'name' in node && node.name !== undefined) { // console.log('setting ref', node.name, node); // context.bfMap.set(node.name, node); // } // const { ref } = child as any; // console.log('current ref on child', ref); // if (typeof ref === 'function') ref(node); // else if (ref) { // ref.current = node; // } // }, ref: callbackRef(child, index), }); } else { // TODO: what to do with non-elements? console.log('warning: non-element child', child); return child; } }); }; // a layout hook export const useBluefishLayout = ( measure: Measure, bbox: Partial, coord: Partial, ref: React.ForwardedRef, domRef: React.RefObject, props: any, children?: React.ReactNode, name?: any, ): NewBBoxWithChildren => { const context = useContext(BluefishContext); const childrenRef = useRef([]); const [left, setLeft] = useState(bbox.left); const [top, setTop] = useState(bbox.top); const [right, setRight] = useState(bbox.right); const [bottom, setBottom] = useState(bbox.bottom); const [width, setWidth] = useState(bbox.width); const [height, setHeight] = useState(bbox.height); // const [_coord, setCoord] = useState(coord); const coordRef = useRef(coord ?? {}); const bboxClassRef = useRef(undefined); const transformStackRef = useRef(undefined); // remember constraints so we can re-measure if they change const constraintRef = useRef(undefined); // remember props so we can re-measure if they change const propsRef = useRef(undefined); // useEffect(() => { // console.log(name, 'left updated to', left); // }, [name, left]); // useEffect(() => { // console.log(name, 'top updated to', top); // }, [name, top]); // useEffect(() => { // if (name !== undefined) { // console.log('setting ref', name, ref); // context.bfMap.set(name, ref as any); // } // }); useImperativeHandle( ref, (): Measurable => ({ domRef: domRef.current, name, get transformStack() { return transformStackRef.current; }, set transformStack(transforms: CoordinateTransform[] | undefined) { if (transforms === undefined) { transformStackRef.current = [coordRef.current]; } else { transformStackRef.current = [...transforms, coordRef.current]; } }, measure(constraints: Constraints, isRef?: boolean): NewBBoxClass { // console.log('measuring', name, 'with constraints', constraints); let bbox; if ( isRef !== true && (bboxClassRef.current === undefined || !isEqual(constraintRef.current, constraints) || propsRef.current !== props) ) { constraintRef.current = constraints; // console.log('measuring', name); childrenRef.current.forEach((child) => { if (child !== undefined) { child.transformStack = transformStackRef.current; } }); const { width, height, left, top, right, bottom } = measure(childrenRef.current, constraints); console.log('measured', name, JSON.stringify({ width, height, left, top, right, bottom })); setWidth(width); setHeight(height); setLeft(left); setTop(top); setRight(right); setBottom(bottom); bbox = new NewBBoxClass( { left, top, right, bottom, width, height, coord: coordRef.current }, { left: (left) => { // console.log(name, 'left set to', left); return setLeft(left); }, top: (top) => { // console.log(name, 'top set to', top); return setTop(top); }, right: (right) => { // console.log(name, 'right set to', right); return setRight(right); }, bottom: (bottom) => { // console.log(name, 'bottom set to', bottom); return setBottom(bottom); }, width: (width) => { // console.log(name, 'width set to', width); return setWidth(width); }, height: (height) => { // console.log(name, 'height set to', height); return setHeight(height); }, coord: (coord) => { // console.log(name, 'coord set to', coord); coordRef.current.scale = coord?.scale ?? {}; coordRef.current.translate = coord?.translate ?? {}; }, }, ); bboxClassRef.current = bbox; } else { bbox = bboxClassRef.current; console.log('using cached bbox', name, bbox); } return bbox!; }, }), [ measure, childrenRef, setLeft, setTop, setRight, setBottom, setWidth, setHeight, name, bboxClassRef, props, domRef, ], ); console.log(`returning bbox for ${name}`, { left, top, right, bottom, width, height }); return { left: left, top: top, right: right, bottom: bottom, width: width, height: height, coord: coordRef.current, children: processChildren(children, (child, index) => (node: any) => { childrenRef.current[index] = node; // console.log('setting child ref', index, node, node.name); if (node !== null && 'name' in node && node.name !== undefined) { console.log('setting ref', node.name, node); context.bfMap.set(node.name, node); } const { ref } = child as any; // console.log('current ref on child', ref); if (typeof ref === 'function') ref(node); else if (ref) { ref.current = node; } }), // children: React.Children.map(children, (child, index) => { // if (isValidElement(child)) { // return React.cloneElement(child as React.ReactElement, { // // store a pointer to every child's ref in an array // // also pass through outer refs // // see: https://github.com/facebook/react/issues/8873 // ref: (node: any) => { // childrenRef.current[index] = node; // // console.log('setting child ref', index, node, node.name); // if (node !== null && 'name' in node && node.name !== undefined) { // console.log('setting ref', node.name, node); // context.bfMap.set(node.name, node); // } // const { ref } = child as any; // console.log('current ref on child', ref); // if (typeof ref === 'function') ref(node); // else if (ref) { // ref.current = node; // } // }, // }); // } else { // // TODO: what to do with non-elements? // console.log('warning: non-element child', child); // return child; // } // }), }; }; // a layout HOC export const withBluefish = ( measure: Measure, WrappedComponent: React.ComponentType }>, ) => forwardRef( ( props: PropsWithChildren /* & { $bbox?: Partial } */ & { name?: any; debug?: boolean }, ref: any, ) => { const domRef = useRef(null); const { left, top, bottom, right, width, height, children, coord } = useBluefishLayout( measure, { // left: props.bbox?.left, // top: props.bbox?.top, // right: props.bbox?.right, // bottom: props.bbox?.bottom, // width: props.bbox?.width, // height: props.bbox?.height, }, {}, ref, domRef, props, props.children, props.name, ); return ( <> {children} {props.debug && ( )} ); }, ); export const withBluefishFn = ( measureFn: (props: ComponentProps & PropsWithChildren<{ $bbox?: Partial }>) => Measure, WrappedComponent: React.ComponentType; $coord?: CoordinateTransform }>, ) => forwardRef((props: PropsWithChildren & { name?: any; debug?: boolean }, ref: any) => { const domRef = useRef(null); const { left, top, bottom, right, width, height, children, coord } = useBluefishLayout( measureFn(props), { // left: props.bbox?.left, // top: props.bbox?.top, // right: props.bbox?.right, // bottom: props.bbox?.bottom, // width: props.bbox?.width, // height: props.bbox?.height, }, {}, ref, domRef, props, props.children, props.name, ); return ( <> {children} {props.debug && ( )} ); }); // export const withBluefishFnWithContext = ( // measureFn: ( // props: ComponentProps & PropsWithChildren<{ $bbox?: Partial }>, // context: BluefishContextValue, // ) => Measure, // WrappedComponent: React.ComponentType }>, // ) => // forwardRef((props: PropsWithChildren & { name?: any }, ref: any) => { // const context = useContext(BluefishContext); // const { left, top, bottom, right, width, height, children, coord } = useBluefishLayout( // measureFn(props, context), // { // // left: props.bbox?.left, // // top: props.bbox?.top, // // right: props.bbox?.right, // // bottom: props.bbox?.bottom, // // width: props.bbox?.width, // // height: props.bbox?.height, // }, // {}, // ref, // props.children, // props.name, // ); // return ( // // {children} // // ); // }); // a pure layout component builder export const Layout = (measurePolicy: Measure) => withBluefish(measurePolicy, (props: PropsWithChildren<{ $bbox?: Partial }>) => { return ( {props.children} ); // return {props.children}; }); export const LayoutFn = ( measurePolicyFn: (props: T & PropsWithChildren<{ $bbox?: Partial }>) => Measure, ) => withBluefishFn(measurePolicyFn, (props: PropsWithChildren<{ $bbox?: Partial }>) => { return ( {props.children} ); // return {props.children}; }); // TODO: this HOC doesn't work :/ export const withBluefishComponent = ( WrappedComponent: React.ComponentType, ) => forwardRef((props: ComponentProps & BBoxWithChildren, ref: any) => { return ( {React.Children.map(props.children, (child, index) => { if (isValidElement(child)) { return React.cloneElement(child as React.ReactElement, { ref, // store a pointer to every child's ref in an array // also pass through outer refs // see: https://github.com/facebook/react/issues/8873 // ref: (node: any) => { // childrenRef.current[index] = node; // const { ref } = child as any; // if (typeof ref === 'function') ref(node); // else if (ref) ref.current = node; // }, }); } else { // TODO: what to do with non-elements? console.log('warning: non-element child', child); return child; } })} ); }); export type BluefishContextValue = { bfMap: Map>; setBFMap: React.Dispatch>>; }; export const BluefishContext = React.createContext({ bfMap: new Map(), setBFMap: () => {}, }); export const useBluefishContext = () => useContext(BluefishContext);