import React from 'react' ; import { deepMemo } from "../../react/ReactUtils" ; import { ILTRect } from "polar-shared/src/util/rects/ILTRect" ; import { VerticalLine } from './VerticalLine' ; import { HorizontalLine } from "./HorizontalLine" ; import { IPoint } from "../../Point" ; import { Rects } from "../../Rects" ; import { ILTBRRects } from "polar-shared/src/util/rects/ILTBRRects" ; export type ResizableBounds = 'parent' ; interface IProps { readonly id ? : string ; readonly color : string ; readonly document ? : Document ; readonly window ? : Window ; readonly style ? : React . CSSProperties ; readonly className ? : string ; readonly resizeAxis ? : 'y' ; readonly computeInitialPosition : ( ) => ILTRect ; readonly onResized ? : ( resizeRect : ILTRect , direction : Direction ) => void ; readonly onContextMenu ? : ( event : React . MouseEvent < HTMLElement , MouseEvent > ) => void ; readonly bounds ? : ResizableBounds ; /**
     * CSS properties for the resize handles.
     */ readonly resizeHandleStyle ? : React . CSSProperties ; } export type Direction = 'top' | 'bottom' | 'left' | 'right' ; type MouseEventHandler = ( event : MouseEvent ) => void ; export const Resizable = deepMemo ( function Resizable ( props ) { const [ position , setPosition ] = React . useState < ILTRect > ( props . computeInitialPosition ( ) ) ; const resizingPositionRef = React . useRef ( position ) ; const mouseDown = React . useRef ( false ) ; const mouseEventOrigin = React . useRef < IPoint | undefined > ( undefined ) ; const mouseMoveHandler = React . useRef < MouseEventHandler | undefined > ( undefined ) ; const elementRef = React . useRef < HTMLElement | null > ( ) ; const win = props . window || window ; const doc = props . document || document ; const computeBoundsParentElement = React . useCallback ( ( ) => { return elementRef . current ?. parentElement ; } , [ ] ) ; const computeBoundsParentElementRect = React . useCallback ( ( ) : ILTRect => { const boundsParentElement = computeBoundsParentElement ( ) ; if ( boundsParentElement ) { return Rects . createFromOffset ( boundsParentElement ) ; } return Rects . createFromOffset ( doc . body ) ; } , [ computeBoundsParentElement , doc . body ] ) ; const toggleUserSelect = React . useCallback ( ( resizing ) => { if ( resizing ) { doc . body . style . userSelect = 'none' ; } else { doc . body . style . userSelect = 'auto' ; } } , [ doc . body . style ] ) ; const toggleCursor = React . useCallback ( ( direction ) => { if ( ! direction ) { doc . body . style . cursor = 'auto' ; return ; } function computeCursor ( direction ) { switch ( direction ) { case "top" : return 'row-resize' ; case "bottom" : return 'row-resize' ; case "left" : return 'col-resize' ; case "right" : return 'col-resize' ; } } doc . body . style . cursor = computeCursor ( direction ) ; } , [ doc . body . style ] ) ; const updatePosition = React . useCallback ( ( position , direction ) => { setPosition ( position ) ; if ( props . onResized ) { props . onResized ( position , direction ) ; } } , [ props ] ) ; const handleMouseUp = React . useCallback ( ( ) => { mouseDown . current = false ; toggleUserSelect ( false ) ; toggleCursor ( undefined ) ; win . removeEventListener ( 'mousemove' , mouseMoveHandler . current ! ) ; } , [ toggleCursor , toggleUserSelect , win ] ) ; const handleMouseMove = React . useCallback ( ( event , direction ) => { if ( ! mouseDown . current ) { return ; } const origin = mouseEventOrigin . current ! ; const delta = { x : event . clientX - origin . x , y : event . clientY - origin . y } ; /**
         * Compute the position raw/directly from the current delta.
         */ function computeResizingPosition ( ) { switch ( direction ) { case "top" : return { ... resizingPositionRef . current , top : Math . min ( resizingPositionRef . current . top + delta . y , resizingPositionRef . current . top + resizingPositionRef . current . height ) , height : Math . max ( resizingPositionRef . current . height - delta . y , 0 ) } ; case "bottom" : return { ... resizingPositionRef . current , height : resizingPositionRef . current . height + delta . y } ; case "left" : return { ... resizingPositionRef . current , left : Math . min ( resizingPositionRef . current . left + delta . x , resizingPositionRef . current . left + resizingPositionRef . current . width ) , width : Math . max ( resizingPositionRef . current . width - delta . x , 0 ) } ; case "right" : return { ... resizingPositionRef . current , width : resizingPositionRef . current . width + delta . x , } ; } } /**
         * Compute the new position but factor in bounds too.
         */ function computePosition ( ) { const resizingPosition = computeResizingPosition ( ) ; if ( props . bounds ) { const boundsParentElementRect = computeBoundsParentElementRect ( ) ; const boundedLTRB = { left : Math . max ( resizingPosition . left , 0 ) , top : Math . max ( resizingPosition . top , 0 ) , right : Math . min ( resizingPosition . left + resizingPosition . width , boundsParentElementRect . width ) , bottom : Math . min ( resizingPosition . top + resizingPosition . height , boundsParentElementRect . height ) } ; return ILTBRRects . toLTRect ( boundedLTRB ) ; } return resizingPosition ; } resizingPositionRef . current = computeResizingPosition ( ) ; updatePosition ( computePosition ( ) , direction ) ; mouseEventOrigin . current = event ; } , [ computeBoundsParentElementRect , props . bounds , updatePosition ] ) ; const handleMouseDown = React . useCallback ( ( event , direction ) => { mouseDown . current = true ; mouseEventOrigin . current = { x : event . clientX , y : event . clientY } ; toggleUserSelect ( true ) ; toggleCursor ( direction ) ; mouseMoveHandler . current = ( event ) => handleMouseMove ( event , direction ) ; win . addEventListener ( 'mousemove' , mouseMoveHandler . current ! ) ; win . addEventListener ( 'mouseup' , ( ) => { handleMouseUp ( ) ; } , { once : true } ) ; } , [ handleMouseMove , handleMouseUp , toggleCursor , toggleUserSelect , win ] ) ; const style = { position : 'absolute' , top : `${ position . top }px` , left : `${ position . left }px` , width : `${ position . width }px` , height : `${ position . height }px` , overflow : 'none' } ; return ( < div id = { props . id } style = { { ... style , ... props . style } } ref = { ref => elementRef . current = ref } draggable = { false } className = { props . className } onContextMenu = { props . onContextMenu } > < VerticalLine side = "left" color = { props . color } height = { position . height } onMouseDown = { event => handleMouseDown ( event , 'left' ) } onMouseUp = { handleMouseUp } / > < VerticalLine side = "right" color = { props . color } height = { position . height } onMouseDown = { event => handleMouseDown ( event , 'right' ) } onMouseUp = { handleMouseUp } / > < HorizontalLine side = "top" color = { props . color } width = { position . width } onMouseDown = { event => handleMouseDown ( event , 'top' ) } onMouseUp = { handleMouseUp } / > < HorizontalLine side = "bottom" color = { props . color } width = { position . width } onMouseDown = { event => handleMouseDown ( event , 'bottom' ) } onMouseUp = { handleMouseUp } / > < /div>); } ) ; export namespace HTMLElements { /**
     * Search backwards in the DOM to find the most positioned ancestor or
     * the current element if it's positioned.
     */ export function findPositionedAncestor ( element ) { if ( element . style . position !== null && element . style . position !== 'static' ) { return element ; } if ( element . parentElement ) { return findPositionedAncestor ( element . parentElement ) ; } return undefined ; } export function computeOffset ( parent , child ) { const parentBCR = parent . getBoundingClientRect ( ) ; const childBCR = child . getBoundingClientRect ( ) ; const x = childBCR . x - parentBCR . x ; const y = childBCR . y - parentBCR . y ; return { x , y } ; } }