You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在dnd-kit可排序列表拖拽时实现Notion风格预览线?

在dnd-kit可排序列表中实现类似Notion的拖拽预览线

要实现拖拽时的水平浅蓝色预览线,核心是利用dnd-kit的拖拽状态和DOM位置计算,以下是具体实现方案:

核心思路

  1. 自定义一个预览线组件,默认隐藏,拖拽时根据hover的列表项位置动态显示并调整位置
  2. 通过useSortable获取拖拽状态(如isDragging、overId),结合拖拽事件计算预览线的摆放位置
  3. 根据拖拽元素与hover元素的相对位置,决定预览线显示在hover项的上方还是下方

代码实现

1. 预览线组件

先定义一个基础的预览线样式:

const DragPreviewLine = ({ style }) => (
  <div
    style={{
      position: 'absolute',
      left: 0,
      right: 0,
      height: 2,
      backgroundColor: '#63b3ed', // 浅蓝色,匹配Notion风格
      opacity: 0.8,
      zIndex: 999,
      ...style,
    }}
  />
);

2. 可排序列表项组件

在每个可拖拽的列表项中,监听拖拽事件并更新预览线状态:

import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useRef, useState, useEffect } from 'react';

const SortableItem = ({ id, children }) => {
  const { attributes, listeners, setNodeRef, transform, transition, isDragging, overId } = useSortable({ id });
  const itemRef = useRef(null);
  const [previewLineStyle, setPreviewLineStyle] = useState({ display: 'none' });

  // 处理拖拽移动事件,计算预览线位置
  const updatePreviewLine = (event) => {
    if (!isDragging || overId !== id) {
      setPreviewLineStyle({ display: 'none' });
      return;
    }

    const overElement = itemRef.current;
    if (!overElement) return;

    const rect = overElement.getBoundingClientRect();
    const dragElementRect = event.active.rect.current;
    const hoverMidpoint = rect.top + rect.height / 2;

    // 根据拖拽元素的位置,决定预览线在hover项的上方还是下方
    if (dragElementRect.top < hoverMidpoint) {
      setPreviewLineStyle({
        display: 'block',
        top: `${rect.top - window.scrollY}px`,
      });
    } else {
      setPreviewLineStyle({
        display: 'block',
        top: `${rect.bottom - window.scrollY}px`,
      });
    }
  };

  // 监听全局拖拽移动事件
  useEffect(() => {
    const handleDragMove = (e) => updatePreviewLine(e.detail);
    window.addEventListener('dragMove', handleDragMove);
    return () => window.removeEventListener('dragMove', handleDragMove);
  }, [isDragging, overId]);

  const itemStyle = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
    margin: '8px 0',
    padding: '12px',
    border: '1px solid #eee',
    borderRadius: '4px',
  };

  return (
    <>
      <div
        ref={(node) => {
          setNodeRef(node);
          itemRef.current = node;
        }}
        style={itemStyle}
        {...attributes}
        {...listeners}
      >
        {children}
      </div>
      <DragPreviewLine style={previewLineStyle} />
    </>
  );
};

3. 完整可排序列表容器

在容器中配置dnd-kit的核心上下文,并触发拖拽移动事件:

import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, arrayMove } from '@dnd-kit/sortable';
import { useState } from 'react';

const SortableList = () => {
  const [items, setItems] = useState(['列表项1', '列表项2', '列表项3', '列表项4']);
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  const handleDragEnd = (event) => {
    const { active, over } = event;
    if (active.id !== over.id) {
      setItems((prev) => {
        const oldIndex = prev.indexOf(active.id);
        const newIndex = prev.indexOf(over.id);
        return arrayMove(prev, oldIndex, newIndex);
      });
    }
  };

  const handleDragMove = (event) => {
    // 触发全局事件,通知所有列表项更新预览线
    window.dispatchEvent(new CustomEvent('dragMove', { detail: event }));
  };

  return (
    <div style={{ position: 'relative', maxWidth: '600px', margin: '20px auto', padding: '10px' }}>
      <DndContext
        sensors={sensors}
        collisionDetection={closestCenter}
        onDragEnd={handleDragEnd}
        onDragMove={handleDragMove}
      >
        <SortableContext items={items}>
          {items.map((item) => (
            <SortableItem key={item} id={item}>
              {item}
            </SortableItem>
          ))}
        </SortableContext>
      </DndContext>
    </div>
  );
};

关键注意点

  • 列表容器必须设置position: relative,确保预览线的绝对定位基于容器
  • 通过全局自定义事件传递拖拽移动信息,避免在每个列表项中重复监听
  • 利用getBoundingClientRect()获取元素位置,结合window.scrollY修正滚动时的位置偏差

内容的提问来源于stack exchange,提问作者55 Cancri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 12:55:33