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

251 lines
7.7 KiB
TypeScript

'use client';
import { SortableContext, useSortable } from '@dnd-kit/sortable';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { BusterSortableItemDragContainer } from './_BusterSortableItemDragContainer';
import { ResizeableGridDragItem } from './interfaces';
import { useMemoizedFn, useMouse } from '@/hooks';
import { BusterDragColumnMarkers } from './_BusterDragColumnMarkers';
import { calculateColumnSpan, columnSpansToPercent } from './config';
import SplitPane, { Pane } from '../layouts/AppSplitter/SplitPane';
import { cn } from '@/lib/classMerge';
type ContainerProps = {
rowId: string;
items: ResizeableGridDragItem[];
index: number;
columnSizes: number[] | undefined;
readOnly?: boolean;
onRowLayoutChange: (layout: number[], rowId: string) => void;
fluid?: boolean;
};
export const BusterResizeColumns: React.FC<ContainerProps> = ({
rowId,
onRowLayoutChange = () => {},
index: rowIndex,
columnSizes,
readOnly = true,
items = [],
fluid = true
}) => {
const { setNodeRef, isOver, active, over } = useSortable({
id: rowId,
disabled: readOnly
});
const mouse = useMouse({ moveThrottleMs: 50, disabled: readOnly || !over });
const [isDragginResizeColumn, setIsDraggingResizeColumn] = useState<number | null>(null);
const columnMarkerColumnIndex = useMemo(
() => (typeof isDragginResizeColumn === 'number' ? isDragginResizeColumn + 1 : null),
[isDragginResizeColumn]
);
const canResize = useMemo(() => items.length > 1 && items.length < 4, [items.length]);
const isDropzoneActives = useMemo(() => !!over?.id && canResize, [over?.id, canResize]);
const insertPosition = useMemoizedFn((itemId: string, _index: number, mouseLeft: number) => {
const movedIndex = _index;
// If the item is the only one in the container, don't show any dropzones
if (active?.data.current?.sortable?.containerId === rowId && items.length === 1) {
return undefined;
}
if (active?.data.current?.sortable?.containerId === over?.data.current?.sortable?.containerId) {
const res =
over?.id === itemId
? movedIndex > activeIndex
? Position.After
: Position.Before
: undefined;
return res;
}
const isLastItem =
over?.id === itemId && over?.data.current?.sortable?.index === items.length - 1;
if (isLastItem) {
const widthOfItem = over?.rect.width;
const leftSideOfItem = over?.rect.left;
const isOverLeftHalf = mouseLeft < leftSideOfItem + widthOfItem / 2;
if (isOverLeftHalf) {
return Position.Before;
}
return Position.After;
}
const res =
over?.id === itemId
? movedIndex > over?.data.current?.sortable?.index
? Position.After
: Position.Before
: undefined;
return res;
});
//NEW LOGIC
const [_sizes, setSizes] = useState<(number | string)[]>(columnSpansToPercent(columnSizes));
const sizes = _sizes.length === items.length ? _sizes : columnSpansToPercent(columnSizes);
const [stagedLayoutColumns, setStagedLayoutColumns] = useState<number[]>([]);
const activeDragId = active?.id;
const activeIndex = useMemo(() => {
return activeDragId ? items.findIndex((item) => item.id === active?.id) : -1;
}, [activeDragId, items, active?.id]);
const memoizedStyle = useMemo(() => {
console.log(isOver);
return {
backgroundColor: isOver ? 'rgba(0, 0, 0, 0.25)' : undefined
};
}, [isOver]);
const onChangeLayout = useMemoizedFn((sizes: number[]) => {
setSizes(sizes);
setStagedLayoutColumns(calculateColumnSpan(sizes));
});
const onDragEnd = useMemoizedFn(() => {
setIsDraggingResizeColumn(null);
const sizesFromColumnSpans = columnSpansToPercent(stagedLayoutColumns);
setSizes(sizesFromColumnSpans);
onRowLayoutChange(stagedLayoutColumns, rowId);
});
const onDragStart = useMemoizedFn((e: MouseEvent) => {
const srcElement = e.target as HTMLElement;
const idOrSrcElement = srcElement?.id;
if (idOrSrcElement) {
const parsedId = parseInt(idOrSrcElement);
if (typeof parsedId === 'number') {
setIsDraggingResizeColumn(parsedId);
}
}
});
const sashRender = useMemoizedFn((index: number, active: boolean) => {
return (
<ColumnSash
allowEdit={!readOnly && canResize}
isDraggingId={isDragginResizeColumn}
active={active}
index={index}
/>
);
});
useLayoutEffect(() => {
setSizes(columnSpansToPercent(columnSizes));
}, [items.length, columnSizes?.length]);
return (
<SortableContext id={rowId} items={items} disabled={false}>
<div ref={setNodeRef} className="relative h-full w-full" style={memoizedStyle}>
<BusterDragColumnMarkers
isDraggingIndex={columnMarkerColumnIndex}
itemsLength={items.length}
stagedLayoutColumns={stagedLayoutColumns}
disabled={!canResize}
/>
<SplitPane
autoSizeId="resize-column"
split="vertical"
sizes={sizes}
allowResize={!readOnly && canResize}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
sashRender={sashRender}
onChange={onChangeLayout}>
{items.map((item, index) => (
<Pane
className={cn(
'overflow-visible!',
index !== items.length - 1 ? 'pr-1.5' : 'pr-0',
index !== 0 ? 'pl-1.5' : 'pl-0'
)}
key={item.id}
minSize={'25%'}>
<div className="relative h-full w-full">
<DropzonePlaceholder
right={false}
isDropzoneActives={isDropzoneActives}
active={
!!over && insertPosition(item.id, index, mouse.clientX) === Position.Before
}
/>
<BusterSortableItemDragContainer itemId={item.id} allowEdit={!readOnly}>
{item.children}
</BusterSortableItemDragContainer>
<DropzonePlaceholder
right={true}
isDropzoneActives={isDropzoneActives}
active={
!!over && insertPosition(item.id, index, mouse.clientX) === Position.After
}
/>
</div>
</Pane>
))}
</SplitPane>
</div>
</SortableContext>
);
};
export enum Position {
Before = -1,
After = 1
}
const DropzonePlaceholder: React.FC<{
active: boolean;
right: boolean;
isDropzoneActives: boolean;
}> = React.memo(({ active, right, isDropzoneActives }) => {
const memoizedStyle = useMemo(() => {
return {
right: right ? -7.5 : undefined,
left: right ? undefined : -7.5,
opacity: active || isDropzoneActives ? 1 : 0
};
}, [active, isDropzoneActives, right]);
return (
<div
className={cn(
'pointer-events-none absolute top-0 bottom-0 h-full w-[4px] rounded-lg transition-opacity duration-200',
'bg-nav-item-hover z-[1]',
isDropzoneActives && 'placeholder',
active && 'bg-primary! opacity-100'
)}
style={memoizedStyle}
/>
);
});
DropzonePlaceholder.displayName = 'DropzonePlaceholder';
const ColumnSash: React.FC<{
index: number;
active: boolean;
isDraggingId: number | null;
allowEdit: boolean;
}> = React.memo(({ active, allowEdit, isDraggingId, index }) => {
return (
<div
className={cn(
'grid-column-sash h-full w-[4px] rounded-lg',
'z-10 transition-colors duration-200 ease-in-out',
allowEdit ? 'hover:bg-border' : 'hidden',
active ? 'bg-border' : '',
isDraggingId === index ? 'bg-primary!' : ''
)}
id={index.toString()}
/>
);
});
ColumnSash.displayName = 'ColumnSash';