跳转到内容

可排序 DnD

@omnicajs/vue-remote/remote 现在导出三个用于 sortable drag-and-drop 的构件:

  • RemoteSortableContainer
  • RemoteSortableItem
  • RemoteDragHandle

它们让 remote 代码可以声明可排序列表和看板,而 Vue host adapter 负责命中测试、指针跟踪、键盘取消以及 host 侧的 DOM 状态更新。

不需要为这个功能额外注册 host 组件。host 的 Vue runtime 已经能够识别这些 remote wrapper 发出的内部 DnD binding。

import {
RemoteDragHandle,
RemoteSortableContainer,
RemoteSortableItem,
type RemoteSortableEvent,
} from '@omnicajs/vue-remote/remote'

核心 props:

  • RemoteSortableContainer
    • containerId:容器的逻辑键。
    • orientation'vertical' | 'horizontal'
    • accepts?:允许的 item type 列表。
    • disabled?:禁用该容器的 drop。
    • onDragenteronDragleaveonDragmoveonDrop
  • RemoteSortableItem
    • itemId:稳定的 item 键。
    • containerId:当前容器键。
    • index:当前视觉顺序索引。
    • type:用于 accepts 判定的 item 类型。
    • payload?:事件中返回的可序列化附加数据。
    • disabled?:禁用该 item 的拖拽。
    • onDragstartonDragendonDragcancel
  • RemoteDragHandle
    • for?:显式指定要拖拽的 itemId
    • disabled?:禁用该 handle 上的指针启动。

这三个组件都支持 as,因此渲染根节点既可以是原生标签,也可以是 host 组件的根节点。

  • Remote 代码声明容器、item、handle 以及重排逻辑。
  • Host runtime 解析这些节点背后的真实 DOM 元素。
  • Host 维护 pointer session,并计算 beforeafterinside
  • 是否允许放置由 item.type 和目标容器的 accepts 决定。
  • Escape 会取消当前拖拽并触发 onDragcancel

这种划分让重排逻辑保留在 remote 状态里,而底层 DOM 处理留在可信的 host 侧。

  1. RemoteDragHandle 上触发 pointerdown 后,会开始一个待激活会话。
  2. 超过拖拽阈值后,源 item 会触发 onDragstart
  3. 在移动过程中,目标容器会收到 onDragenteronDragmoveonDragleave
  4. 当在允许的目标上触发 pointerup 时,容器会收到 onDrop
  5. 源 item 最终总会以 onDragend 结束;如果发生 Escapepointercancel,则会触发 onDragcancel

RemoteSortableEvent 包含归一化后的拖拽状态:

  • accepted
  • itemIdtypepayload
  • sourceContainerIdtargetContainerId
  • targetItemIdtargetIndex
  • placement
  • pointer
  • sessionId
  • beforeafter 用于悬停在现有 item 边缘时。
  • inside 用于把内容放到容器本身中,包括空容器。
  • targetIndex 已经按目标容器顺序做过归一化。
  • 如果 RemoteDragHandle 渲染在被拖拽 item 的子树内部,则 for 可以省略。

示例:remote SFC 中的 sortable backlog

Section titled “示例:remote SFC 中的 sortable backlog”
<script setup lang="ts">
import { ref } from 'vue'
import {
RemoteDragHandle,
RemoteSortableContainer,
RemoteSortableItem,
type RemoteSortableEvent,
} from '@omnicajs/vue-remote/remote'
type Task = {
id: string;
title: string;
}
const tasks = ref<Task[]>([
{ id: 'task-1', title: 'Design DnD contract' },
{ id: 'task-2', title: 'Cover host runtime' },
{ id: 'task-3', title: 'Ship docs' },
])
const moveTask = (event: RemoteSortableEvent) => {
if (!event.accepted || event.targetIndex == null) {
return
}
const fromIndex = tasks.value.findIndex(task => task.id === event.itemId)
if (fromIndex < 0) {
return
}
const [task] = tasks.value.splice(fromIndex, 1)
const nextIndex = Math.min(event.targetIndex, tasks.value.length)
tasks.value.splice(nextIndex, 0, task)
}
</script>
<template>
<RemoteSortableContainer
as="ul"
class="task-list"
container-id="backlog"
:accepts="['task']"
orientation="vertical"
:on-drop="moveTask"
>
<RemoteSortableItem
v-for="(task, index) in tasks"
:key="task.id"
as="li"
class="task-row"
container-id="backlog"
:index="index"
:item-id="task.id"
:payload="{ title: task.title }"
type="task"
>
<RemoteDragHandle
as="button"
class="task-handle"
:for="task.id"
>
Drag
</RemoteDragHandle>
<span>{{ task.title }}</span>
</RemoteSortableItem>
</RemoteSortableContainer>
</template>

对于看板式场景,可以使用多个 RemoteSortableContainer,并在 onDrop 中同时更新源数组和目标数组。

拖拽过程中,host runtime 会把状态属性加到真实 DOM 上:

  • 静态标记:data-dnd-sortable-containerdata-dnd-sortable-itemdata-dnd-handle
  • 活动会话标记:data-dnd-sourcedata-dnd-draggingdata-dnd-targetdata-dnd-drag-over
  • placement 与可接受性标记:data-dnd-placementdata-dnd-drop-alloweddata-dnd-drop-forbidden

这样就可以在不把内部 transport props 泄漏到 DOM 的前提下,通过 host CSS 来控制拖拽态样式。