mcpField
D
drag-and-drop

drag-and-drop

github.com/wodsmith开发48 次安装
代码仓库

关于 drag-and-drop

drag-and-drop 是使用 @atlaskit/pragmatic-drag-and-drop 实现拖放界面的编码技能。它提供 React 模式,涵盖可拖动项、列表作用域、放置指示器和顺序持久化。

主要功能

  1. 01设置可拖动项和放置目标
  2. 02用 ref 跟踪易变状态
  3. 03通过列表实例 ID 限定操作范围
  4. 04显示基于边缘的放置指示器
  5. 05以乐观更新方式调整顺序

适用场景

  1. 01在 React 中构建可排序列表
  2. 02添加拖动手柄和自定义预览
  3. 03调整项目顺序并保存位置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wodsmith/thewodapp --skill drag-and-drop

安装后在 Claude Code 中输入/drag-and-drop确认已加载。

详细介绍

使用 Pragmatic DnD 拖放

本项目使用 @atlaskit/pragmatic-drag-and-drop 实现拖放功能。

必需的导入

import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"
import {
  draggable,
  dropTargetForElements,
  type ElementDropTargetEventBasePayload,
} 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 {
  attachClosestEdge,
  type Edge,
  extractClosestEdge,
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"
import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box"

关键模式:为易变状态使用 Refs

绝不要将易变的拖放状态放入 useEffect 依赖项中。 这样会导致处理程序在每次状态变化时重新注册。

// BAD - re-registers handlers on every edge change
const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
useEffect(() => {
  // ...handlers using closestEdge
}, [closestEdge]) // Re-runs on every drag movement!

// GOOD - use ref + useCallback for volatile state
const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
const closestEdgeRef = useRef<Edge | null>(null)

// Wrap in useCallback for lint compliance (exhaustive-deps)
const updateClosestEdge = useCallback((edge: Edge | null) => {
  closestEdgeRef.current = edge
  setClosestEdge(edge) // Still update state for rendering
}, [])

useEffect(() => {
  // ...handlers read closestEdgeRef.current instead
}, [/* stable deps */, updateClosestEdge]) // Include updateClosestEdge

导入 useCallback:

import { useCallback, useEffect, useRef, useState } from "react"

可拖动项目的基本模式

function DraggableItem({ item, index, instanceId, onDrop }) {
  const ref = useRef<HTMLDivElement>(null)
  const dragHandleRef = useRef<HTMLButtonElement>(null)
  const [isDragging, setIsDragging] = useState(false)
  const [closestEdge, setClosestEdge] = useState<Edge | null>(null)
  const closestEdgeRef = useRef<Edge | null>(null)

  const updateClosestEdge = useCallback((edge: Edge | null) => {
    closestEdgeRef.current = edge
    setClosestEdge(edge)
  }, [])

  useEffect(() => {
    const element = ref.current
    const dragHandle = dragHandleRef.current
    if (!element || !dragHandle) return

    const itemData = { id: item.id, index, instanceId }

    return combine(
      draggable({
        element: dragHandle,
        getInitialData: () => itemData,
        onDragStart: () => setIsDragging(true),
        onDrop: () => setIsDragging(false),
        onGenerateDragPreview({ nativeSetDragImage }) {
          setCustomNativeDragPreview({
            nativeSetDragImage,
            getOffset: pointerOutsideOfPreview({ x: "16px", y: "8px" }),
            render({ container }) {
              const preview = document.createElement("div")
              preview.style.cssText = `
                background: hsl(var(--background));
                border: 2px solid hsl(var(--border));
                border-radius: 6px;
                padding: 8px 12px;
                font-size: 14px;
                color: hsl(var(--foreground));
                box-shadow: 0 2px 8px rgba(0,0,0,0.15);
              `
              preview.textContent = item.label
              container.appendChild(preview)
            },
          })
        },
      }),
      dropTargetForElements({
        element,
        canDrop: ({ source }) =>
          source.data.instanceId === instanceId && source.data.index !== index,
        getData({ input }) {
          return attachClosestEdge(itemData, {
            element,
            input,
            allowedEdges: ["top", "bottom"],
          })
        },
        onDrag({ source, self }: ElementDropTargetEventBasePayload) {
          if (source.data.index === index) {
            updateClosestEdge(null)
            return
          }

          const edge = extractClosestEdge(self.data)
          const sourceIndex = source.data.index
          if (typeof sourceIndex !== "number") return

          // Hide indicator when it would be redundant
          const isItemBeforeSource = index === sourceIndex - 1
          const isItemAfterSource = index === sourceIndex + 1
          const isDropIndicatorHidden =
            (isItemBeforeSource && edge === "bottom") ||
            (isItemAfterSource && edge === "top")

          updateClosestEdge(isDropIndicatorHidden ? null : edge)
        },
        onDragLeave: () => updateClosestEdge(null),
        onDrop({ source }) {
          const sourceIndex = source.data.index
          if (typeof sourceIndex === "number" && sourceIndex !== index) {
            const edge = closestEdgeRef.current // Read from ref!
            const targetIndex = edge === "top" ? index : index + 1
            const adjustedTargetIndex =
              sourceIndex < targetIndex ? targetIndex - 1 : targetIndex
            onDrop(sourceIndex, adjustedTargetIndex)
          }
          updateClosestEdge(null)
        },
      }),
    )
  }, [item.id, item.label, index, instanceId, onDrop, updateClosestEdge])

  return (
    <div ref={ref} className="relative">
      {closestEdge && <DropIndicator edge={closestEdge} gap="2px" />}
      <div className={isDragging ? "opacity-50" : ""}>
        <button ref={dragHandleRef} type="button" aria-label="Drag to reorder">
          <GripVertical />
        </button>
        {/* Item content */}
      </div>
    </div>
  )
}

多个列表的实例 ID

使用 Symbol 将拖放操作限定在单个列表中:

function SortableList({ items }) {
  const [instanceId] = useState(() => Symbol("list"))
  // Pass instanceId to each item
}

重新排序处理程序

const handleDrop = async (sourceIndex: number, targetIndex: number) => {
  const newItems = [...items]
  const [movedItem] = newItems.splice(sourceIndex, 1)
  if (movedItem) {
    newItems.splice(targetIndex, 0, movedItem)
    const updated = newItems.map((item, i) => ({ ...item, position: i }))
    setItems(updated) // Optimistic update
    await saveOrder(updated) // Persist
  }
}

检查清单

  • 为易变状态使用 Refs(closestEdge 等)
  • 使用 useCallback 包装 updateClosestEdge(符合 lint 要求)
  • 在 useEffect 依赖项中包含 updateClosestEdge
  • 使用实例 ID 限定列表范围
  • 使用边缘检测显示放置指示器
  • 使用自定义拖动预览
  • 使用乐观 UI 更新

评论

支持 Markdown