Skip to content

指南

EffDND 仅需属性即可为您的 HTML 添加拖放功能。无需 JavaScript 调用,也无需任何设置,只需导入一次即可。本指南将解释您可以使用它做什么以及如何操作。

入门

sh
npm i effdnd

只需在您的应用中导入一次:

ts
import 'effdnd';            // 图书馆
import 'effdnd/style.css';  // 可选:默认样式

就是这样。任何带有以下属性的元素都会立即变为可拖动状态——即使是之后添加到页面中的元素。

框架: 可与任何框架兼容。无需组件包装器。

工作原理

EffDND 的核心理念只有一个,本指南中的其他内容都只是对其的延伸:

  • 只有 trigger 才能启动拖拽操作。 任何物体本身都无法拖动。你需要抓住一个标记为 data-dnd-trigger 的拖拽手柄才能开始拖拽;如果没有触发器,任何物体都不会移动。
  • 实际移动的是 item 对象。 触发器通常位于 item 对象内部,因此整个元素会跟随你的指针移动,但它们不必是同一个节点。
  • scope 是沙盒。 item 对象只能在其 scope 范围内移动和放置。范围之外的放置区域或容器会被忽略。

这是基本情况scope 范围内的 item 对象上的 trigger

html
<div data-dnd-scope="board">              <!-- 范围:沙盒 -->
  <div data-dnd-item="a">                 <!-- item: 移动的内容 -->
    <span data-dnd-trigger>⠿</span>       <!-- 触发器:你抓取的东西 -->
      抓住我
  </div>
</div>

接下来的所有功能——重新排序、传输、滚动或下面的触发参数——都只是对这一核心行为进行改进或扩展。

拖动时的样式。 您看到的移动元素是 item 的克隆(副本), 并插入到该元素的自身父元素中。这意味着级联样式和继承样式,以及针对父元素选择器(ul > li.zone .card 等)编写的规则, 仍然适用于移动的克隆元素——无需为每种颜色单独设置。原始元素保持在列表中的原位,并通过 passive 状态变暗(参见下文的样式)。 注意——已转换的祖先元素。 克隆元素的 position:fixed 属性使其与布局分离。

但是,如果该项的任何祖先元素具有 transformfilterperspective(或 will-change:transform),浏览器会将 fixed 视为 absolute:克隆项将相对于该祖先元素进行定位,并且可能会出现偏移(并且它会随着可滚动的变换容器一起滚动)。避免拖拽项的祖先元素具有变换属性,或者通过 data-dnd-state 和您为该项设置的内联样式来覆盖固定视觉样式。

属性概览

属性功能
data-dnd-trigger标记用户拖动的“手柄”
data-dnd-item标记被拖动的元素
data-dnd-scope将拖放目标限制在一个容器内
data-dnd-reorder将容器转换为可排序列表(xy
data-dnd-target标记放置区域
data-dnd-transfer为放置区域添加操作:appendprependremove
data-dnd-scroll拖动时自动滚动容器
data-dnd-transition平滑移动动画
data-dnd-disabled禁用触发器
data-dnd-state拖拽过程中设置的运行时状态(active/passive)— 用于样式设置

最基本的情况只需要其中三个:容器(scope)内元素(item)的句柄(trigger)。其他所有元素都是可选的,用于添加不同的行为。

触发器参数

由于 trigger 是入口点,因此它是您最常调整的属性。它接受以分号分隔的参数列表:

html
<span data-dnd-trigger="dist:12;axis:y;scope:board;item:task-1">⠿</span>

需要设置拖动距离(dist

防止点击时意外拖动——只有当指针移动指定像素数后才会进行拖动:

html
<span data-dnd-trigger="dist:12">⠿</span>   <!-- 指针移动 12px 后才会拖动 -->

锁定到单轴(axis

html
<span data-dnd-trigger="axis:x">⠿</span>   <!-- 仅限水平方向 -->
<span data-dnd-trigger="axis:y">⠿</span>   <!-- 仅限垂直方向 -->

指定一个特定的容器(scope

默认情况下,EffDND 会通过查看 DOM 来查找最近的 scope。当您需要指定一个特定的 scope 时,请为其命名:

html
<span data-dnd-trigger="scope:board">⠿</span>

指定要移动的上一级项(item

触发器可以指定要移动的上一级项:

html
<span data-dnd-trigger="scope:board;item:task-1">⠿</span>

限制投放目标(target

仅允许投放到 data-dnd-target 以给定名称开头的区域:

html
<span data-dnd-trigger="scope:board;target:drop">⠿</span>

在整个页面上自由移动(scope:*

特殊的 scope:* 值会完全忽略任何 scope 边界:

html
<span data-dnd-trigger="scope:*">⠿</span>

然后可以将该项目拖动到页面上的任何位置。

禁用触发器

html
<span data-dnd-trigger data-dnd-disabled>⠿</span>

移除该属性即可重新启用。

典型场景

以下是实际界面中各个组件的常见组合方式。

重新排序:可排序列表

使用 data-dnd-reorder="y" 将列表项包裹起来,标记每个列表项,并添加一个句柄。

html
<ul data-dnd-scope="todo" data-dnd-reorder="y">
  <li data-dnd-item="1"><span data-dnd-trigger>⠿</span>Buy milk</li>
  <li data-dnd-item="2"><span data-dnd-trigger>⠿</span>Read a book</li>
</ul>
  • y 参数按垂直方向排序,x 参数按水平方向排序。
  • 只有列表的直接子项才会重新排列。

转移:在容器之间移动项目

将容器标记为放置区并为其添加转移操作。

html
<div data-dnd-scope="sprint">
  <div class="zone" data-dnd-target="backlog" data-dnd-transfer="append">Backlog</div>
  <div class="zone" data-dnd-target="done" data-dnd-transfer="prepend">Done</div>
</div>
  • append — 该项目被添加到区域的末尾
  • prepend — 该项目被添加到区域的开始
  • remove — 该项目被删除(非常适合垃圾箱)。

拖放区域必须位于同一“范围”内(或使用“范围:*”,见上文)。

组合:带有垃圾箱的可排序列表

html
<div data-dnd-scope="mailbox">
  <ul data-dnd-reorder="y">
    <li data-dnd-item="1"><span data-dnd-trigger>⠿</span>Invoice</li>
    <li data-dnd-item="2"><span data-dnd-trigger>⠿</span>Newsletter</li>
  </ul>
  <button data-dnd-target="trash" data-dnd-transfer="remove">Delete</button>
</div>

这里列表会自动排序,并且可以通过拖放按钮删除行。

看板:列和卡片

给看板添加 data-dnd-reorder="x" 参数以对列进行排序,并为每列的卡片区域添加一个

垂直重排序区域和一个转移接收器。

html
<div data-dnd-scope="kanban" data-dnd-reorder="x">          <!-- 对列进行排序 -->
  <div class="col" data-dnd-item="col-1">
    <div class="col-head"><span data-dnd-trigger>⠿</span>Backlog</div>
    <div class="cards" data-dnd-reorder="y"
         data-dnd-target="col-1" data-dnd-transfer="append"> <!-- 分类并接受卡片 -->
      <div class="card" data-dnd-item="task-1"><span data-dnd-trigger>⠿</span>Write spec</div>
    </div>
  </div>
  <!-- 更多列 ... -->
</div>

拖动列标题可重新排列列顺序;拖动卡片可在列之间移动。

调整常用行为

自动滚动长容器

data-dnd-scroll 附加到可滚动元素:

html
<div data-dnd-scroll="threshold:50;speed:12">
  • threshold(默认值 30)— 滚动开始的起始距离,单位为像素。

  • speed(默认值 10)— 滚动速度,单位为像素/帧。

现在,在边缘附近拖动会滚动容器,并且越靠近边缘,滚动速度越快。

平滑动画

html
<div data-dnd-transition="150ms ease">…</div>

默认值为 100ms linear

通过 JavaScript 响应拖拽事件

你仍然可以这样做——EffDND 只是将其设为可选。以下每个函数都会返回一个取消订阅函数:

ts
import { onDrag, onDrop, onReorder, onTransfer, onDragStart, onDragEnd } from 'effdnd';

onReorder((event) => {
  console.log('Item was reordered:', event.detail.keys.item);
});

可用事件:effdragstarteffdrageffdragendeffdragentereffdragleaveeffdropeffreorderefftransfer

每个事件都会提供一个可靠的“详情”对象:

ts
event.detail.keys.item;          // 哪个元素
event.detail.keys.scope;         // 在哪个范围内
event.detail.keys.target;        // 哪个目标
event.detail.item;

您可能会觉得有用的助手

ts
import { getItem, getScope, getTargets, getReorderContainer, reset } from 'effdnd';

getItem(trigger);           // 元素所属的项
getScope(trigger);          // 元素所属的作用域
getTargets(trigger);        // 可用于触发器的投放区域
reset(item);                // 将物品弹回原位

使用 data-dnd-state 进行样式设置

在拖拽过程中,EffDND 会为相关元素添加运行时 data-dnd-state 属性。这样,您就可以轻松地为当前拖拽的元素设置样式,无需编写内联 JavaScript。当没有元素被拖拽时,data-dnd-state 属性不会被应用。

状态值

元素状态含义
data-dnd-itemactive移动的克隆元素 — 当前跟随鼠标指针的元素
data-dnd-itempassive原始元素保持原位,在克隆元素后方变暗
data-dnd-scopeactive鼠标指针位于作用域边界之外
data-dnd-scopepassive鼠标指针位于作用域内 — 拖动时正常显示
data-dnd-targetactive当前鼠标悬停的放置区域
data-dnd-targetpassive可放置的有效放置区域

捆绑的 index.css 文件已经基于这些选择器构建了美观的默认样式,您可以使用相同的选择器来覆盖它们:

css
/* 默认值来自 index.css */
[data-dnd-item][data-dnd-state="passive"] { opacity: 0.2; }
[data-dnd-item][data-dnd-state="active"]  { opacity: 0.75; z-index: 1000; }

/* 您自己的主题 */
[data-dnd-item][data-dnd-state="passive"] { opacity: 0.08; }
[data-dnd-item][data-dnd-state="active"]  {
    opacity: 1;
    box-shadow: 0 0 0 2px cornflowerblue;
    border-radius: 8px;
}
[data-dnd-scope][data-dnd-state="active"] { outline: 2px dashed tomato; }
[data-dnd-target][data-dnd-state="active"] { background: rgba(100, 200, 255, 0.2); }

由于默认样式位于 index.css 文件中,因此加载它们是可选的:如果您跳过导入并编写自己的 [data-dnd-state="…"] 规则,EffDND 将完全摆脱依赖,让您拥有完全的视觉控制权。

提示: 原始元素是通过 opacity 属性变暗,而不是被移除——因此列表保持其布局,并且 拖动时,同级元素不会跳动。如果您希望原始元素完全消失,请将 passive 状态的 opacity 设置为 0(或将 visibility: hidden 设置为 0)。

简明参考

每个属性及其参数的精简版。

属性 / 参数默认值用途
data-dnd-trigger标记开始拖动的手柄
… dist数字指针移动指定像素后才拖动
… axisx / yboth将拖动锁定在一个轴上
… scope名称 / *nearest in DOM使用特定容器(或忽略作用域)
… item名称nearest in DOM拖动特定项
… target名称all仅允许拖放到名称匹配的目标上
data-dnd-disabled禁用触发器
data-dnd-item唯一名称标记被拖动的元素
data-dnd-scope唯一名称限制拖放操作只能在一个容器内进行
data-dnd-reorderx / y将容器转换为可排序列表
data-dnd-target名称标记放置区域
data-dnd-transferappend / prepend / remove放置在放置区域时执行的操作
data-dnd-scrollthreshold;speed30;10拖动时自动滚动容器
data-dnd-transition持续时间和缓动100ms linear平滑移动动画
data-dnd-stateactive / passive运行时拖动状态,通过 CSS 设置样式(参见样式

根据 Apache-2.0 许可证发布。