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