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