"use client"; import { createContext, FC, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import ReactDOM from "react-dom"; import invariant from "tiny-invariant"; import { triggerPostMoveFlash } from "@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash"; import { attachClosestEdge, type Edge, extractClosestEdge, } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index"; import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region"; import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { draggable, dropTargetForElements, monitorForElements, } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { pointerOutsideOfPreview } from "@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview"; import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"; import { Box, xcss } from "@atlaskit/primitives"; import { token } from "@atlaskit/tokens"; import { cn } from "@/lib/utils"; import { ButtonBetter } from "./button"; import { RiDraggable } from "react-icons/ri"; type ItemPosition = "first" | "last" | "middle" | "only"; type CleanupFn = () => void; type ItemEntry = { itemId: string; element: HTMLElement }; type ListContextValue = { getListLength: () => number; registerItem: (entry: ItemEntry) => CleanupFn; reorderItem: (args: { startIndex: number; indexOfTarget: number; closestEdgeOfTarget: Edge | null; }) => void; instanceId: symbol; }; const ListContext = createContext(null); function useListContext() { const listContext = useContext(ListContext); invariant(listContext !== null); return listContext; } type Item = { id: string; label: string; }; const itemKey = Symbol("item"); type ItemData = { [itemKey]: true; item: Item; index: number; instanceId: symbol; }; function getItemData({ item, index, instanceId, }: { item: Item; index: number; instanceId: symbol; }): ItemData { return { [itemKey]: true, item, index, instanceId, }; } function isItemData(data: Record): data is ItemData { return data[itemKey] === true; } function getItemPosition({ index, items, }: { index: number; items: Item[]; }): ItemPosition { if (items.length === 1) { return "only"; } if (index === 0) { return "first"; } if (index === items.length - 1) { return "last"; } return "middle"; } const listItemContainerStyles = xcss({ position: "relative", backgroundColor: "elevation.surface", // eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-selectors -- Ignored via go/DSP-18766 }); const listItemStyles = xcss({ position: "relative", padding: "space.100", }); const listItemDisabledStyles = xcss({ opacity: 0.4 }); type DraggableState = | { type: "idle" } | { type: "preview"; container: HTMLElement } | { type: "dragging" }; const idleState: DraggableState = { type: "idle" }; const draggingState: DraggableState = { type: "dragging" }; const listItemPreviewStyles = xcss({ paddingBlock: "space.050", paddingInline: "space.100", borderRadius: "border.radius.100", backgroundColor: "elevation.surface.overlay", maxWidth: "360px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", }); const itemLabelStyles = xcss({ flexGrow: 1, whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", }); function ListItem({ item, index, position, children, className, classDragBtn, }: { item: Item; index: number; position: ItemPosition; children: (data?: any, index?: number) => any; className?: string; classDragBtn?: string; }) { const { registerItem, instanceId } = useListContext(); const ref = useRef(null); const [closestEdge, setClosestEdge] = useState(null); const dragHandleRef = useRef(null); const [draggableState, setDraggableState] = useState(idleState); useEffect(() => { const element = ref.current; const dragHandle = dragHandleRef?.current; invariant(element); invariant(dragHandle); const data = getItemData({ item, index, instanceId }); return combine( registerItem({ itemId: item.id, element }), draggable({ element: dragHandle, getInitialData: () => data, onGenerateDragPreview({ nativeSetDragImage }) { setCustomNativeDragPreview({ nativeSetDragImage, getOffset: pointerOutsideOfPreview({ x: token("space.200", "16px"), y: token("space.100", "8px"), }), render({ container }) { setDraggableState({ type: "preview", container }); return () => setDraggableState(draggingState); }, }); }, onDragStart() { setDraggableState(draggingState); }, onDrop() { setDraggableState(idleState); }, }), dropTargetForElements({ element, canDrop({ source }) { return ( isItemData(source.data) && source.data.instanceId === instanceId ); }, getData({ input }) { return attachClosestEdge(data, { element, input, allowedEdges: ["top", "bottom"], }); }, onDrag({ self, source }) { const isSource = source.element === element; if (isSource) { setClosestEdge(null); return; } const closestEdge = extractClosestEdge(self.data); const sourceIndex = source.data.index; invariant(typeof sourceIndex === "number"); const isItemBeforeSource = index === sourceIndex - 1; const isItemAfterSource = index === sourceIndex + 1; const isDropIndicatorHidden = (isItemBeforeSource && closestEdge === "bottom") || (isItemAfterSource && closestEdge === "top"); if (isDropIndicatorHidden) { setClosestEdge(null); return; } setClosestEdge(closestEdge); }, onDragLeave() { setClosestEdge(null); }, onDrop() { setClosestEdge(null); }, }) ); }, [instanceId, item, index, registerItem]); return (
{typeof children === "function" ? children(item, index) : <>}
{closestEdge && }
{draggableState.type === "preview" && ReactDOM.createPortal( {item.label}, draggableState.container )}
); } function getItemRegistry() { const registry = new Map(); function register({ itemId, element }: ItemEntry) { registry.set(itemId, element); return function unregister() { registry.delete(itemId); }; } function getElement(itemId: string): HTMLElement | null { return registry.get(itemId) ?? null; } return { register, getElement }; } type ListState = { items: Item[]; lastCardMoved: { item: Item; previousIndex: number; currentIndex: number; numberOfItems: number; } | null; }; export const ListBetterDragDrop: FC<{ data: any[]; onChange: (data: any) => void | Promise; className?: string; classContainer?: string; children: (data?: any, index?: number) => any; classDragBtn?: string; }> = ({ children, data, onChange, className, classContainer, classDragBtn, }) => { const [item, setItem] = useState(data); const [{ items, lastCardMoved }, setListState] = useState({ items: data, lastCardMoved: null, }); const [registry] = useState(getItemRegistry); // Isolated instances of this component from one another const [instanceId] = useState(() => Symbol("instance-id")); const reorderItem = useCallback( ({ startIndex, indexOfTarget, closestEdgeOfTarget, }: { startIndex: number; indexOfTarget: number; closestEdgeOfTarget: Edge | null; }) => { const finishIndex = getReorderDestinationIndex({ startIndex, closestEdgeOfTarget, indexOfTarget, axis: "vertical", }); if (finishIndex === startIndex) { // If there would be no change, we skip the update return; } setListState((listState) => { const item = listState.items[startIndex]; return { items: reorder({ list: listState.items, startIndex, finishIndex, }), lastCardMoved: { item, previousIndex: startIndex, currentIndex: finishIndex, numberOfItems: listState.items.length, }, }; }); const moveItem = (fromIndex: any, toIndex: any) => { setItem((prevItems) => { const updatedItems = [...prevItems]; const [movedItem] = updatedItems.splice(fromIndex, 1); // Hapus elemen updatedItems.splice(toIndex, 0, movedItem); // Masukkan ke target index return updatedItems; }); }; // finishIndex === startIndex moveItem(startIndex, finishIndex); onChange(items); }, [] ); useEffect(() => { console.log(data); return monitorForElements({ canMonitor({ source }) { return isItemData(source.data) && source.data.instanceId === instanceId; }, onDrop({ location, source }) { const target = location.current.dropTargets[0]; if (!target) { return; } const sourceData = source.data; const targetData = target.data; if (!isItemData(sourceData) || !isItemData(targetData)) { return; } const indexOfTarget = items.findIndex( (item) => item.id === targetData.item.id ); if (indexOfTarget < 0) { return; } const closestEdgeOfTarget = extractClosestEdge(targetData); reorderItem({ startIndex: sourceData.index, indexOfTarget, closestEdgeOfTarget, }); }, }); }, [instanceId, items, reorderItem]); // once a drag is finished, we have some post drop actions to take useEffect(() => { if (lastCardMoved === null) { return; } const { item, previousIndex, currentIndex, numberOfItems } = lastCardMoved; const element = registry.getElement(item.id); if (element) { triggerPostMoveFlash(element); } }, [lastCardMoved, registry]); // cleanup the live region when this component is finished useEffect(() => { return function cleanup() { liveRegion.cleanup(); }; }, []); const getListLength = useCallback(() => items.length, [items.length]); const contextValue: ListContextValue = useMemo(() => { return { registerItem: registry.register, reorderItem, instanceId, getListLength, }; }, [registry.register, reorderItem, instanceId, getListLength]); if (!items?.length) return <>; return (
{items.map((item, index) => ( ))}
); };