From ebaf689164d2df8be281634d667c9cb0731c25cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Thu, 2 Jun 2022 14:33:46 +0200 Subject: [PATCH] [dome/dnd] full featured DragSource --- ivette/src/dome/renderer/newdnd.tsx | 161 ++++++++++++++++++++++------ ivette/src/dome/renderer/style.css | 2 +- ivette/src/sandbox/sandbox.css | 8 ++ ivette/src/sandbox/usednd.tsx | 46 +++++--- 4 files changed, 172 insertions(+), 45 deletions(-) create mode 100644 ivette/src/sandbox/sandbox.css diff --git a/ivette/src/dome/renderer/newdnd.tsx b/ivette/src/dome/renderer/newdnd.tsx index 341b9b088cc..fc40741d610 100644 --- a/ivette/src/dome/renderer/newdnd.tsx +++ b/ivette/src/dome/renderer/newdnd.tsx @@ -29,60 +29,157 @@ */ import React from 'react'; +import { classes, styles } from 'dome/misc/utils'; import { DraggableCore, DraggableEventHandler } from 'react-draggable'; +/** + Current dragging informations: + - `rootX,rootY` is the position where dragging started; + - `dragX,dragY` is the current dragging position; + - `rect` is the original DOM Rectangle of the dragged HTML node. + + Hence, the relative move during dragging is simply `(dragX-rootX,dragY-rootY)`. + */ +export interface Dragging { + rootX: number; + rootY: number; + dragX: number; + dragY: number; + rect: DOMRect; +} + +/** + Can be used to conditionally render an element wrt to dragging informations. + */ +export type DraggingRenderer = (d: Dragging | undefined) => JSX.Element; + +interface OverlayRendering { + outerClass?: string; + innerClass?: string; + outerStyle?: React.CSSProperties; + innerStyle?: React.CSSProperties; +} + +function RenderOverlay( + props: DragSourceProps, + dragging: Dragging | undefined, +): OverlayRendering { + const { className, style } = props; + if (dragging) { + const { dragX, dragY, rootX, rootY, rect } = dragging; + const { left, top, width, height } = rect; + const { + zIndex = 1, + offsetX = 0, + offsetY = 0, + classDragged = 'dome-dragged', + classDragging = 'dome-dragging', + } = props; + const position: React.CSSProperties = { + position: 'fixed', + left: left + offsetX + dragX - rootX, + top: top + offsetY + dragY - rootY, + width, height, zIndex, margin: 0 + }; + const holder = { width, height }; + return { + outerClass: classes(className, classDragged), + innerClass: classes(className, classDragging), + outerStyle: styles(style, props.styleDragged, holder), + innerStyle: styles(style, props.styleDragging, position), + }; + } + return { outerClass: className, outerStyle: style } +} + export interface DragSourceProps { + /** Disabled dragging. */ disabled?: boolean; + /** Class of the element from where a drag can be initiated. */ handle?: string; - children?: React.ReactNode; + /** Class of the DragSource elements. */ + className?: string; + /** Style of the DragSource elements. */ + style?: React.CSSProperties; + /** Additional class for the dragged (initial) element (default is `'dome-dragged'`). */ + classDragged?: string; + /** Additional class for the dragging (moved) element (default is `'dome-dragging'`) */ + classDragging?: string; + /** Additional style for the dragged (initial) element. */ + styleDragged?: React.CSSProperties; + /** Additional style for the dragging (moved) element. */ + styleDragging?: React.CSSProperties; + /** X-offset when dragging (defaults to 0). */ + offsetX?: number; + /** Y-offset when dragging (defaults to 0). */ + offsetY?: number; + /** Z-index when dragging (defaults to 1). */ + zIndex?: number; + /** Callback when drag is initiated. */ onStart?: () => void; - onDrag?: (deltaX: number, deltaY: number) => void; + /** Callback current dragging. */ + onDrag?: (dragging: Dragging) => void; + /** Callback when drag is interrupted. */ onStop?: () => void; + /** Inner contents of the DragSource element. */ + children?: React.ReactNode | DraggingRenderer; } -interface Dragging { - rootX: number, - rootY: number, - dragX: number, - dragY: number, -} +/** + This container can be dragged around all over the application window. Its + content is rendered inside a double `<div/>`, the outer one being fixed when + dragged, and the inner one being moved around when dragging. + The content can be rendered conditionnaly by using a function. + */ export function DragSource(props: DragSourceProps): JSX.Element | null { + //--- Props const { disabled, handle, children } = props; + const { onStart, onDrag, onStop } = props; + //--- Dragging State const [dragging, setDragging] = React.useState<Dragging | undefined>(); - const onStart: DraggableEventHandler = (_, { x, y }) => { - setDragging({ - rootX: x, rootY: y, - dragX: x, dragY: y - }); - if (props.onStart) props.onStart(); - }; - const onDrag: DraggableEventHandler = (_, { x, y }) => { - if (dragging) { - setDragging({ ...dragging, dragX: x, dragY: y }); - if (props.onDrag) { - const deltaX = x - dragging.rootX; - const deltaY = y - dragging.rootY; - props.onDrag(deltaX, deltaY); + //--- onStart + const handleStart: DraggableEventHandler = React.useCallback( + (_, { x, y, node }) => { + setDragging({ + rootX: x, rootY: y, + dragX: x, dragY: y, + rect: node.getBoundingClientRect(), + }); + if (onStart) onStart(); + }, [onStart]); + //--- onDrag + const handleDrag: DraggableEventHandler = React.useCallback( + (_, { x, y }) => { + if (dragging) { + setDragging({ ...dragging, dragX: x, dragY: y }); + if (onDrag) onDrag(dragging); } - } - }; - const onStop: DraggableEventHandler = () => { - setDragging(undefined); - if (props.onStop) props.onStop(); - }; + }, [dragging, onDrag]); + //--- onStop + const handleStop: DraggableEventHandler = React.useCallback( + () => { + setDragging(undefined); + if (onStop) onStop(); + }, [onStop]); + //--- Renderer + const render = RenderOverlay(props, dragging); return ( <DraggableCore disabled={disabled} handle={handle} - onStart={onStart} - onDrag={onDrag} - onStop={onStop} + onStart={handleStart} + onDrag={handleDrag} + onStop={handleStop} > - <div>{children}</div> + <div className={render.outerClass} style={render.outerStyle}> + <div className={render.innerClass} style={render.innerStyle}> + {typeof (children) === 'function' ? children(dragging) : children} + </div> + </div> </DraggableCore> ); } diff --git a/ivette/src/dome/renderer/style.css b/ivette/src/dome/renderer/style.css index 9b168aa91ce..ed2873fe60a 100644 --- a/ivette/src/dome/renderer/style.css +++ b/ivette/src/dome/renderer/style.css @@ -69,7 +69,7 @@ div.dome-dragged { border: none ; } -.dome-dragging * { +div.dome-dragging, .dome-dragging * { cursor: move ; } diff --git a/ivette/src/sandbox/sandbox.css b/ivette/src/sandbox/sandbox.css new file mode 100644 index 00000000000..f1fce9e8715 --- /dev/null +++ b/ivette/src/sandbox/sandbox.css @@ -0,0 +1,8 @@ +.sandbox-item { + background: var(--lcd-button-background); + border-radius: 4px; + padding: 2px; + margin: 2px; + width: 100px; + text-align: center; +} diff --git a/ivette/src/sandbox/usednd.tsx b/ivette/src/sandbox/usednd.tsx index fbb694800aa..aa6c4b67b66 100644 --- a/ivette/src/sandbox/usednd.tsx +++ b/ivette/src/sandbox/usednd.tsx @@ -26,28 +26,50 @@ /* -------------------------------------------------------------------------- */ import React from 'react'; -//import * as Dome from 'dome'; -//import * as Ctrl from 'dome/controls/buttons'; -import * as Disp from 'dome/controls/displays'; +import { LCD } from 'dome/controls/displays'; import * as Box from 'dome/layout/boxes'; import * as DnD from 'dome/newdnd'; import { registerSandbox } from 'ivette'; +import './sandbox.css'; + +const delta = (id: string, d: DnD.Dragging): string => { + const dx = d.dragX - d.rootX; + const dy = d.dragY - d.rootY; + return `${id} ${dx}:${dy}` +}; + +interface ItemProps { + id: string; + setState: (s: string) => void; +} + +function Item(props: ItemProps): JSX.Element { + const { id, setState } = props; + return ( + <DnD.DragSource + className='sandbox-item' + styleDragging={{ background: 'lightgreen' }} + onStart={() => setState(id)} + onDrag={(d) => setState(delta(id, d))} + onStop={() => setState('--')} + > + Item {id} + </DnD.DragSource> + ); +} function UseDnD(): JSX.Element { const [state, setState] = React.useState('--'); - //const [blink, setBlink] = React.useState(false); return ( <Box.Vfill> <Box.Hbox> - <Disp.LCD label={state} /> + <LCD label={state} /> </Box.Hbox> - <DnD.DragSource - onStart={() => setState('??')} - onDrag={(x, y) => setState(`${x}:${y}`)} - onStop={() => setState('--')} - > - Using Drag & Drop - </DnD.DragSource> + <Box.Vbox> + <Item id='A' setState={setState} /> + <Item id='B' setState={setState} /> + <Item id='C' setState={setState} /> + </Box.Vbox> </Box.Vfill> ); } -- GitLab