209 lines
6.5 KiB
TypeScript
209 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
import React, {
|
|
CSSProperties,
|
|
ReactNode,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
|
|
import {
|
|
restrictToHorizontalAxis,
|
|
restrictToVerticalAxis,
|
|
} from "@dnd-kit/modifiers";
|
|
import { SortableContext } from "@dnd-kit/sortable";
|
|
import { LexoRank } from "lexorank";
|
|
import { FreeMode, Mousewheel, Scrollbar } from "swiper/modules";
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
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";
|
|
import classes from "./SortableDnd.module.css";
|
|
|
|
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;
|
|
swiperEnabled?: boolean;
|
|
};
|
|
|
|
const SortableDnd = <T extends BaseItem>({
|
|
initialItems,
|
|
renderItem,
|
|
renderDraggable,
|
|
dragHandleStyle,
|
|
onDragEnd,
|
|
onItemClick,
|
|
containerStyle,
|
|
vertical = false,
|
|
disabled = false,
|
|
swiperEnabled = 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(initialItems);
|
|
}, [initialItems]);
|
|
|
|
const sensors = useDndSensors();
|
|
|
|
const onDragEndLocal = ({ active, over }: DragEndEvent) => {
|
|
if (!over || active.id === over?.id || !activeItem) {
|
|
setActive(null);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
};
|
|
|
|
const renderWithSwiper = () => (
|
|
<Swiper
|
|
modules={[Scrollbar, Mousewheel, FreeMode]}
|
|
spaceBetween={15}
|
|
slidesPerView={"auto"}
|
|
scrollbar={{ hide: false }}
|
|
mousewheel={{
|
|
enabled: true,
|
|
sensitivity: 0.2,
|
|
}}
|
|
className={classes["swiper-container"]}
|
|
style={containerStyle}
|
|
direction={vertical ? "vertical" : "horizontal"}
|
|
freeMode={{ enabled: true }}
|
|
grabCursor>
|
|
{items.map((item, index) => (
|
|
<SwiperSlide
|
|
style={{ width: "fit-content" }}
|
|
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}
|
|
/>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
);
|
|
|
|
const renderWithFlex = () => (
|
|
<Flex
|
|
style={{
|
|
gap: 0,
|
|
flexWrap: "nowrap",
|
|
flexDirection: vertical ? "column" : "row",
|
|
...containerStyle,
|
|
}}>
|
|
{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>
|
|
);
|
|
|
|
const restrictModifier = vertical
|
|
? restrictToVerticalAxis
|
|
: restrictToHorizontalAxis;
|
|
|
|
return (
|
|
<DndContext
|
|
modifiers={[restrictModifier]}
|
|
sensors={sensors}
|
|
onDragStart={({ active }) => setActive(active)}
|
|
onDragEnd={onDragEndLocal}
|
|
onDragCancel={() => setActive(null)}>
|
|
<SortableContext
|
|
items={items}
|
|
disabled={disabled}>
|
|
{swiperEnabled ? renderWithSwiper() : renderWithFlex()}
|
|
</SortableContext>
|
|
<SortableOverlay>
|
|
{activeItem ? renderItem(activeItem, renderDraggable) : null}
|
|
</SortableOverlay>
|
|
</DndContext>
|
|
);
|
|
};
|
|
|
|
export default SortableDnd;
|