153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import React, {
|
|
CSSProperties,
|
|
ReactNode,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
|
|
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
|
import { SortableContext } from "@dnd-kit/sortable";
|
|
import { LexoRank } from "lexorank";
|
|
import { Box, Flex } from "@mantine/core";
|
|
import useDndSensors from "@/app/deals/hooks/useSensors";
|
|
import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay";
|
|
import SortableItem from "@/components/dnd/SortableItem";
|
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
|
|
|
type BaseItem = {
|
|
id: number;
|
|
lexorank: string;
|
|
};
|
|
|
|
type Props<T extends BaseItem> = {
|
|
initialItems: T[];
|
|
renderItem: (
|
|
item: T,
|
|
renderDraggable?: (item: T) => ReactNode
|
|
) => ReactNode;
|
|
renderDraggable?: (item: T) => ReactNode; // if not passed - the whole item renders as draggable
|
|
dragHandleStyle?: CSSProperties;
|
|
onDragEnd: (itemId: number, newLexorank: string) => void;
|
|
onItemClick?: (item: T) => void;
|
|
containerStyle?: CSSProperties;
|
|
vertical?: boolean;
|
|
disabled?: boolean;
|
|
};
|
|
|
|
const SortableDnd = <T extends BaseItem>({
|
|
initialItems,
|
|
renderItem,
|
|
renderDraggable,
|
|
dragHandleStyle,
|
|
onDragEnd,
|
|
onItemClick,
|
|
containerStyle,
|
|
vertical,
|
|
disabled = false,
|
|
}: Props<T>) => {
|
|
const [active, setActive] = useState<Active | null>(null);
|
|
const [items, setItems] = useState<T[]>([]);
|
|
const activeItem = useMemo(
|
|
() => initialItems.find(item => item.id === active?.id),
|
|
[active, items]
|
|
);
|
|
|
|
useEffect(() => {
|
|
setItems(sortByLexorank(initialItems));
|
|
}, [initialItems]);
|
|
|
|
const sensors = useDndSensors();
|
|
|
|
const onDragEndLocal = ({ active, over }: DragEndEvent) => {
|
|
if (over && active.id !== over?.id && activeItem) {
|
|
const overIndex: number = items.findIndex(
|
|
({ id }) => id === over.id
|
|
);
|
|
const activeIndex: number = items.findIndex(
|
|
({ id }) => id === activeItem.id
|
|
);
|
|
|
|
let leftIndex = overIndex;
|
|
let rightIndex = overIndex + 1;
|
|
if (overIndex < activeIndex) {
|
|
leftIndex = overIndex - 1;
|
|
rightIndex = overIndex;
|
|
}
|
|
|
|
const leftLexorank: LexoRank | null =
|
|
leftIndex >= 0
|
|
? LexoRank.parse(items[leftIndex].lexorank)
|
|
: null;
|
|
const rightLexorank: LexoRank | null =
|
|
rightIndex < items.length
|
|
? LexoRank.parse(items[rightIndex].lexorank)
|
|
: null;
|
|
|
|
const newLexorank = getNewLexorank(
|
|
leftLexorank,
|
|
rightLexorank
|
|
).toString();
|
|
|
|
items[activeIndex].lexorank = newLexorank;
|
|
onDragEnd(items[activeIndex].id, newLexorank);
|
|
const sortedItems = sortByLexorank(items);
|
|
setItems([...sortedItems]);
|
|
}
|
|
setActive(null);
|
|
};
|
|
|
|
return (
|
|
<DndContext
|
|
modifiers={[restrictToHorizontalAxis]}
|
|
sensors={sensors}
|
|
onDragStart={({ active }) => setActive(active)}
|
|
onDragEnd={onDragEndLocal}
|
|
onDragCancel={() => setActive(null)}>
|
|
<SortableContext
|
|
items={items}
|
|
disabled={disabled}>
|
|
<Flex
|
|
gap={0}
|
|
style={{
|
|
flexWrap: "nowrap",
|
|
flexDirection: vertical ? "column" : "row",
|
|
...containerStyle,
|
|
}}
|
|
role="application">
|
|
{items.map((item, index) => (
|
|
<Box
|
|
key={index}
|
|
onClick={e => {
|
|
if (!onItemClick) return;
|
|
e.stopPropagation();
|
|
onItemClick(item);
|
|
}}>
|
|
<SortableItem
|
|
id={item.id}
|
|
disabled={disabled}
|
|
renderItem={renderDraggable =>
|
|
renderItem(item, renderDraggable)
|
|
}
|
|
renderDraggable={
|
|
renderDraggable
|
|
? () => renderDraggable(item)
|
|
: undefined
|
|
}
|
|
dragHandleStyle={dragHandleStyle}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</Flex>
|
|
</SortableContext>
|
|
<SortableOverlay>
|
|
{activeItem ? renderItem(activeItem, renderDraggable) : null}
|
|
</SortableOverlay>
|
|
</DndContext>
|
|
);
|
|
};
|
|
|
|
export default SortableDnd;
|