diff --git a/components/form/field/TypeTag.tsx b/components/form/field/TypeTag.tsx index 3a54797..5942b57 100644 --- a/components/form/field/TypeTag.tsx +++ b/components/form/field/TypeTag.tsx @@ -1,19 +1,4 @@ -import { useLocal } from "@/lib/utils/use-local"; -import { Input } from "../../ui/input"; import { useEffect, useRef, useState } from "react"; -import { - useEditor, - EditorContent, - useCurrentEditor, - EditorProvider, -} from "@tiptap/react"; -import Link from "@tiptap/extension-link"; -import StarterKit from "@tiptap/starter-kit"; -import { Color } from "@tiptap/extension-color"; -import ListItem from "@tiptap/extension-list-item"; -import TextStyle from "@tiptap/extension-text-style"; -import { Popover } from "../../Popover/Popover"; -import { ButtonBetter } from "../../ui/button"; import get from "lodash.get"; export const TypeTag: React.FC = ({ @@ -31,17 +16,17 @@ export const TypeTag: React.FC = ({ const [editingIndex, setEditingIndex] = useState(null); // Index tag yang sedang diedit const [tempValue, setTempValue] = useState(""); // Nilai sementara untuk pengeditan const tagRefs = useRef<(HTMLDivElement | null)[]>([]); - + const val = fm?.data?.[name]; useEffect(() => { if (get(fm, `data.[${name}].length`)) { setTags(fm.data?.[name]); } - }, []); + }, [val]); useEffect(() => { - console.log("NEW"); + console.log("MASUK", tags); fm.data[name] = tags; fm.render(); - + console.log("MASUK"); if (typeof onChange === "function") { onChange(tags); } diff --git a/components/ui/list-drag.tsx b/components/ui/list-drag.tsx new file mode 100644 index 0000000..9cd0721 --- /dev/null +++ b/components/ui/list-drag.tsx @@ -0,0 +1,503 @@ +"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) => ( + + ))} +
+
+ ); +}; diff --git a/package.json b/package.json index 66d18c0..7d0c3f6 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,11 @@ "version": "0.1.0", "private": true, "dependencies": { + "@atlaskit/pragmatic-drag-and-drop-flourish": "^1.2.0", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", + "@atlaskit/pragmatic-drag-and-drop-live-region": "^1.3.0", + "@atlaskit/pragmatic-drag-and-drop-react-accessibility": "^1.3.1", + "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^1.2.0", "@emotion/css": "^11.13.5", "@faker-js/faker": "^9.2.0", "@floating-ui/react": "^0.26.28",