draggable="true"仅控制光标,拖拽交互需手动补全事件链;Safari要求dragstart中首行显式设置effectAllowed='move'且与dragover中dropEffect严格一致,否则光标变禁止图标并静默丢弃drop事件。

draggable="true" 本身不构成可用拖拽交互,只是个光标开关;真正能拖能放,全靠事件链手动补全,且 Safari 和移动端是重灾区。
dragstart 里必须显式设 effectAllowed,否则 Safari 直接禁用 drop
Safari 对 effectAllowed 的校验极其严格:只要没在 dragstart 回调第一行写 e.dataTransfer.effectAllowed = 'move',拖拽光标立刻变成带斜杠的圆圈(?),drop 事件被静默丢弃——Chrome 和 Firefox 可能还容忍,但 Safari 认死理。
- 可选值只有
'move'、'copy'、'link';'all'或'uninitialized'在 Safari 里无效 - 大小写、拼写、空格必须和后续
dragover中的dropEffect完全一致,差一个字符就失败 - 别依赖默认值,也别在条件分支里才赋值——必须无条件、第一时间设置
dragover 不调 preventDefault(),目标区永远接不住
dragover 是高频事件,但它的核心作用只有一个:e.preventDefault()。不调它,浏览器默认禁止投放,光标变红叉,松手毫无反应——这不是 bug,是规范强制行为。
- 必须绑定到目标容器(比如
.column或#drop-zone),不能只绑在 body 或 document 上 - 回调第一句就得是
e.preventDefault(),不能包在 if 里,也不能延迟执行 - 可选加
e.dataTransfer.dropEffect = 'move'控制光标样式,但这不影响是否能投放
dataTransfer.getData 在跨 iframe 或 Firefox 下大概率为空
别指望 e.dataTransfer.getData('text/plain') 总能拿到 ID。Firefox 下常返回空字符串;跨 iframe 场景基本失效;React/Vue 动态渲染时,e.target 还可能指向子元素而非原始卡片节点。
立即学习“前端免费学习笔记(深入)”;
- 推荐用
WeakMap存上下文:const dragState = new WeakMap() -
dragstart里存:dragState.set(e.dataTransfer, { cardId: e.target.dataset.id, sourceColumn: 'todo' }) -
drop里取:const { cardId, sourceColumn } = dragState.get(e.dataTransfer) - 记得
dragend后调dragState.delete(e.dataTransfer),否则内存泄漏
移动端原生 draggable 基本不可用,检测到 touch 就该降级
iOS Safari 和 Android Chrome 根本不触发 dragstart/drop,这不是兼容性问题,是平台级缺失。你在 DevTools 里看到的“能拖”,真机上连事件监听器都不会进。
- 用
'ontouchstart' in window检测,一旦为真,立刻放弃原生 API - 降级方案:用
touchstart+touchmove+touchend模拟,配合requestAnimationFrame节流 - 长按 300ms 启动拖拽模式,比单击更防误触;用
transform: translate()更新位置,避免 layout 触发 - 如果项目允许,interact.js 或 sortablejs 这类已做平台适配的轻量库,比硬啃原生快得多
真正难的不是监听几个事件,而是源列和目标列之间 DOM 插入时机与数据状态的原子性对齐——松手那一帧,DOM 移动、数组更新、滚动修正、焦点迁移,必须全部完成,缺一不可。



















