diff --git a/vuu-ui/packages/vuu-layout/src/common-types.ts b/vuu-ui/packages/vuu-layout/src/common-types.ts deleted file mode 100644 index bb392dcc2..000000000 --- a/vuu-ui/packages/vuu-layout/src/common-types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface rect { - bottom: number; - left: number; - right: number; - top: number; -} -export type rectTuple = [number, number, number, number]; - -export type dimension = 'width' | 'height'; diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts index 0125035f7..f47ca39c0 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/BoxModel.ts @@ -1,5 +1,5 @@ import { ReactElement } from "react"; -import { rect } from "../common-types"; +import { boxContainsPoint } from "@finos/vuu-utils"; import { LayoutModel } from "../layout-reducer"; import { isContainer } from "../registry/ComponentRegistry"; import { getProps, typeOf } from "../utils"; @@ -132,7 +132,7 @@ export function getPosition( if (targetOrientation === "row") { position = pctX < 0.5 ? WEST : EAST; - } else if (rect.header && containsPoint(rect.header, x, y)) { + } else if (rect.header && boxContainsPoint(rect.header, x, y)) { position = HEADER; if (rect.Stack) { @@ -199,7 +199,7 @@ function getPositionWithinBox( pctY: number ) { const centerBox = getCenteredBox(rect, 0.2); - if (containsPoint(centerBox, x, y)) { + if (boxContainsPoint(centerBox, x, y)) { return CENTRE; } else { const quadrant = `${pctY < 0.5 ? "north" : "south"}${ @@ -474,7 +474,7 @@ function allBoxesContainingPoint( const type = typeOf(component) as string; const rect = measurements[path]; - if (!containsPoint(rect, x, y)) return boxes; + if (!boxContainsPoint(rect, x, y)) return boxes; if (dropTargets && dropTargets.length) { if (dropTargets.includes(path)) { @@ -494,7 +494,7 @@ function allBoxesContainingPoint( return boxes; } - if (rect.header && containsPoint(rect.header, x, y)) { + if (rect.header && boxContainsPoint(rect.header, x, y)) { return boxes; } @@ -520,12 +520,6 @@ function allBoxesContainingPoint( return boxes; } -function containsPoint(rect: rect, x: number, y: number) { - if (rect) { - return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; - } -} - function scrollIntoViewIfNeccesary( { top, bottom, scrolling }: DragDropRect, x: number, diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts index 7311452e3..f0f84b5d8 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/Draggable.ts @@ -1,5 +1,5 @@ +import { rect } from "@finos/vuu-utils"; import { ReactElement } from "react"; -import { rect } from "../common-types"; import { LayoutModel } from "../layout-reducer"; import { findTarget, followPath, getProps } from "../utils"; import { BoxModel, Measurements, Position } from "./BoxModel"; diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts index e0734ce9e..e4a6d3c96 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/DropTarget.ts @@ -1,4 +1,4 @@ -import { rect, rectTuple } from "../common-types"; +import { rect, rectTuple } from "@finos/vuu-utils"; import { LayoutModel } from "../layout-reducer"; import { getProps, typeOf } from "../utils"; import { diff --git a/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts b/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts index cba6fd2d3..5a47f9c8a 100644 --- a/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/drag-drop/dragDropTypes.ts @@ -1,4 +1,4 @@ -import type { rect } from '../common-types'; +import type { rect } from "@finos/vuu-utils"; export interface DragDropRect extends rect { children?: DragDropRect[]; header?: { @@ -28,7 +28,7 @@ export interface DropPosition { West: boolean; } -export type RelativePosition = 'after' | 'before'; +export type RelativePosition = "after" | "before"; export type DropPosTab = { index: number; diff --git a/vuu-ui/packages/vuu-layout/src/index.ts b/vuu-ui/packages/vuu-layout/src/index.ts index e533d5162..e8fb4cef9 100644 --- a/vuu-ui/packages/vuu-layout/src/index.ts +++ b/vuu-ui/packages/vuu-layout/src/index.ts @@ -1,5 +1,4 @@ export * from "./dock-layout"; -export * from "./common-types"; export { default as Component } from "./Component"; export * from "./drag-drop"; export * from "./DraggableLayout"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts index ea553c6ae..f9e555000 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/flexUtils.ts @@ -1,6 +1,5 @@ -import { uuid } from "@finos/vuu-utils"; +import { dimension, rect, rectTuple, uuid } from "@finos/vuu-utils"; import React, { CSSProperties, ReactElement, ReactNode } from "react"; -import { dimension, rect, rectTuple } from "../common-types"; import { DropPos } from "../drag-drop/dragDropTypes"; import { ComponentRegistry } from "../registry/ComponentRegistry"; import { getProps, resetPath } from "../utils"; @@ -91,13 +90,14 @@ export function getFlexStyle( } export function hasUnboundedFlexStyle(component: ReactElement) { - const { style: { flex, flexGrow, flexShrink, flexBasis } = NO_STYLE } = component.props; + const { style: { flex, flexGrow, flexShrink, flexBasis } = NO_STYLE } = + component.props; if (typeof flex === "number") { return true; - } + } if (flexBasis === 0 && flexGrow === 1 && flexShrink === 1) { return true; - } + } if (typeof flexBasis === "number") { return false; } @@ -212,11 +212,14 @@ export function wrapIntrinsicSizeComponentWithFlexbox( ); } -const getFlexValue = (flexBasis: number, flexFill: boolean): number | undefined => { +const getFlexValue = ( + flexBasis: number, + flexFill: boolean +): number | undefined => { if (flexFill) { return undefined; } - return flexBasis === 0 ? 1 : 0 + return flexBasis === 0 ? 1 : 0; }; export function createFlexbox( diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts index 792c76304..ad4840702 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/insert-layout-element.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { uuid } from "@finos/vuu-utils"; +import { rectTuple, uuid } from "@finos/vuu-utils"; import React, { ReactElement } from "react"; -import { rectTuple } from "../common-types"; import { DropPos } from "../drag-drop"; import { DropTarget } from "../drag-drop/DropTarget"; import { getProp, getProps, nextStep, resetPath, typeOf } from "../utils"; @@ -11,7 +10,7 @@ import { getFlexDimensions, getFlexOrIntrinsicStyle, getIntrinsicSize, - wrapIntrinsicSizeComponentWithFlexbox + wrapIntrinsicSizeComponentWithFlexbox, } from "./flexUtils"; import { LayoutModel } from "./layoutTypes"; import { getManagedDimension, LayoutProps } from "./layoutUtils"; @@ -325,12 +324,14 @@ function getStyledComponents( newComponent: ReactElement, targetRect: DropTarget["clientRect"] ): [ReactElement, ReactElement] { - const id = uuid() + const id = uuid(); let { version = 0 } = getProps(newComponent); version += 1; if (typeOf(container) === "Flexbox") { const [dim] = getManagedDimension(container.props.style); const splitterSize = 6; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const size = { [dim]: (targetRect[dim] - splitterSize) / 2 }; const existingComponentStyle = getFlexOrIntrinsicStyle( existingComponent, diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts index 16edd1ea0..03f63e8d8 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { uuid } from "@finos/vuu-utils"; +import { dimension, uuid } from "@finos/vuu-utils"; import React, { cloneElement, CSSProperties, ReactElement } from "react"; -import { dimension } from "../common-types"; import { ComponentWithId, ComponentRegistry, diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts index b92430948..b8d6e534e 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/resize-flex-children.ts @@ -1,5 +1,5 @@ +import { dimension } from "@finos/vuu-utils"; import React, { CSSProperties, ReactElement } from "react"; -import { dimension } from "../common-types"; import { followPath, getProps } from "../utils"; import { LayoutResizeAction, SplitterResizeAction } from "./layoutTypes"; import { swapChild } from "./replace-layout-element"; diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts index f98d0aa03..98521e537 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/wrap-layout-element.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { uuid } from "@finos/vuu-utils"; +import { rectTuple, uuid } from "@finos/vuu-utils"; import React, { ReactElement } from "react"; -import { rectTuple } from "../common-types"; import { DropPos } from "../drag-drop/dragDropTypes"; import { DropTarget } from "../drag-drop/DropTarget"; import { ComponentRegistry } from "../registry/ComponentRegistry"; @@ -12,7 +11,7 @@ import { flexDirection, getFlexStyle, getIntrinsicSize, - wrapIntrinsicSizeComponentWithFlexbox + wrapIntrinsicSizeComponentWithFlexbox, } from "./flexUtils"; import { LayoutModel } from "./layoutTypes"; import { applyLayoutProps, LayoutProps } from "./layoutUtils"; diff --git a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css index 589a0b32a..73e43da2b 100644 --- a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css +++ b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.css @@ -3,7 +3,7 @@ --overflow-direction: row; --overflow-width: 0px; --border-size: calc((var(--overflow-container-height) - 24px) / 2); - background-color: var(--vuuOverflowContainer-background, black); + background-color: var(--vuuOverflowContainer-background); height: var(--overflow-container-height); } diff --git a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx index b92fa4457..198083a62 100644 --- a/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx +++ b/vuu-ui/packages/vuu-layout/src/overflow-container/OverflowContainer.tsx @@ -57,6 +57,10 @@ const WrapContainer = React.memo( orientation, }); + console.log( + `Overflow container WRAPPER ${React.Children.count(children)} children` + ); + const height = orientation === "vertical" ? "100%" : `${heightProp}px`; // TODO measure the height, if not provided const style = { @@ -137,6 +141,9 @@ export const OverflowContainer = forwardRef(function OverflowContainer( forwardedRef: ForwardedRef ) { const id = useId(idProp); + + console.log(`Overflow container ${React.Children.count(children)} children`); + return (
{ for (const entry of entries) { - const { [sizeProp]: size } = entry.contentRect; + const { [sizeProp]: actualSize } = entry.contentRect; + // This is important. Sometimes tiny sub-pixel differeces + // can be reported, which break the layout assumptions + const size = Math.round(actualSize as number); if (isValidNumber(size) && currentSize !== size) { currentSize = size; handleResize(); diff --git a/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx b/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx index 1398397b2..5edac67f3 100644 --- a/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx +++ b/vuu-ui/packages/vuu-layout/src/palette/Palette.tsx @@ -54,7 +54,10 @@ export const PaletteItem = memo( PaletteItem.displayName = "PaletteItem"; export interface PaletteProps - extends Omit, "onSelect"> { + extends Omit< + HTMLAttributes, + "onDragStart" | "onDrop" | "onSelect" + > { children: ReactElement[]; itemHeight?: number; orientation: "horizontal" | "vertical"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx index 99f498eee..3eefb40bf 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx @@ -3,7 +3,10 @@ import { TableProps } from "@finos/vuu-table"; import { isGroupColumn, metadataKeys, notHidden } from "@finos/vuu-utils"; import cx from "classnames"; import { CSSProperties, useEffect, useRef } from "react"; -import { GroupHeaderCell, HeaderCell } from "./header-cell"; +import { + GroupHeaderCellNext as GroupHeaderCell, + HeaderCell, +} from "./header-cell"; import { Row as DefaultRow } from "./Row"; import { useTable } from "./useTableNext"; import { MeasuredContainer, useId } from "@finos/vuu-layout"; diff --git a/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx b/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx index 99090d91e..9be31ded7 100644 --- a/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx +++ b/vuu-ui/packages/vuu-table/src/table-next/header-cell/GroupHeaderCellNext.tsx @@ -8,12 +8,23 @@ import { ColumnResizer, useTableColumnResize } from "../column-resizing"; import { HeaderCellProps } from "./HeaderCell"; import { useCell } from "../useCell"; import { ColumnHeaderPill, GroupColumnPill } from "../column-header-pill"; -import { OverflowContainer } from "@finos/vuu-layout"; +import { OverflowContainer, useLayoutEffectSkipFirst } from "@finos/vuu-layout"; import "./GroupHeaderCell.css"; const classBase = "vuuTableNextGroupHeaderCell"; +const switchIfChanged = ( + columns: KeyedColumnDescriptor[], + newColumns: KeyedColumnDescriptor[] +) => { + if (columns === newColumns) { + return columns; + } else { + return newColumns; + } +}; + export interface GroupHeaderCellNextProps extends Omit { column: GroupColumnDescriptor; @@ -27,7 +38,6 @@ export const GroupHeaderCellNext = ({ onResize, ...htmlAttributes }: GroupHeaderCellNextProps) => { - console.log({ groupColumn }); const rootRef = useRef(null); const { isResizing, ...resizeProps } = useTableColumnResize({ column: groupColumn, @@ -36,9 +46,7 @@ export const GroupHeaderCellNext = ({ }); const [columns, setColumns] = useState(groupColumn.columns); - const { className, style } = useCell(groupColumn, classBase, true); - const columnPillProps = columns.length > 1 ? { @@ -47,8 +55,7 @@ export const GroupHeaderCellNext = ({ } : undefined; - const handleDrop = useCallback((fromIndex, toIndex) => { - console.log(`handle drop from ${fromIndex} to ${toIndex}`); + const handleMoveItem = useCallback((fromIndex, toIndex) => { setColumns((cols) => { const newCols = cols.slice(); const [tab] = newCols.splice(fromIndex, 1); @@ -61,6 +68,10 @@ export const GroupHeaderCellNext = ({ }); }, []); + useLayoutEffectSkipFirst(() => { + setColumns((cols) => switchIfChanged(cols, groupColumn.columns)); + }, [groupColumn.columns]); + return (
{columns.map((column) => { diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx index 1f5e6bd6e..70f761135 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropProvider.tsx @@ -5,6 +5,9 @@ import React, { useContext, useMemo, } from "react"; +import { DragDropState } from "./DragDropState"; +import { MouseOffset } from "./dragDropTypesNext"; +import { ResumeDragHandler, useGlobalDragDrop } from "./useGlobalDragDrop"; const NO_DRAG_CONTEXT = { isDragSource: false, @@ -15,10 +18,24 @@ const NO_DRAG_CONTEXT = { const unconfiguredRegistrationCall = () => console.log(`have you forgotten to provide a DragDrop Provider ?`); +export type DragOutHandler = ( + id: string, + dragDropState: DragDropState +) => boolean; + +export type DragDropRegistrationFn = ( + id: string, + resumeDrag?: ResumeDragHandler +) => void; + +export type EndOfDragOperationHandler = (id: string) => void; + export interface DragDropContextProps { dragSources?: Map; dropTargets?: Map; - registerDragDropParty: (id: string) => void; + onDragOut?: DragOutHandler; + onEndOfDragOperation?: EndOfDragOperationHandler; + registerDragDropParty: DragDropRegistrationFn; } const DragDropContext = createContext({ @@ -31,12 +48,50 @@ export interface DragDropProviderProps { dragSources: DragSources; } +export type MeasuredTarget = { + bottom: number; + left: number; + right: number; + top: number; +}; + +const measureDropTargets = (dropTargetIds: string[] = []) => { + return dropTargetIds.reduce>((map, id) => { + const el = document.getElementById(id); + if (el) { + const { top, right, bottom, left } = el.getBoundingClientRect(); + map[id] = { top, right, bottom, left }; + } + return map; + }, {}); +}; + export const DragDropProvider = ({ children, dragSources: dragSourcesProp, }: DragDropProviderProps) => { + const resumeDragHandlers = useMemo( + () => new Map(), + [] + ); + const handleDragOverDropTarget = useCallback( + (dropTargetId: string, dragDropState: DragDropState) => { + const resumeDrag = resumeDragHandlers.get(dropTargetId); + if (resumeDrag) { + return resumeDrag(dragDropState); + } else { + return false; + } + }, + [resumeDragHandlers] + ); + + const { measuredDropTargetsRef, resumeDrag } = useGlobalDragDrop({ + onDragOverDropTarget: handleDragOverDropTarget, + }); const [dragSources, dropTargets] = useMemo(() => { const sources = new Map(); + // TODO do we need the targets ? const targets = new Map(); for (const [sourceId, { dropTargets }] of Object.entries(dragSourcesProp)) { @@ -66,17 +121,44 @@ export const DragDropProvider = ({ dropTargets, }); - const registerDragDropParty = useCallback((id: string) => { - console.log(`registerDragDropParty ${id}`); + const onDragOut = useCallback( + (id, dragDropState) => { + // we call releaseItem if and when the dragged item is dropped onto a remote dropTarget + measuredDropTargetsRef.current = measureDropTargets(dragSources.get(id)); + resumeDrag(dragDropState); + return true; + }, + [dragSources, measuredDropTargetsRef, resumeDrag] + ); + + const onEndOfDragOperation = useCallback((id) => { + console.log(`end of drag operation, id= ${id}`); }, []); + const registerDragDropParty = useCallback( + (id, resumeDrag) => { + if (resumeDrag) { + resumeDragHandlers.set(id, resumeDrag); + } + }, + [resumeDragHandlers] + ); + const contextValue: DragDropContextProps = useMemo( () => ({ dragSources, dropTargets, + onDragOut, + onEndOfDragOperation, registerDragDropParty, }), - [dragSources, dropTargets, registerDragDropParty] + [ + dragSources, + dropTargets, + onDragOut, + onEndOfDragOperation, + registerDragDropParty, + ] ); return ( @@ -89,12 +171,19 @@ export const DragDropProvider = ({ export interface DragDropProviderResult { isDragSource: boolean; isDropTarget: boolean; - register: (id: string) => void; + onDragOut?: DragOutHandler; + onEndOfDragOperation?: (id: string) => void; + register: DragDropRegistrationFn; } export const useDragDropProvider = (id?: string): DragDropProviderResult => { - const { dragSources, dropTargets, registerDragDropParty } = - useContext(DragDropContext); + const { + dragSources, + dropTargets, + onDragOut, + onEndOfDragOperation, + registerDragDropParty, + } = useContext(DragDropContext); if (id) { const isDragSource = dragSources?.has(id) ?? false; const isDropTarget = dropTargets?.has(id) ?? false; @@ -102,6 +191,8 @@ export const useDragDropProvider = (id?: string): DragDropProviderResult => { return { isDragSource, isDropTarget, + onDragOut, + onEndOfDragOperation, register: registerDragDropParty, }; } else { diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts new file mode 100644 index 000000000..bd605ec06 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts @@ -0,0 +1,36 @@ +import { MouseOffset } from "./dragDropTypesNext"; + +export class DragDropState { + /** Distance between start (top | left) of dragged element and point where user pressed to drag */ + readonly mouseOffset: MouseOffset; + /** Element where the initial mousedown triggered the drag operation */ + readonly initialDragElement: HTMLElement; + /** Element being dragged, (initial element cloned and rendered in portal). */ + draggableElement: HTMLElement | null = null; + + payload: unknown = null; + + constructor(evt: MouseEvent, dragElement: HTMLElement) { + this.initialDragElement = dragElement; + this.mouseOffset = this.getMouseOffset(evt, dragElement); + } + + /** Used to capture a ref to the Draggable JSX.Element */ + setDraggable = (el: HTMLElement) => { + this.draggableElement = el; + }; + + setPayload(payload: unknown) { + this.payload = payload; + } + + private getMouseOffset(evt: MouseEvent, dragElement: HTMLElement) { + const { clientX, clientY } = evt; + const draggableRect = dragElement.getBoundingClientRect(); + + return { + x: clientX - draggableRect.left, + y: clientY - draggableRect.top, + }; + } +} diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drag-utils.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drag-utils.ts deleted file mode 100644 index eb3d2cb0d..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drag-utils.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { orientationType } from "@finos/vuu-utils"; -import { Direction, FWD } from "./dragDropTypesNext"; - -const LEFT_RIGHT = ["left", "right"]; -const TOP_BOTTOM = ["top", "bottom"]; -// duplicated in repsonsive - -export type MeasuredDropTarget = { - currentIndex: number; - dataIndex?: number; - element: HTMLElement; - index: number; - order: number | undefined; - isDraggedElement: boolean; - isOverflowIndicator?: boolean; - start: number; - end: number; - mid: number; - size: number; -}; - -export type targetType = { - element: HTMLElement | null; - index: number; - isLast?: boolean; -}; - -type MousePosKey = keyof Pick; -type DOMRectKey = keyof Omit; -type DOMRectDimensionKey = keyof Pick; -type Dimension = keyof Pick; -type ElementDimension = keyof Pick< - HTMLElement, - | "scrollHeight" - | "scrollWidth" - | "clientHeight" - | "clientWidth" - | "scrollTop" - | "scrollLeft" ->; - -export const measureElementSizePositionAndOrder = ( - element: HTMLElement, - dimension: Dimension = "width", - includeAutoMargin = false -): [number, number, number | undefined] => { - const pos = dimension === "width" ? "left" : "top"; - const { [dimension]: size, [pos]: position } = - element.getBoundingClientRect(); - const { padEnd = false, padStart = false } = element.dataset; - const style = getComputedStyle(element); - const [start, end] = dimension === "width" ? LEFT_RIGHT : TOP_BOTTOM; - const marginStart = - padStart && !includeAutoMargin - ? 0 - : parseInt(style.getPropertyValue(`margin-${start}`), 10); - const marginEnd = - padEnd && !includeAutoMargin - ? 0 - : parseInt(style.getPropertyValue(`margin-${end}`), 10); - const order = parseInt(style.getPropertyValue("order")); - - let minWidth = size; - const flexShrink = parseInt(style.getPropertyValue("flex-shrink"), 10); - if (flexShrink > 0) { - const flexBasis = parseInt(style.getPropertyValue("flex-basis"), 10); - if (!isNaN(flexBasis) && flexBasis > 0) { - minWidth = flexBasis; - } - } - return [ - position, - marginStart + minWidth + marginEnd, - isNaN(order) ? undefined : order, - ]; -}; - -const DIMENSIONS = { - horizontal: { - CLIENT_SIZE: "clientWidth" as ElementDimension, - CONTRA: "top" as DOMRectKey, - CONTRA_POS: "clientY" as MousePosKey, - DIMENSION: "width" as DOMRectDimensionKey, - END: "right" as DOMRectKey, - POS: "clientX" as MousePosKey, - SCROLL_POS: "scrollTop" as ElementDimension, - SCROLL_SIZE: "scrollWidth" as ElementDimension, - START: "left" as DOMRectKey, - }, - vertical: { - CLIENT_SIZE: "clientHeight" as ElementDimension, - CONTRA: "left" as DOMRectKey, - CONTRA_POS: "clientX" as MousePosKey, - DIMENSION: "height" as DOMRectDimensionKey, - END: "bottom" as DOMRectKey, - POS: "clientY" as MousePosKey, - SCROLL_POS: "scrollLeft" as ElementDimension, - SCROLL_SIZE: "scrollHeight" as ElementDimension, - START: "top" as DOMRectKey, - }, -}; -export const dimensions = (orientation: orientationType) => - DIMENSIONS[orientation]; - -export const getDraggedItem = ( - measuredItems: MeasuredDropTarget[] -): MeasuredDropTarget => { - const result = measuredItems.find((item) => item.isDraggedElement); - if (result) { - return result; - } else { - throw Error("measuredItems do not contain a draggedElement"); - } -}; - -export const moveDragItem = ( - measuredItems: MeasuredDropTarget[], - dropTarget: MeasuredDropTarget -): MeasuredDropTarget[] => { - const items: MeasuredDropTarget[] = measuredItems.slice(); - const draggedItem = getDraggedItem(items); - const draggedIndex = items.indexOf(draggedItem!); - const targetIndex = items.indexOf(dropTarget); - - const firstPos = Math.min(draggedIndex, targetIndex); - const lastPos = Math.max(draggedIndex, targetIndex); - let { start } = items[firstPos]; - - items[draggedIndex] = { ...dropTarget }; - items[targetIndex] = { ...draggedItem }; - - for (let i = firstPos; i <= lastPos; i++) { - const item = items[i]; - item.currentIndex = i; - item.start = start; - item.end = start + item.size; - item.mid = start + item.size / 2; - start = item.end; - } - - return items; -}; - -export const isDraggedElement = (item: MeasuredDropTarget) => - item.isDraggedElement; - -export const measureDropTargets = ( - container: HTMLElement, - orientation: orientationType, - draggedItem: HTMLElement, - itemQuery?: string -) => { - const dragThresholds: MeasuredDropTarget[] = []; - - // TODO need to make sure we're including only the children we should. - const children = Array.from( - itemQuery ? container.querySelectorAll(itemQuery) : container.children - ); - for (let index = 0; index < children.length; index++) { - const element = children[index] as HTMLElement; - const dimension = orientation === "horizontal" ? "width" : "height"; - const [start, size, order] = measureElementSizePositionAndOrder( - element, - dimension - ); - - dragThresholds.push({ - currentIndex: index, - dataIndex: parseInt(element.dataset.index ?? "-1"), - index, - isDraggedElement: element === draggedItem, - isOverflowIndicator: element.dataset.overflowIndicator === "true", - element: element as HTMLElement, - order, - start, - end: start + size, - size, - mid: start + size / 2, - }); - } - return dragThresholds; -}; - -export const getNextDropTarget = ( - dropTargets: MeasuredDropTarget[], - pos: number, - direction: Direction -) => { - const len = dropTargets.length; - if (direction === FWD) { - for (let index = 0; index < len; index++) { - let dropTarget = dropTargets[index]; - const { start, mid, end } = dropTarget; - if (pos > end) { - continue; - } else if (pos > mid) { - return dropTarget.isDraggedElement ? null : dropTarget; - } else if (pos > start) { - dropTarget = dropTargets[index - 1]; - return dropTarget.isDraggedElement ? null : dropTarget; - } - } - } else { - for (let index = len - 1; index >= 0; index--) { - let dropTarget = dropTargets[index]; - const { start, mid, end } = dropTarget; - if (pos < start) { - continue; - } else if (pos < mid) { - return dropTarget.isDraggedElement ? null : dropTarget; - } else if (pos < end) { - dropTarget = dropTargets[Math.min(len - 1, index + 1)]; - return dropTarget.isDraggedElement ? null : dropTarget; - } - } - } - return null; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts index 19697941c..c8e7e1eac 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/dragDropTypesNext.ts @@ -1,5 +1,6 @@ import { MouseEventHandler, RefObject } from "react"; import { orientationType } from "@finos/vuu-utils"; +import { DragDropState } from "./DragDropState"; //----------------------------------- // From useScrollPosition in List @@ -56,7 +57,7 @@ export interface DragHookResult { export interface InternalDragHookResult extends Omit { - beginDrag: (evt: MouseEvent) => void; + beginDrag: (dragElement: HTMLElement) => void; drag: (dragPos: number, mouseMoveDirection: "fwd" | "bwd") => void; drop: () => void; handleScrollStart: () => void; @@ -65,18 +66,39 @@ export interface InternalDragHookResult _scrollPos: number, atEnd: boolean ) => void; + /** + * Draggable item has been dragged out of container. Remove any local drop + * indicators. Dragged element itself should not yet be removed from DOM. + */ + releaseDrag?: () => void; } +export interface DropOptions { + fromIndex?: number; + toIndex: number; + isExternal?: boolean; + payload?: unknown; +} + +export type DragStartHandler = (dragDropState: DragDropState) => void; + +export type DropHandler = ( + fromIndex: number, + toIndex: number, + options: DropOptions +) => void; + export interface DragDropProps { allowDragDrop?: boolean | dragStrategy; /** this is the className that will be assigned during drag to the dragged element */ draggableClassName: string; extendedDropZone?: boolean; + getDragPayload?: (dragElement: HTMLElement) => unknown; id?: string; isDragSource?: boolean; isDropTarget?: boolean; - onDragStart?: () => void; - onDrop: (fromIndex: number, toIndex: number) => void; + onDragStart?: DragStartHandler; + onDrop: DropHandler; onDropSettle?: (toIndex: number) => void; orientation: orientationType; containerRef: RefObject; @@ -89,8 +111,13 @@ export type DragDropHook = (props: DragDropProps) => DragHookResult; export interface InternalDragDropProps extends Omit { - draggableRef: RefObject; isDragSource?: boolean; isDropTarget?: boolean; selected?: unknown; } + +export type DragDropContext = { + dragElement: HTMLElement; + dragPayload: unknown; + mouseOffset: MouseOffset; +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts index 66e79cf83..7abc8f959 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/drop-target-utils.ts @@ -9,17 +9,18 @@ const TOP_BOTTOM = ["top", "bottom"]; export const NOT_OVERFLOWED = ":not(.wrapped)"; export const NOT_HIDDEN = ':not([aria-hidden="true"])'; +// TODO figure out which of these sttributes we no longer need export type MeasuredDropTarget = { /** The index position currently occupied by this item. If draggable is dropped here, this will be the destination drop position. */ currentIndex: number; - dataIndex?: number; element: HTMLElement; id: string; index: number; isDraggedItem: boolean; + isExternal?: boolean; isLast?: boolean; isOverflowIndicator?: boolean; start: number; @@ -180,11 +181,8 @@ export const measureDropTargets = ( const isLast = index === itemCount - 1; const id = element.id; - const dataIndex = parseInt(element.dataset.index ?? "-1"); - dragThresholds.push({ currentIndex: index, - dataIndex: isNaN(dataIndex) ? -1 : dataIndex, id, index, isDraggedItem: draggedItemId === id, @@ -214,8 +212,8 @@ export const getIndexOfDraggedItem = ( absoluteIndex = false ) => { const indexOfDraggedItem = dropTargets.findIndex((d) => d.isDraggedItem); - const { index: draggedItemOriginalIndex } = dropTargets[indexOfDraggedItem]; if (absoluteIndex) { + const { index: draggedItemOriginalIndex } = dropTargets[indexOfDraggedItem]; const minIndex = dropTargets .filter((d) => !d.isDraggedItem) .reduce((min, d) => Math.min(min, d.index), Number.MAX_SAFE_INTEGER); @@ -234,9 +232,6 @@ export const mutateDropTargetsSwitchDropTargetPosition = ( dropTargets: MeasuredDropTarget[], direction: Direction ) => { - // console.log(`switchDropTargetPosition - // direction: ${direction} ${dropTargetsDebugString(dropTargets)}`); - const indexOfDraggedItem = getIndexOfDraggedItem(dropTargets); const indexOfTarget = direction === "fwd" ? indexOfDraggedItem + 1 : indexOfDraggedItem - 1; @@ -293,25 +288,24 @@ export const mutateDropTargetsSwitchDropTargetPosition = ( } as MeasuredDropTarget; dropTargets.splice(indexOfTarget, 2, newDraggedItem, newTargetItem); } - - // console.log(`${direction} ${dropTargetsDebugString(dropTargets)}`); }; export const getNextDropTarget = ( dropTargets: MeasuredDropTarget[], pos: number, + draggedItemSize: number, mouseMoveDirection: Direction ): MeasuredDropTarget => { const len = dropTargets.length; const indexOfDraggedItem = getIndexOfDraggedItem(dropTargets); + // draggedItem will be undefined if we are handling an external drag const draggedItem = dropTargets[indexOfDraggedItem]; - if (mouseMoveDirection === "fwd") { - const leadingEdge = Math.round(pos + draggedItem.size); + const leadingEdge = Math.round(pos + draggedItemSize); for (let index = len - 1; index >= 0; index--) { const dropTarget = dropTargets[index]; if (leadingEdge > dropTarget.mid) { - if (index < indexOfDraggedItem) { + if (draggedItem && index < indexOfDraggedItem) { return draggedItem; } else { return dropTarget; @@ -358,6 +352,6 @@ export const dropTargetsDebugString = (dropTargets: MeasuredDropTarget[]) => d.size )} ${Math.floor(d.start)} - ${Math.floor(d.end)} (mid ${Math.floor( d.mid - )})` + )}) ${d.element?.textContent} ` ) .join(""); diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts index 3755730fb..9bea71262 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDisplacers.ts @@ -5,6 +5,7 @@ import { } from "./drop-target-utils"; import { createDragSpacer as createDragDisplacer } from "./Draggable"; import { Direction } from "./dragDropTypesNext"; +import { orientationType } from "packages/vuu-utils/src"; export type DragDisplacersHookResult = { displaceItem: ( @@ -23,16 +24,20 @@ export type DragDisplacersHookResult = { direction?: Direction | "static", orientation?: "horizontal" | "vertical" ) => void; - clearSpacers: () => void; + clearSpacers: (useAnimation?: boolean) => void; }; -export type DragDisplacersHook = () => DragDisplacersHookResult; +export type DragDisplacersHook = ( + orientation: orientationType +) => DragDisplacersHookResult; /** * Manage a pair of displacer elements to smoothly display a moving gap between * list items of any kind. Designed to be used in a drag drop operation. The 'static' * direction option should be used at drag start or following scroll. */ -export const useDragDisplacers: DragDisplacersHook = () => { +export const useDragDisplacers: DragDisplacersHook = ( + orientation = "horizontal" +) => { const animationFrame = useRef(0); const transitioning = useRef(false); @@ -42,11 +47,6 @@ export const useDragDisplacers: DragDisplacersHook = () => { [] ); - const clearSpacers = useCallback( - () => spacers.forEach((spacer) => spacer.remove()), - [spacers] - ); - const animateTransition = useCallback( (size: number, propertyName = "width") => { const [spacer1, spacer2] = spacers; @@ -61,6 +61,24 @@ export const useDragDisplacers: DragDisplacersHook = () => { [spacers] ); + const clearSpacers = useCallback( + (useTransition = false) => { + if (useTransition === true) { + const [spacer] = spacers; + const cleanup = () => { + spacer.removeEventListener("transitionend", cleanup); + clearSpacers(); + }; + const propertyName = orientation === "horizontal" ? "width" : "height"; + spacer.addEventListener("transitionend", cleanup); + animateTransition(0, propertyName); + } else { + spacers.forEach((spacer) => spacer.remove()); + } + }, + [animateTransition, orientation, spacers] + ); + const cancelAnyPendingAnimation = useCallback(() => { if (animationFrame.current) { cancelAnimationFrame(animationFrame.current); @@ -74,8 +92,7 @@ export const useDragDisplacers: DragDisplacersHook = () => { dropTarget: MeasuredDropTarget, size: number, useTransition = false, - direction: Direction | "static" = "static", - orientation: "horizontal" | "vertical" = "horizontal" + direction: Direction | "static" = "static" ) => { if (dropTarget) { const propertyName = orientation === "horizontal" ? "width" : "height"; @@ -114,7 +131,13 @@ export const useDragDisplacers: DragDisplacersHook = () => { } } }, - [animateTransition, cancelAnyPendingAnimation, clearSpacers, spacers] + [ + animateTransition, + cancelAnyPendingAnimation, + clearSpacers, + orientation, + spacers, + ] ); const displaceLastItem = useCallback( ( @@ -122,8 +145,7 @@ export const useDragDisplacers: DragDisplacersHook = () => { dropTarget: MeasuredDropTarget, size: number, useTransition = false, - direction: Direction | "static" = "static", - orientation: "horizontal" | "vertical" = "horizontal" + direction: Direction | "static" = "static" ) => { const propertyName = orientation === "horizontal" ? "width" : "height"; const [spacer1, spacer2] = spacers; @@ -154,7 +176,13 @@ export const useDragDisplacers: DragDisplacersHook = () => { mutateDropTargetsSwitchDropTargetPosition(dropTargets, direction); } }, - [animateTransition, cancelAnyPendingAnimation, clearSpacers, spacers] + [ + animateTransition, + cancelAnyPendingAnimation, + clearSpacers, + orientation, + spacers, + ] ); return { diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx index 1a9014640..478fe90d6 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropIndicator.tsx @@ -24,7 +24,6 @@ const NOT_OVERFLOWED = ':not([data-overflowed="true"])'; const NOT_HIDDEN = ':not([aria-hidden="true"])'; export const useDragDropIndicator = ({ - draggableRef, onDrop, orientation = "horizontal", containerRef, @@ -94,7 +93,12 @@ export const useDragDropIndicator = ({ const dragPos = dragPosRef.current; const midPos = dragPos + size / 2; const { current: dropTargets } = measuredDropTargets; - const nextDropTarget = getNextDropTarget(dropTargets, midPos, "fwd"); + const nextDropTarget = getNextDropTarget( + dropTargets, + midPos, + size, + "fwd" + ); if (nextDropTarget) { if (atEnd && scrollDirection === "fwd") { positionDropIndicator(dropTargets[dropTargets.length - 1], "start"); @@ -116,9 +120,7 @@ export const useDragDropIndicator = ({ ); const beginDrag = useCallback( - (evt: MouseEvent) => { - const evtTarget = evt.target as HTMLElement; - const dragElement = evtTarget.closest(itemQuery) as HTMLElement; + (dragElement: HTMLElement) => { if ( dragElement.ariaSelected && Array.isArray(selected) && @@ -204,13 +206,11 @@ export const useDragDropIndicator = ({ } }, [ - itemQuery, selected, containerRef, orientation, fullItemQuery, viewportRange, - // setVizData, positionDropIndicator, ] ); @@ -221,7 +221,7 @@ export const useDragDropIndicator = ({ const { current: draggedItem } = draggedItemRef; if (draggedItem) { - if (draggableRef.current && containerRef.current) { + if (containerRef.current) { const START = orientation === "horizontal" ? "left" : "top"; dragPosRef.current = dragPos; @@ -229,6 +229,7 @@ export const useDragDropIndicator = ({ const nextDropTarget = getNextDropTarget( dropTargets, dragPos, + draggedItem.size, mouseMoveDirection ); @@ -277,7 +278,7 @@ export const useDragDropIndicator = ({ } } }, - [draggableRef, containerRef, orientation, positionDropIndicator] + [containerRef, orientation, positionDropIndicator] ); const drop = useCallback(() => { @@ -301,17 +302,32 @@ export const useDragDropIndicator = ({ //TODO why is this different from Natural Movement ? if (overflowMenuShowingRef.current) { - onDrop(fromIndex, -1); + onDrop(fromIndex, -1, { + fromIndex, + roIndex: -1, + }); } else { if (fromIndex < originalDropTargetIndex) { onDrop( fromIndex, - dropBefore ? currentDropTargetIndex : currentDropTargetIndex + 1 + dropBefore ? currentDropTargetIndex : currentDropTargetIndex + 1, + { + fromIndex, + toIndex: dropBefore + ? currentDropTargetIndex + : currentDropTargetIndex + 1, + } ); } else { onDrop( fromIndex, - dropBefore ? originalDropTargetIndex : originalDropTargetIndex + 1 + dropBefore ? originalDropTargetIndex : originalDropTargetIndex + 1, + { + fromIndex, + toIndex: dropBefore + ? originalDropTargetIndex + : originalDropTargetIndex + 1, + } ); } } @@ -320,6 +336,10 @@ export const useDragDropIndicator = ({ setShowOverflow(false); }, [clearSpacer, onDrop]); + const releaseDrag = useCallback(() => { + // TODO + }, []); + return { beginDrag, drag, @@ -327,6 +347,7 @@ export const useDragDropIndicator = ({ dropIndicator, handleScrollStart, handleScrollStop, + releaseDrag, revealOverflowedItems: showOverflow, }; }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx index 69874974b..5c921c713 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNaturalMovementNext.tsx @@ -10,6 +10,7 @@ import { useDragDisplacers } from "./useDragDisplacers"; import { dispatchMouseEvent } from "@finos/vuu-utils"; import { dimensions, + dropTargetsDebugString, getIndexOfDraggedItem, getNextDropTarget, MeasuredDropTarget, @@ -19,7 +20,6 @@ import { } from "./drop-target-utils"; export const useDragDropNaturalMovement = ({ - draggableRef, onDrop, orientation = "horizontal", containerRef, @@ -37,7 +37,8 @@ export const useDragDropNaturalMovement = ({ const [showOverflow, setShowOverflow] = useState(false); - const { clearSpacers, displaceItem, displaceLastItem } = useDragDisplacers(); + const { clearSpacers, displaceItem, displaceLastItem } = + useDragDisplacers(orientation); const draggedItemRef = useRef(); const fullItemQuery = `:is(${itemQuery}${NOT_OVERFLOWED}${NOT_HIDDEN},.vuuOverflowContainer-OverflowIndicator)`; @@ -73,13 +74,11 @@ export const useDragDropNaturalMovement = ({ measuredDropTargets.current.unshift(draggedItem); } - // setVizData?.(measuredDropTargets.current); - const { size } = draggedItem; const dragPos = dragPosRef.current; const midPos = dragPos + size / 2; const { current: dropTargets } = measuredDropTargets; - const dropTarget = getNextDropTarget(dropTargets, midPos, "fwd"); + const dropTarget = getNextDropTarget(dropTargets, midPos, size, "fwd"); if (dropTarget) { const targetIndex = indexOf(dropTarget); @@ -92,41 +91,19 @@ export const useDragDropNaturalMovement = ({ dropTargets[dropTargets.length - 1], size, false, - "static", - orientation + "static" ); } else { - displaceItem( - dropTargets, - nextDropTarget, - size, - true, - "static", - orientation - ); + displaceItem(dropTargets, nextDropTarget, size, true, "static"); } - // setVizData?.( - // measuredDropTargets.current, - // nextDropTarget, - // dropZoneRef.current - // ); } } }, - [ - containerRef, - displaceItem, - displaceLastItem, - fullItemQuery, - orientation, - // setVizData, - ] + [containerRef, displaceItem, displaceLastItem, fullItemQuery, orientation] ); const beginDrag = useCallback( - (evt: MouseEvent) => { - const evtTarget = evt.target as HTMLElement; - const dragElement = evtTarget.closest(itemQuery) as HTMLElement; + (dragElement: HTMLElement) => { if ( //TODO need a different check for selected dragElement.ariaSelected && @@ -137,9 +114,9 @@ export const useDragDropNaturalMovement = ({ } const { current: container } = containerRef; if (container && dragElement) { + const internalDrag = container.contains(dragElement); const { SCROLL_SIZE, CLIENT_SIZE } = dimensions(orientation); const { id: draggedItemId } = dragElement; - const { [SCROLL_SIZE]: scrollSize, [CLIENT_SIZE]: clientSize } = container; isScrollable.current = scrollSize > clientSize; @@ -151,29 +128,65 @@ export const useDragDropNaturalMovement = ({ draggedItemId )); - console.log({ dropTargets }); - - const indexOfDraggedItem = getIndexOfDraggedItem(dropTargets); - const draggedItem = dropTargets[indexOfDraggedItem]; + if (internalDrag) { + console.log(dropTargetsDebugString(dropTargets)); + const indexOfDraggedItem = getIndexOfDraggedItem(dropTargets); + const draggedItem = dropTargets[indexOfDraggedItem]; + if (draggedItem && container) { + draggedItemRef.current = draggedItem; + const displaceFunction = draggedItem.isLast + ? displaceLastItem + : displaceItem; + displaceFunction( + dropTargets, + draggedItem, + draggedItem.size, + false, + "static" + ); + } + } else { + // prettier-ignore + const { top: dragPos, height: size } = dragElement.getBoundingClientRect(); + // prettier-ignore + const dropTarget = getNextDropTarget( dropTargets, dragPos, size, "fwd"); + const index = dropTargets.indexOf(dropTarget); + const { start, end, mid } = dropTarget; + + console.log(`nextDropTarget ${dropTarget.element.textContent}`); + + // need to compute the correct position of this + const draggedItem = (draggedItemRef.current = { + end, + mid, + start, + isDraggedItem: true, + isExternal: true, + size, + }); + + const indexOfDropTarget = dropTargets.indexOf(dropTarget); + console.log({ indexOfDropTarget }); + dropTargets.splice(indexOfDropTarget, 0, draggedItem); + for (let i = index + 1; i < dropTargets.length; i++) { + const target = dropTargets[i]; + target.mid += size; + target.end += size; + target.start += size; + } - if (draggedItem && container) { - draggedItemRef.current = draggedItem; + console.log(dropTargetsDebugString(dropTargets)); - const displaceFunction = draggedItem.isLast + const displaceFunction = dropTarget.isLast ? displaceLastItem : displaceItem; - // setVizData?.(dropTargets, displacedItem, dropZone); - - console.log({ indexOfDraggedItem, draggedItem }); - displaceFunction( dropTargets, - draggedItem, - draggedItem.size, - false, - "static", - orientation + dropTarget, + dropTarget.size, + true, + "static" ); } } @@ -183,10 +196,8 @@ export const useDragDropNaturalMovement = ({ displaceItem, displaceLastItem, fullItemQuery, - itemQuery, orientation, selected, - // setVizData, viewportRange, ] ); @@ -225,13 +236,14 @@ export const useDragDropNaturalMovement = ({ const { current: draggedItem } = draggedItemRef; if (draggedItem) { - if (draggableRef.current && containerRef.current) { + if (containerRef.current) { dragPosRef.current = dragPos; const { current: dropTargets } = measuredDropTargets; const nextDropTarget = getNextDropTarget( dropTargets, dragPos, + draggedItem.size, mouseMoveDirection ); @@ -254,12 +266,9 @@ export const useDragDropNaturalMovement = ({ nextDropTarget, size, true, - mouseMoveDirection, - orientation + mouseMoveDirection ); - // setVizData?.(dropTargets, nextDropTarget, nextDropZone); - const overflowIndicator = dropTargets.at( -1 ) as MeasuredDropTarget; @@ -272,15 +281,7 @@ export const useDragDropNaturalMovement = ({ } } }, - [ - containerRef, - displaceItem, - displaceLastItem, - draggableRef, - hidePopup, - orientation, - showPopup, - ] + [containerRef, displaceItem, displaceLastItem, hidePopup, showPopup] ); const drop = useCallback(() => { @@ -292,13 +293,21 @@ export const useDragDropNaturalMovement = ({ dragDirectionRef.current = undefined; if (overflowMenuShowingRef.current) { - onDrop(draggedItem.index, -1); + onDrop(draggedItem.index, -1, { + fromIndex: draggedItem.index, + toIndex: -1, + isExternal: draggedItem.isExternal, + }); } else { const absoluteIndexDraggedItem = getIndexOfDraggedItem( dropTargets, true ); - onDrop(draggedItem.index, absoluteIndexDraggedItem); + onDrop(draggedItem.index, absoluteIndexDraggedItem, { + fromIndex: draggedItem.index, + toIndex: absoluteIndexDraggedItem, + isExternal: draggedItem.isExternal, + }); } } setShowOverflow(false); @@ -313,12 +322,17 @@ export const useDragDropNaturalMovement = ({ } }, [clearSpacers, containerRef, onDrop]); + const releaseDrag = useCallback(() => { + clearSpacers(true); + }, [clearSpacers]); + return { beginDrag, drag, drop, handleScrollStart, handleScrollStop, + releaseDrag, revealOverflowedItems: showOverflow, }; }; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx index 0b8064556..9ec50928f 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragDropNext.tsx @@ -1,9 +1,11 @@ import { DragDropHook, + DropHandler, InternalDragDropProps, InternalDragHookResult, MouseOffset, } from "./dragDropTypesNext"; +import { DragDropState } from "./DragDropState"; import { useDragDropNaturalMovement } from "./useDragDropNaturalMovementNext"; import { useDragDropIndicator } from "./useDragDropIndicator"; import { useDragDropProvider } from "./DragDropProvider"; @@ -23,6 +25,7 @@ import { } from "./drop-target-utils"; import { useAutoScroll, ScrollStopHandler } from "./useAutoScroll"; import { Draggable } from "./Draggable"; +import { ResumeDragHandler } from "./useGlobalDragDrop"; const NULL_DRAG_DROP_RESULT = { beginDrag: () => undefined, @@ -84,6 +87,7 @@ export const useDragDropNext: DragDropHook = ({ allowDragDrop, containerRef, draggableClassName, + getDragPayload, id, itemQuery = "*", onDragStart, @@ -103,33 +107,89 @@ export const useDragDropNext: DragDropHook = ({ draggedItemIndex: -1, isDragging: false, }); - // A ref to the draggable element - const draggableRef = useRef(null); - const dragElementRef = useRef(); + + const dragDropStateRef = useRef(null); const mouseDownTimer = useRef(null); /** do we actually have scrollable content */ const isScrollableRef = useRef(false); - /** Distance between start (top | left) of dragged element and point where user pressed to drag */ - const mouseOffsetRef = useRef({ x: 0, y: 0 }); /** current mouse position */ const mousePosRef = useRef({ x: 0, y: 0 }); /** mouse position when mousedown initiated drag */ const startPosRef = useRef({ x: 0, y: 0 }); /** references the dragged Item during its final 'settling' phase post drop */ - const settlingItemRef = useRef(null); + const settlingItemRef = useRef(null); const dropPosRef = useRef(-1); const dropIndexRef = useRef(-1); const handleScrollStopRef = useRef(); - const { isDragSource, isDropTarget, register } = useDragDropProvider(id); - - useEffect(() => { - if (id && (isDragSource || isDropTarget)) { - register(id); + const { + isDragSource, + isDropTarget, + onDragOut, + onEndOfDragOperation, + register, + } = useDragDropProvider(id); + + type NativeMouseHandler = (evt: MouseEvent) => void; + /** refs for drag handlers to avoid circular dependency issues */ + const dragMouseMoveHandlerRef = useRef(); + const dragMouseUpHandlerRef = useRef(); + + const attachDragHandlers = useCallback(() => { + const { current: dragMove } = dragMouseMoveHandlerRef; + const { current: dragUp } = dragMouseUpHandlerRef; + if (dragMove && dragUp) { + // prettier-ignore + document.addEventListener("mousemove", dragMove, false); + document.addEventListener("mouseup", dragUp, false); + } + }, []); + const removeDragHandlers = useCallback(() => { + const { current: dragMove } = dragMouseMoveHandlerRef; + const { current: dragUp } = dragMouseUpHandlerRef; + if (dragMove && dragUp) { + // prettier-ignore + document.removeEventListener("mousemove", dragMove, false); + document.removeEventListener("mouseup", dragUp, false); } - }, [id, isDragSource, isDropTarget, register]); + }, []); + + /** + * Establish the boundaries for the current drag operation. When dragging along + * a single axis (eg list items within a list, tabs within a tabstrip), constrain + * valid drag positions to the confines of the container. A sharp drag away from + * the primary drag axis is interpreted as a request to drag an item out of the + * container. This will be allowed if configured appropriately. + */ + const setDragBoundaries = useCallback( + (containerRect: DOMRect, draggableRect: DOMRect) => { + const { current: container } = containerRef; + if (container) { + const [lastElement, lastItemIsOverflowIndicator] = getLastElement( + container, + itemQuery + ); + const { CONTRA, CONTRA_END, DIMENSION, END, START } = + dimensions(orientation); + + const draggableSize = draggableRect[DIMENSION]; + const { [START]: lastItemStart, [END]: lastItemEnd } = + lastElement.getBoundingClientRect(); + + dragBoundaries.current.start = containerRect[START]; + dragBoundaries.current.end = lastItemIsOverflowIndicator + ? Math.max(lastItemStart, containerRect.right - draggableSize) + : isScrollableRef.current + ? containerRect[START] + containerRect[DIMENSION] - draggableSize + : lastItemEnd - draggableSize; + dragBoundaries.current.contraStart = containerRect[CONTRA]; + dragBoundaries.current.contraEnd = containerRect[CONTRA_END]; + } + }, + [containerRef, itemQuery, orientation] + ); const terminateDrag = useCallback(() => { const { current: toIndex } = dropIndexRef; @@ -150,7 +210,9 @@ export const useDragDropNext: DragDropHook = ({ const getScrollDirection = useCallback( (mousePos: number) => { - if (containerRef.current) { + if (containerRef.current && dragDropStateRef.current) { + const { mouseOffset } = dragDropStateRef.current; + const { POS, SCROLL_POS, SCROLL_SIZE, CLIENT_SIZE } = dimensions(orientation); const { @@ -164,10 +226,8 @@ export const useDragDropNext: DragDropHook = ({ const viewportEnd = dragBoundaries.current.end; const bwd = scrollPos > 0 && - mousePos - mouseOffsetRef.current[POS] <= - dragBoundaries.current.start; - const fwd = - canScrollFwd && mousePos - mouseOffsetRef.current[POS] >= viewportEnd; + mousePos - mouseOffset[POS] <= dragBoundaries.current.start; + const fwd = canScrollFwd && mousePos - mouseOffset[POS] >= viewportEnd; return bwd ? "bwd" : fwd ? "fwd" : ""; } }, @@ -194,14 +254,23 @@ export const useDragDropNext: DragDropHook = ({ orientation, }); - const handleDrop = useCallback( - (fromIndex: number, toIndex: number) => { + const handleDrop = useCallback( + (fromIndex, toIndex, options) => { //TODO why do we need both this and dropIndexRef ? dropPosRef.current = toIndex; - onDrop?.(fromIndex, toIndex); + if (options.isExternal) { + onDrop?.(fromIndex, toIndex, { + ...options, + payload: dragDropStateRef.current?.payload, + }); + } else { + onDrop?.(fromIndex, toIndex, options); + } dropIndexRef.current = toIndex; + onEndOfDragOperation?.(id); + dragDropStateRef.current = null; }, - [onDrop] + [id, onDrop, onEndOfDragOperation] ); const { @@ -210,11 +279,12 @@ export const useDragDropNext: DragDropHook = ({ drop, handleScrollStart, handleScrollStop, + releaseDrag, ...dragResult } = useDragDropHook({ ...dragDropProps, containerRef, - draggableRef, + // draggableRef, isDragSource, isDropTarget, itemQuery, @@ -224,82 +294,105 @@ export const useDragDropNext: DragDropHook = ({ // To avoid circular ref between hooks handleScrollStopRef.current = handleScrollStop; - const dragMouseMoveHandler = useCallback( - (evt: MouseEvent) => { - const { CLIENT_POS, CONTRA_CLIENT_POS, CONTRA_POS, POS } = - dimensions(orientation); - const { clientX, clientY } = evt; - const { [CLIENT_POS]: clientPos, [CONTRA_CLIENT_POS]: clientContraPos } = - evt; - const lastClientPos = mousePosRef.current[POS]; + const dragHandedOvertoProvider = useCallback( + (dragDistance: number, clientContraPos: number) => { + const { CONTRA_POS } = dimensions(orientation); const lastClientContraPos = mousePosRef.current[CONTRA_POS]; - const dragDistance = Math.abs(lastClientPos - clientPos); const dragOutDistance = isDragSource ? Math.abs(lastClientContraPos - clientContraPos) : 0; - if (dragOutDistance - dragDistance > 5) { - console.log("going unbounded"); + if (dragDropStateRef.current && dragOutDistance - dragDistance > 5) { + if (onDragOut?.(id as string, dragDropStateRef.current)) { + // TODO create a cleanup function + removeDragHandlers(); + releaseDrag(); + dragDropStateRef.current = null; + } // remove the drag boundaries dragBoundaries.current = UNBOUNDED; - // Need to notify the dragDropHook, so it can clearSpacers - // and begin tracking draggable coordinates for entry into a droptarget + return true; + } + }, + [id, isDragSource, onDragOut, orientation, removeDragHandlers] + ); + + const dragMouseMoveHandler = useCallback( + (evt: MouseEvent) => { + const { CLIENT_POS, CONTRA_CLIENT_POS, POS } = dimensions(orientation); + const { clientX, clientY } = evt; + const { [CLIENT_POS]: clientPos, [CONTRA_CLIENT_POS]: clientContraPos } = + evt; + const lastClientPos = mousePosRef.current[POS]; + const dragDistance = Math.abs(lastClientPos - clientPos); + const { current: dragDropState } = dragDropStateRef; + + if (dragHandedOvertoProvider(dragDistance, clientContraPos)) { + console.log("drag handed over to provider"); + return; } mousePosRef.current.x = clientX; mousePosRef.current.y = clientY; - if (dragBoundaries.current === UNBOUNDED && draggableRef.current) { - const dragPosX = mousePosRef.current.x - mouseOffsetRef.current.x; - const dragPosY = mousePosRef.current.y - mouseOffsetRef.current.y; - draggableRef.current.style.top = `${dragPosY}px`; - draggableRef.current.style.left = `${dragPosX}px`; - } else if (dragDistance > 0 && draggableRef.current) { - const mouseMoveDirection = lastClientPos < clientPos ? "fwd" : "bwd"; - const scrollDirection = getScrollDirection(clientPos); - const dragPos = mousePosRef.current[POS] - mouseOffsetRef.current[POS]; - - if ( - scrollDirection && - isScrollableRef.current && - !isScrolling.current - ) { - handleScrollStart(); - startScrolling(scrollDirection, 1); - } else if (!scrollDirection && isScrolling.current) { - stopScrolling(); - } + if (dragDropState) { + const { draggableElement, mouseOffset } = dragDropState; + + if (dragBoundaries.current === UNBOUNDED && draggableElement) { + const dragPosX = mousePosRef.current.x - mouseOffset.x; + const dragPosY = mousePosRef.current.y - mouseOffset.y; + draggableElement.style.top = `${dragPosY}px`; + draggableElement.style.left = `${dragPosX}px`; + } else if (dragDistance > 0 && draggableElement) { + const mouseMoveDirection = lastClientPos < clientPos ? "fwd" : "bwd"; + const scrollDirection = getScrollDirection(clientPos); + const dragPos = mousePosRef.current[POS] - mouseOffset[POS]; + + if ( + scrollDirection && + isScrollableRef.current && + !isScrolling.current + ) { + handleScrollStart(); + startScrolling(scrollDirection, 1); + } else if (!scrollDirection && isScrolling.current) { + stopScrolling(); + } - if (!isScrolling.current) { - const renderDragPos = Math.round( - Math.max( - dragBoundaries.current.start, - Math.min(dragBoundaries.current.end, dragPos) - ) - ); - const START = orientation === "horizontal" ? "left" : "top"; - draggableRef.current.style[START] = `${renderDragPos}px`; - drag(renderDragPos, mouseMoveDirection); + if (!isScrolling.current) { + const renderDragPos = Math.round( + Math.max( + dragBoundaries.current.start, + Math.min(dragBoundaries.current.end, dragPos) + ) + ); + const START = orientation === "horizontal" ? "left" : "top"; + draggableElement.style[START] = `${renderDragPos}px`; + drag(renderDragPos, mouseMoveDirection); + } } } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ drag, - draggableRef, getScrollDirection, handleScrollStart, + id, isDragSource, isScrolling, + onDragOut, orientation, startScrolling, stopScrolling, ] ); const dragMouseUpHandler = useCallback(() => { - document.removeEventListener("mousemove", dragMouseMoveHandler, false); - document.removeEventListener("mouseup", dragMouseUpHandler, false); - settlingItemRef.current = draggableRef.current; + removeDragHandlers(); + if (dragDropStateRef.current) { + settlingItemRef.current = dragDropStateRef.current.draggableElement; + } // The implementation hook is currently invoking the onDrop callback, we should move it into here drop(); setDraggableStatus((status) => ({ @@ -307,54 +400,66 @@ export const useDragDropNext: DragDropHook = ({ draggedItemIndex: -1, isDragging: false, })); - dragElementRef.current = undefined; - }, [dragMouseMoveHandler, draggableRef, drop]); + // TODO clear the dragDropState + }, [drop, removeDragHandlers]); + + dragMouseMoveHandlerRef.current = dragMouseMoveHandler; + dragMouseUpHandlerRef.current = dragMouseUpHandler; + + const resumeDrag = useCallback( + (dragDropState: DragDropState) => { + dragDropStateRef.current = dragDropState; + // Note this is using the draggable element rather than the original draggedElement + const { draggableElement, mouseOffset, initialDragElement } = + dragDropState; + const { current: container } = containerRef; + + console.log({ container, draggableElement, initialDragElement }); + + if (container && draggableElement) { + const containerRect = container.getBoundingClientRect(); + const draggableRect = draggableElement.getBoundingClientRect(); + setDragBoundaries(containerRect, draggableRect); + + mousePosRef.current.x = draggableRect.left + mouseOffset.x; + mousePosRef.current.y = draggableRect.top + mouseOffset.y; + + // why doesn't this work if we use the initialDragEement + beginDrag(draggableElement); + + attachDragHandlers(); + + return true; + } else { + return false; + } + }, + [attachDragHandlers, beginDrag, containerRef, setDragBoundaries] + ); const dragStart = useCallback( (evt: MouseEvent) => { - const { clientX, clientY, target } = evt; + const { target } = evt; const dragElement = getDraggableElement(target, itemQuery); const { current: container } = containerRef; if (container && dragElement) { - const { - CONTRA, - CONTRA_END, - DIMENSION, - END, - SCROLL_SIZE, - CLIENT_SIZE, - START, - } = dimensions(orientation); - - dragElementRef.current = dragElement; + const { SCROLL_SIZE, CLIENT_SIZE } = dimensions(orientation); + const { [SCROLL_SIZE]: scrollSize, [CLIENT_SIZE]: clientSize } = container; isScrollableRef.current = scrollSize > clientSize; - const [lastElement, lastItemIsOverflowIndicator] = getLastElement( - container, - itemQuery - ); - const containerRect = container.getBoundingClientRect(); const draggableRect = dragElement.getBoundingClientRect(); - const draggableSize = draggableRect[DIMENSION]; - const { [START]: lastItemStart, [END]: lastItemEnd } = - lastElement.getBoundingClientRect(); - mouseOffsetRef.current.x = clientX - draggableRect.left; - mouseOffsetRef.current.y = clientY - draggableRect.top; + const dragDropState = (dragDropStateRef.current = new DragDropState( + evt, + dragElement + )); - dragBoundaries.current.start = containerRect[START]; - dragBoundaries.current.end = lastItemIsOverflowIndicator - ? Math.max(lastItemStart, containerRect.right - draggableSize) - : isScrollableRef.current - ? containerRect[START] + containerRect[DIMENSION] - draggableSize - : lastItemEnd - draggableSize; - dragBoundaries.current.contraStart = containerRect[CONTRA]; - dragBoundaries.current.contraEnd = containerRect[CONTRA_END]; + setDragBoundaries(containerRect, draggableRect); - beginDrag(evt); + beginDrag(dragElement); const { dataset: { index = "-1" }, @@ -366,7 +471,7 @@ export const useDragDropNext: DragDropHook = ({ @@ -374,22 +479,19 @@ export const useDragDropNext: DragDropHook = ({ draggedItemIndex: parseInt(index), }); - onDragStart?.(); - - document.addEventListener("mousemove", dragMouseMoveHandler, false); - document.addEventListener("mouseup", dragMouseUpHandler, false); + onDragStart?.(dragDropState); + attachDragHandlers(); } }, [ + attachDragHandlers, beginDrag, containerRef, - dragMouseMoveHandler, - dragMouseUpHandler, draggableClassName, - draggableRef, itemQuery, onDragStart, orientation, + setDragBoundaries, terminateDrag, ] ); @@ -406,6 +508,7 @@ export const useDragDropNext: DragDropHook = ({ } document.removeEventListener("mousemove", preDragMouseMoveHandler); document.removeEventListener("mouseup", preDragMouseUpHandler, false); + dragStart(evt); } }, @@ -424,8 +527,10 @@ export const useDragDropNext: DragDropHook = ({ const mouseDownHandler: MouseEventHandler = useCallback( (evt) => { - console.log("mousedown drag drop"); const { current: container } = containerRef; + // We don't want to prevent other handlers on this element from working + // but we do want to stop a drag drop being initiated on a bubbled event. + evt.stopPropagation(); if (container && !evt.defaultPrevented) { const { clientX, clientY } = evt; mousePosRef.current.x = startPosRef.current.x = clientX; @@ -483,6 +588,12 @@ export const useDragDropNext: DragDropHook = ({ } }, [containerRef, itemQuery, settlingItem, terminateDrag]); + useEffect(() => { + if (id && (isDragSource || isDropTarget)) { + register(id, resumeDrag); + } + }, [id, isDragSource, isDropTarget, register, resumeDrag]); + return { ...dragResult, ...draggableStatus, diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragSpacers.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragSpacers.ts deleted file mode 100644 index 818c3967e..000000000 --- a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragSpacers.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { useCallback, useMemo, useRef } from "react"; -import { MeasuredDropTarget } from "./drag-utils"; -import { createDragSpacer } from "./Draggable"; -import { Direction } from "./dragDropTypesNext"; - -export const useDragSpacers = () => { - const animationFrame = useRef(0); - const transitioning = useRef(false); - - const spacers = useMemo( - // We only need to listen for transition end on one of the spacers - () => [createDragSpacer(transitioning), createDragSpacer()], - [] - ); - - const clearSpacers = useCallback( - () => - spacers.forEach((spacer) => spacer.parentElement?.removeChild(spacer)), - [spacers] - ); - - const animateTransition = useCallback( - (size: number, order: number | undefined = 1) => { - const [spacer1, spacer2] = spacers; - animationFrame.current = requestAnimationFrame(() => { - transitioning.current = true; - spacer1.style.cssText = `width: 0px; order: ${order}; `; - spacer2.style.cssText = `width: ${size}px; order: ${order};`; - spacers[0] = spacer2; - spacers[1] = spacer1; - }); - }, - [spacers] - ); - - const cancelAnyPendingAnimation = useCallback(() => { - if (animationFrame.current) { - cancelAnimationFrame(animationFrame.current); - animationFrame.current = 0; - } - }, []); - - const displaceItem = useCallback( - ( - item: MeasuredDropTarget | null = null, - draggedItem: MeasuredDropTarget, - useTransition = false, - direction?: Direction - ) => { - if (item) { - const { order, size } = draggedItem; - const [spacer1, spacer2] = spacers; - cancelAnyPendingAnimation(); - if (useTransition) { - if (transitioning.current) { - clearSpacers(); - spacer1.style.cssText = - order === undefined - ? `width: ${size}px;` - : `width: ${size}px; order:${order}`; - spacer2.style.cssText = `width: 0px;`; - - const target = - direction === "fwd" - ? item.element.previousElementSibling - : item.element.nextElementSibling; - - item.element.parentElement?.insertBefore(spacer1, target); - item.element.parentElement?.insertBefore(spacer2, item.element); - } else { - item.element.parentElement?.insertBefore(spacer2, item.element); - } - animateTransition(size); - } else { - spacer1.style.cssText = - order === undefined - ? `width: ${size}px;` - : `width: ${size}px; order:${order}`; - item.element.parentElement?.insertBefore(spacer1, item.element); - } - } - }, - [animateTransition, cancelAnyPendingAnimation, clearSpacers, spacers] - ); - const displaceLastItem = useCallback( - (item: MeasuredDropTarget, size: number, useTransition = false) => { - const [spacer1, spacer2] = spacers; - - cancelAnyPendingAnimation(); - if (useTransition) { - if (transitioning.current) { - clearSpacers(); - - spacer1.style.cssText = `width: ${size}px`; - spacer2.style.cssText = `width: 0px`; - - item.element.parentElement?.insertBefore( - spacer1, - item.element.previousElementSibling - ); - item.element.parentElement?.insertBefore( - spacer2, - item.element.nextElementSibling - ); - } else { - item.element.parentElement?.insertBefore( - spacer2, - item.element.nextElementSibling - ); - } - animateTransition(size); - } else { - spacer1.style.cssText = `width: ${size}px`; - item.element.parentElement?.insertBefore( - spacer1, - item.element.nextElementSibling - ); - } - }, - [animateTransition, cancelAnyPendingAnimation, clearSpacers, spacers] - ); - - return { - displaceItem, - displaceLastItem, - clearSpacers, - }; -}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts new file mode 100644 index 000000000..f3d67e188 --- /dev/null +++ b/vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts @@ -0,0 +1,89 @@ +import { boxContainsPoint } from "@finos/vuu-utils"; +import { useCallback, useRef } from "react"; +import { MeasuredTarget } from "./DragDropProvider"; +import { DragDropState } from "./DragDropState"; +import { MouseOffset } from "./dragDropTypesNext"; + +export type ResumeDragHandler = (dragDropState: DragDropState) => boolean; + +export const useGlobalDragDrop = ({ + onDragOverDropTarget, +}: { + onDragOverDropTarget: ( + dropTargetId: string, + dragDropState: DragDropState + ) => boolean; +}) => { + const measuredDropTargetsRef = useRef>(); + + const dragDropStateRef = useRef(null); + /** current mouse position */ + const mousePosRef = useRef({ x: 0, y: 0 }); + + const overDropTarget = useCallback((x: number, y: number) => { + const { current: dropTargets } = measuredDropTargetsRef; + if (dropTargets) { + for (const [id, measuredTarget] of Object.entries(dropTargets)) { + if (boxContainsPoint(measuredTarget, x, y)) { + return id; + } + } + } + return undefined; + }, []); + + const dragMouseMoveHandler = useCallback( + (evt: MouseEvent) => { + const { clientX, clientY } = evt; + const { current: dragDropState } = dragDropStateRef; + + mousePosRef.current.x = clientX; + mousePosRef.current.y = clientY; + + if (dragDropState?.draggableElement) { + const { draggableElement, mouseOffset } = dragDropState; + + const dragPosX = mousePosRef.current.x - mouseOffset.x; + const dragPosY = mousePosRef.current.y - mouseOffset.y; + draggableElement.style.top = `${dragPosY}px`; + draggableElement.style.left = `${dragPosX}px`; + + const dropTarget = overDropTarget(dragPosX, dragPosY); + if (dropTarget) { + if (onDragOverDropTarget(dropTarget, dragDropState)) { + // prettier-ignore + document.removeEventListener("mousemove", dragMouseMoveHandler, false); + document.removeEventListener("mouseup", dragMouseUpHandler, false); + dragDropStateRef.current = null; + } + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const dragMouseUpHandler = useCallback(() => { + document.removeEventListener("mousemove", dragMouseMoveHandler, false); + document.removeEventListener("mouseup", dragMouseUpHandler, false); + }, [dragMouseMoveHandler]); + + const resumeDrag = useCallback( + (dragDropState) => { + console.log(`resume drag of `, { + el: dragDropState.draggableElement, + }); + dragDropStateRef.current = dragDropState; + document.addEventListener("mousemove", dragMouseMoveHandler, false); + document.addEventListener("mouseup", dragMouseUpHandler, false); + + return true; + }, + [dragMouseMoveHandler, dragMouseUpHandler] + ); + + return { + measuredDropTargetsRef, + resumeDrag, + }; +}; diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx b/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx index d837f6a7e..0df4987ca 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx +++ b/vuu-ui/packages/vuu-ui-controls/src/list/List.tsx @@ -69,6 +69,8 @@ export const List = forwardRef(function List< maxWidth, minHeight, minWidth, + onDragStart, + onDrop, onMoveListItem, onSelect, onSelectionChange, @@ -164,6 +166,8 @@ export const List = forwardRef(function List< id, label: "List", listHandlers: listHandlersProp, // should this be in context ? + onDragStart, + onDrop, onMoveListItem, onSelect, onSelectionChange, diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts b/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts index 23a4547e2..989802717 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/listTypes.ts @@ -22,7 +22,12 @@ import { SelectionProps, SelectionStrategy, } from "../common-hooks"; -import { DragHookResult, dragStrategy } from "../drag-drop"; +import { + DragHookResult, + DragStartHandler, + dragStrategy, + DropHandler, +} from "../drag-drop"; import { ViewportRange } from "./useScrollPosition"; export type ComponentType = ( @@ -33,6 +38,8 @@ export type ListItemType = ComponentType< ListItemProps & { ref?: Ref } >; +export type MoveItemHandler = (fromIndex: number, toIndex: number) => void; + export interface ListItemProps extends HTMLAttributes { children?: React.ReactNode; @@ -173,9 +180,17 @@ export interface ListProps< */ minWidth?: number | string; + // TODO implement a DragDrop interface + onDragStart?: DragStartHandler; + /** + * Handle item dropped onto list. Note, this will not be triggered if a list item is + * dragged within its owning list - this will trigger the onMoveListItem callback. + */ + onDrop?: DropHandler; + onHighlight?: (index: number) => void; - onMoveListItem?: (fromIndex: number, toIndex: number) => void; + onMoveListItem?: MoveItemHandler; onViewportScroll?: ( firstVisibleRowIndex: number, @@ -223,38 +238,43 @@ export interface ListControlProps { export interface ListHookProps extends Omit< - SelectionProps, - "onSelect" | "onSelectionChange" - > { - allowDragDrop?: boolean | dragStrategy; - collapsibleHeaders?: boolean; + SelectionProps, + "onSelect" | "onSelectionChange" + >, + Pick< + ListProps, + | "allowDragDrop" + | "collapsibleHeaders" + | "disabled" + | "id" + | "onDragStart" + | "onDrop" + | "onHighlight" + | "onMoveListItem" + | "restoreLastFocus" + | "stickyHeaders" + | "tabToSelect" + > { collectionHook: CollectionHookResult; containerRef: RefObject; contentRef?: RefObject; defaultHighlightedIndex?: number; - disabled?: boolean; disableAriaActiveDescendant?: boolean; disableHighlightOnFocus?: boolean; disableTypeToSelect?: boolean; focusVisible?: boolean; highlightedIndex?: number; - id?: string; label?: string; listHandlers?: ListHandlers; - onHighlight?: (index: number) => void; onKeyboardNavigation?: ( event: React.KeyboardEvent, currentIndex: number ) => void; onKeyDown?: (evt: KeyboardEvent) => void; - onMoveListItem?: (fromIndex: number, toIndex: number) => void; onSelect?: SelectHandler; onSelectionChange?: SelectionChangeHandler; - restoreLastFocus?: boolean; scrollContainerRef?: RefObject; selectionKeys?: string[]; - stickyHeaders?: boolean; - tabToSelect?: boolean; viewportRange?: ViewportRange; } diff --git a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts index 72453de07..9c02f82fa 100644 --- a/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts +++ b/vuu-ui/packages/vuu-ui-controls/src/list/useList.ts @@ -16,7 +16,11 @@ import { SelectionChangeHandler, SelectionStrategy, } from "../common-hooks"; -import { useDragDropNext as useDragDrop } from "../drag-drop"; +import { + DragStartHandler, + DropHandler, + useDragDropNext as useDragDrop, +} from "../drag-drop"; import { closestListItemIndex, useCollapsibleGroups, @@ -44,6 +48,8 @@ export const useList = ({ id, label = "", listHandlers: listHandlersProp, + onDragStart, + onDrop, onHighlight, onKeyboardNavigation, onKeyDown, @@ -140,9 +146,13 @@ export const useList = ({ collectionHook: dataHook, }); - const handleDragStart = useCallback(() => { - setHighlightedIndex(-1); - }, [setHighlightedIndex]); + const handleDragStart = useCallback( + (dragDropState) => { + setHighlightedIndex(-1); + onDragStart?.(dragDropState); + }, + [onDragStart, setHighlightedIndex] + ); const selectionHook = useSelection({ containerRef, @@ -190,8 +200,8 @@ export const useList = ({ [adjustIndex] ); - const handleDrop = useCallback( - (fromIndex: number, toIndex: number) => { + const handleDrop = useCallback( + (fromIndex, toIndex, options) => { if (hasSelection(selectionHook.selected)) { selectedByIndexRef.current = reorderSelectedIndices( selectionHook.selected, @@ -199,14 +209,19 @@ export const useList = ({ toIndex ); } - onMoveListItem?.(fromIndex, toIndex); + if (options.isExternal) { + onDrop?.(fromIndex, toIndex, options); + } else { + onMoveListItem?.(fromIndex, toIndex); + } setHighlightedIndex(-1); }, [ selectionHook.selected, - onMoveListItem, setHighlightedIndex, reorderSelectedIndices, + onDrop, + onMoveListItem, ] ); diff --git a/vuu-ui/packages/vuu-utils/src/box-utils.ts b/vuu-ui/packages/vuu-utils/src/box-utils.ts new file mode 100644 index 000000000..39783fe82 --- /dev/null +++ b/vuu-ui/packages/vuu-utils/src/box-utils.ts @@ -0,0 +1,15 @@ +export interface rect { + bottom: number; + left: number; + right: number; + top: number; +} +export type rectTuple = [number, number, number, number]; + +export type dimension = "width" | "height"; + +export function boxContainsPoint(rect: rect, x: number, y: number) { + if (rect) { + return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; + } +} diff --git a/vuu-ui/packages/vuu-utils/src/index.ts b/vuu-ui/packages/vuu-utils/src/index.ts index 4b5a78f4d..9ec7fa41d 100644 --- a/vuu-ui/packages/vuu-utils/src/index.ts +++ b/vuu-ui/packages/vuu-utils/src/index.ts @@ -1,4 +1,5 @@ export * from "./array-utils"; +export * from "./box-utils"; export * from "./column-utils"; export * from "./cookie-utils"; export * from "./component-registry"; @@ -30,4 +31,4 @@ export * from "./selection-utils"; export * from "./sort-utils"; export * from "./text-utils"; export * from "./url-utils"; -export * from "./screenshot-utils" \ No newline at end of file +export * from "./screenshot-utils"; diff --git a/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx new file mode 100644 index 000000000..455242833 --- /dev/null +++ b/vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx @@ -0,0 +1,126 @@ +import { Flexbox } from "@finos/vuu-layout"; +import { + DragDropProvider, + DragStartHandler, + DropHandler, + List, + MoveItemHandler, +} from "@finos/vuu-ui-controls"; +import { useCallback, useMemo, useState } from "react"; +import { usa_states } from "./List.data"; + +let displaySequence = 1; + +export const DraggableListsOneWayDrag = () => { + const [source1, source2] = useMemo( + () => [usa_states.map((s) => `${s} 1`), usa_states.map((s) => `${s} 2`)], + [] + ); + const dragSource = useMemo( + () => ({ + list1: { dropTargets: "list2" }, + }), + [] + ); + + const [state1, setState1] = useState(source1); + const [state2, setState2] = useState(source2); + + const handleMoveListItem1 = useCallback( + (fromIndex, toIndex) => { + setState1((data) => { + const newData = data.slice(); + const [tab] = newData.splice(fromIndex, 1); + if (toIndex === -1) { + return newData.concat(tab); + } else { + newData.splice(toIndex, 0, tab); + return newData; + } + }); + }, + [] + ); + + const handleMoveListItem2 = useCallback( + (fromIndex, toIndex) => { + setState2((data) => { + const newData = data.slice(); + const [tab] = newData.splice(fromIndex, 1); + if (toIndex === -1) { + return newData.concat(tab); + } else { + newData.splice(toIndex, 0, tab); + return newData; + } + }); + }, + [] + ); + + const handleDragStart1 = useCallback( + (dragDropState) => { + const { initialDragElement } = dragDropState; + const { + dataset: { index = "-1" }, + } = initialDragElement; + + const value = state1[parseInt(index)]; + if (value) { + dragDropState.setPayload(value); + } + }, + [state1] + ); + const handleDragStart2 = useCallback((dragDropState) => { + console.log("handleDragStart2", { + dragDropState, + }); + }, []); + + const handleDrop2 = useCallback( + (fromIndex, toIndex, options) => { + setState2((data) => { + const newData = data.slice(); + const payload = options.payload as string; + if (toIndex === -1) { + return newData.concat(payload); + } else { + newData.splice(toIndex, 0, payload); + return newData; + } + }); + }, + [] + ); + + return ( + + + +
+ + + + ); +}; +DraggableListsOneWayDrag.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/UiControls/List.examples.tsx b/vuu-ui/showcase/src/examples/UiControls/List.examples.tsx index 97dbe5df7..9fa13e13b 100644 --- a/vuu-ui/showcase/src/examples/UiControls/List.examples.tsx +++ b/vuu-ui/showcase/src/examples/UiControls/List.examples.tsx @@ -1,6 +1,5 @@ import { Flexbox } from "@finos/vuu-layout"; import { - DragDropProvider, dragStrategy, List, ListItem, @@ -318,38 +317,6 @@ export const DraggableListItemsDropIndicator = () => { }; DraggableListItemsDropIndicator.displaySequence = displaySequence++; -export const DraggableLists = () => { - const dragSource = useMemo( - () => ({ - list1: { dropTargets: "list2" }, - }), - [] - ); - - return ( - - - -
- - - - ); -}; -DraggableLists.displaySequence = displaySequence++; - export const ListWithinFlexLayout = () => { const handleSelect = useCallback((evt, selected) => { console.log(`handleSelect`, { selected }); diff --git a/vuu-ui/showcase/src/examples/UiControls/index.ts b/vuu-ui/showcase/src/examples/UiControls/index.ts index 666c0eee8..49b3154b1 100644 --- a/vuu-ui/showcase/src/examples/UiControls/index.ts +++ b/vuu-ui/showcase/src/examples/UiControls/index.ts @@ -1,4 +1,5 @@ export * as ComboBox from "./Combobox.examples"; +export * as DragDrop from "./DragDrop.examples"; export * as Dropdown from "./Dropdown.examples"; export * as EditableLabel from "./EditableLabel.examples"; export * as InstrumentSearch from "./InstrumentSearch.examples";