如何在dnd-kit可排序列表拖拽时实现Notion风格预览线?
在dnd-kit可排序列表中实现类似Notion的拖拽预览线
要实现拖拽时的水平浅蓝色预览线,核心是利用dnd-kit的拖拽状态和DOM位置计算,以下是具体实现方案:
核心思路
- 自定义一个预览线组件,默认隐藏,拖拽时根据hover的列表项位置动态显示并调整位置
- 通过
useSortable获取拖拽状态(如isDragging、overId),结合拖拽事件计算预览线的摆放位置 - 根据拖拽元素与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
相关产品推荐
相关产品推荐

