buster/web/src/components/ui/grid/BusterResizeRows.tsx

189 lines
5.8 KiB
TypeScript

'use client';
import React, { useMemo, useRef, useState } from 'react';
import { BusterResizeableGridRow } from './interfaces';
import { BusterResizeColumns } from './BusterResizeColumns';
import { BusterNewItemDropzone } from './_BusterBusterNewItemDropzone';
import { MIN_ROW_HEIGHT, TOP_SASH_ID, NEW_ROW_ID, MAX_ROW_HEIGHT } from './config';
import clamp from 'lodash/clamp';
import { useDebounceFn, useMemoizedFn, useUpdateLayoutEffect } from 'ahooks';
import { useDroppable } from '@dnd-kit/core';
import { cn } from '@/lib/classMerge';
export const BusterResizeRows: React.FC<{
rows: BusterResizeableGridRow[];
className: string;
allowEdit?: boolean;
onRowLayoutChange: (rows: BusterResizeableGridRow[]) => void;
}> = ({ allowEdit = true, rows, className, onRowLayoutChange }) => {
const ref = useRef<HTMLDivElement>(null);
const [isDraggingResizeId, setIsDraggingResizeId] = useState<number | null>(null);
const [sizes, setSizes] = useState<number[]>(rows.map((r) => r.rowHeight ?? MIN_ROW_HEIGHT));
const { run: handleRowLayoutChangeDebounced } = useDebounceFn(
useMemoizedFn((sizes: number[]) => {
const newRows = rows.map((r, index) => ({
...r,
rowHeight: sizes[index]
}));
onRowLayoutChange(newRows);
}),
{ wait: 375 }
);
const handleResize = useMemoizedFn((index: number, size: number) => {
const newSizes = [...sizes];
newSizes[index] = size;
setSizes(newSizes);
handleRowLayoutChangeDebounced(newSizes);
});
const onRowLayoutChangePreflight = useMemoizedFn((columnSizes: number[], rowId: string) => {
const newRows: BusterResizeableGridRow[] = rows.map((r) => {
if (r.id === rowId) {
return { ...r, columnSizes };
}
return r;
});
onRowLayoutChange(newRows);
});
useUpdateLayoutEffect(() => {
setSizes(rows.map((r) => r.rowHeight ?? MIN_ROW_HEIGHT));
}, [rows.length]);
return (
<div
ref={ref}
className={cn(
className,
'buster-resize-row relative',
'mb-10 flex h-full w-full flex-col space-y-3 transition',
'opacity-100'
)}>
<ResizeRowHandle
id={TOP_SASH_ID}
top={true}
sizes={sizes}
active={false}
setIsDraggingResizeId={setIsDraggingResizeId}
onResize={handleResize}
allowEdit={allowEdit}
/>
{rows.map((row, index) => (
<div
key={row.id}
className="relative h-full w-full"
style={{
height: sizes[index]
}}>
<BusterResizeColumns
rowId={row.id}
items={row.items}
index={index}
allowEdit={allowEdit}
columnSizes={row.columnSizes}
onRowLayoutChange={onRowLayoutChangePreflight}
/>
<ResizeRowHandle
id={index.toString()}
index={index}
sizes={sizes}
active={isDraggingResizeId === index}
setIsDraggingResizeId={setIsDraggingResizeId}
onResize={handleResize}
allowEdit={allowEdit}
hideDropzone={index === rows.length - 1}
/>
</div>
))}
{allowEdit && <BusterNewItemDropzone />}
</div>
);
};
const ResizeRowHandle: React.FC<{
id: string;
index?: number;
sizes: number[];
setIsDraggingResizeId: (index: number | null) => void;
onResize: (index: number, size: number) => void;
allowEdit: boolean;
active: boolean;
top?: boolean; //if true we will not use dragging, just dropzone
hideDropzone?: boolean;
}> = React.memo(
({ hideDropzone, top, id, active, allowEdit, setIsDraggingResizeId, index, sizes, onResize }) => {
const { setNodeRef, isOver, over } = useDroppable({
id: `${NEW_ROW_ID}_${id}}`,
disabled: !allowEdit,
data: { id }
});
const showDropzone = !!over?.id && !hideDropzone;
const isDropzoneActive = showDropzone && isOver;
const handler = useMemoizedFn((mouseDownEvent: React.MouseEvent<HTMLDivElement>) => {
const startPosition = mouseDownEvent.pageY;
const style = document.createElement('style');
style.innerHTML = `* { cursor: row-resize; }`;
document.head.appendChild(style);
setIsDraggingResizeId(index!);
function onMouseMove(mouseMoveEvent: MouseEvent) {
const newSize = sizes[index!] + (mouseMoveEvent.pageY - startPosition);
const clampedSize = clamp(newSize, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
onResize(index!, clampedSize);
}
function onMouseUp() {
document.body.removeEventListener('mousemove', onMouseMove);
style.remove();
setIsDraggingResizeId(null);
}
document.body.addEventListener('mousemove', onMouseMove);
document.body.addEventListener('mouseup', onMouseUp, { once: true });
});
const onMouseDown = top ? undefined : handler;
const memoizedStyle = useMemo(() => {
return {
zIndex: 1,
bottom: !top ? -4 : -4,
transform: !top ? 'translateY(100%)' : 'translateY(100%)'
};
}, [top]);
const showActive = (active || isDropzoneActive) && allowEdit;
return (
<div className="relative">
<div
id={id}
className={cn(
allowEdit && 'hover:bg-border cursor-row-resize',
showActive && 'bg-primary! z-10 opacity-100',
'h-[4px] w-full rounded-sm transition-colors duration-200 ease-in-out select-none',
!top && 'dragger absolute'
)}
style={memoizedStyle}
onMouseDown={onMouseDown}
/>
<div
className={cn(
'pointer-events-all absolute right-0 left-0 z-50 h-[54px] opacity-0',
top ? '-top-[36px]' : '-bottom-[15px]'
)}
ref={setNodeRef}
/>
</div>
);
}
);
ResizeRowHandle.displayName = 'ResizeRowHandle';