mirror of https://github.com/buster-so/buster.git
189 lines
5.8 KiB
TypeScript
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';
|