资讯动态

Umbraco Backoffice 拖拽排序实战:基于 UmbSorterController 实现嵌套容器 Sorter 示例解析

发布时间:2026/10/13 9:15:23 来源:尧图企业网站定制
CMS后端【免费下载链接】Umbraco-CMSUmbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.项目地址https://gitcode.com/gh_mirrors/um/Umbraco-CMS点击查看免费下载本文以 Umbraco-CMS 仓库中的sorter-with-nested-containers示例包为主体完整讲解如何在 Umbraco Backoffice 中配置并运行UmbSorterController重点剖析“嵌套容器nested containers”场景下的接入方式、数据模型同步机制以及控制器底层基于原生 HTML5 Drag and Drop 的跨容器移动原理。读完后你可以照示例搭建自己的多级拖拽排序界面并理解每个配置项在源码中的作用。示例定位它演示什么示例位于 examples/sorter-with-nested-containers 目录其 README.md 对目标只有一句话This example demonstrates how to set up the Sorter and how it can be used in nested setups.也就是说它演示两件事如何接入 SorterUmbSorterController以及如何在“容器套容器”的嵌套结构中协同工作。示例目录包含 4 个文件各自职责如下文件职责index.ts声明 dashboard manifest把示例注册进 Backofficesorter-dashboard.ts顶层 dashboard 元素持有两组数据并渲染两个排序分组sorter-group.ts可复用的“分组”元素内部实例化UmbSorterController并渲染嵌套分组sorter-item.ts可拖拽的条目元素带操作 slot 与占位样式按照 examples 目录的说明这些示例包既是扩展用法的演示也用于验证各扩展点持续可用。仓库中还有一个平行的示例 sorter-with-two-containers演示两个平级容器间的移动两者 manifest 别名相同example.dashboard.dataset由于一次只会运行一个示例包互不冲突。运行示例包示例由 devops/example-runner/index.js 驱动。在src/Umbraco.Web.UI.Client目录下执行# 交互式选择示例列出 examples/ 下所有子目录支持方向键/输入过滤 npm run example # 或直接指定本示例 npm run example sorter-with-nested-containers # 使用 MSW mock 数据运行对应 runner 的 --mock 参数底层执行 npm run dev:mock npm run example:mock sorter-with-nested-containers从 runner 源码可以看到其工作方式选中示例后设置环境变量VITE_EXAMPLE_PATHexamples/sorter-with-nested-containers然后调用npm run devmock 模式为npm run dev:mock启动 Vite 开发服务器由前端构建流程把该目录作为扩展包加载。脚本还会校验工作目录若找不到examples目录会提示 “Run this from src/Umbraco.Web.UI.Client”。Manifest把 dashboard 注册进 Backofficeindex.ts 声明了一个ManifestDashboardimport type { ManifestDashboard } from umbraco-cms/backoffice/dashboard; export const manifests: ArrayManifestDashboard [ { type: dashboard, name: Example Sorter Dashboard, alias: example.dashboard.dataset, // 懒加载 dashboard 组件 element: () import(./sorter-dashboard.js), weight: 900, meta: { label: Sorter example, pathname: sorter-example, }, }, ];各字段含义alias扩展包全局唯一标识element返回 Web Component 模块的懒加载函数meta.label/meta.pathname决定该 dashboard 在 Backoffice 导航中的显示名称与路由路径本例为sorter-exampleweight排序权重。顶层结构两个分组、可递归嵌套sorter-dashboard.ts 定义了example-sorter-dashboard自定义元素它维护两个ModelEntryType[]数组并渲染两个example-sorter-groupcustomElement(example-sorter-dashboard) export class ExampleSorterDashboard extends UmbElementMixin(LitElement) { groupOneItems: ModelEntryType[] [ { name: Apple, children: [{ name: Juice }, { name: Milk }] }, { name: Banana, children: [] }, { name: Pear }, { name: Pineapple }, { name: Lemon, children: [{ name: Cola }, { name: Pepsi }] }, ]; groupTwoItems: ModelEntryType[] [ { name: DXP }, { name: H5YR }, { name: UUI }, ]; override render() { return html uui-box classuui-text div classouter-wrapper example-sorter-group .value${this.groupOneItems} change${(e: Event) { this.groupOneItems (e.target as ExampleSorterGroup).value; }}/example-sorter-group example-sorter-group .value${this.groupTwoItems} change${(e: Event) { this.groupTwoItems (e.target as ExampleSorterGroup).value; }}/example-sorter-group /div /uui-box ; } }注意两点数据是自引用树形结构——每个条目可以带children这正是“嵌套容器”的数据基础dashboard 只负责“状态提升”把.value属性传给分组元素并在change事件后把分组的新模型读回来赋给自身状态触发 Lit 重渲染。数据模型定义在 sorter-group.ts 中export type ModelEntryType { name: string; children?: ModelEntryType[]; };核心接入ExampleSorterGroup 与 UmbSorterController 配置sorter-group.ts 是示例的关键——一个分组元素内部实例化一个控制器#sorter new UmbSorterControllerModelEntryType, ExampleSorterItem(this, { getUniqueOfElement: (element) { return element.name; }, getUniqueOfModel: (modelEntry) { return modelEntry.name; }, identifier: string-that-identifies-all-example-sorters, itemSelector: example-sorter-item, containerSelector: .sorter-container, onChange: ({ model }) { const oldValue this._value; this._value model; this.requestUpdate(value, oldValue); // Fire an event for the parent to know that the model has changed. this.dispatchEvent(new CustomEvent(change)); }, });各配置项结合 控制器源码 逐一说明配置项示例取值源码中的默认值/作用getUniqueOfElementelement.name必选。由 DOM 元素提取唯一标识返回undefined会取消本次移动getUniqueOfModelmodelEntry.name必选。由模型条目提取唯一标识用于元素 ↔ 模型互查identifierstring-that-identifies-all-example-sorters缺省为Symbol()源码config.identifier ?? Symbol()即每个控制器实例默认互不相通显式共享同一字符串后所有带该 identifier 的 Sorter 之间才允许跨容器拖放——这是本示例嵌套/跨组拖拽能成立的前提itemSelectorexample-sorter-item必选。可拖拽条目元素的查询选择器containerSelector.sorter-container缺省为宿主元素自身从host.shadowRoot中按选择器查找找不到会抛出 “Sorter could not find the container element” 错误ignorerSelector未设置默认a,img,iframe,input,textarea,select,option这些元素及draggablefalse的元素不触发拖拽placeholderClass/placeholderAttr未设置二者都未设置时默认placeholderAttr drag-placeholder即拖拽占位符会被加上该属性条目组件据此降低透明度onChange更新模型并派发change模型发生 move/insert/remove 后的统一回调除本例用到的字段外UmbSorterConfig还支持disabledItemSelector、draggableSelector指定元素内部真正可拖拽的子元素、handleSelector指定唯一可交互的拖拽把手、dataTransferResolver向 DataTransfer 写入额外数据、onStart/onEnd、onDisallowed/onAllowed/onRequestMove允许/拒绝指示、onRequestDrop接收来自其他窗口/标签页的原生拖放、requestExternalRemove/requestExternalInsert跨 Sorter 感知范围的外部移除/插入、resolvePlacement自定义放置位置判定返回true/false决定前后放置、返回null阻止移动以及细粒度的performItemMove/performItemInsert/performItemRemove可返回Promiseboolean拦截具体操作。value属性是模型与控制器之间的桥梁property({ type: Array, attribute: false }) public get value(): ModelEntryType[] { return this._value ?? []; } public set value(value: ModelEntryType[]) { const oldValue this._value; this._value value; this.#sorter.setModel(this._value); // 父级重渲染时把最新模型同步进控制器 this.requestUpdate(value, oldValue); } removeItem (item: ModelEntryType) { this.value this.value.filter((r) r.name ! item.name); };渲染部分用 Lit 的repeat生成条目并在每个条目内部再渲染一个嵌套分组override render() { return html div classsorter-container ${repeat( this.value, // Note: ideally you have an unique identifier for each item, // but for this example we use the name as identifier. (item) item.name, (item) htmlexample-sorter-item name${item.name} button slotaction click${() this.removeItem(item)}Delete/button example-sorter-group .value${item.children ?? []} change${(e: Event) { item.children (e.target as ExampleSorterGroup).value; }}/example-sorter-group /example-sorter-item, )} /div ; }这里有三层含义.sorter-container就是containerSelector指向的容器repeat渲染的每个example-sorter-item都是该控制器的拖拽条目每个条目内部又嵌了一个example-sorter-group绑定item.children ?? []——这就是嵌套容器父条目的子节点本身也是一个可排序、可拖拽的容器源码注释明确提示用name当唯一标识只是示例方便真实项目应使用真正的唯一 ID。sorter-item.ts 则是条目外观展示名称、操作 slot 与默认 slot供嵌套分组使用并定义了一个umbDragPlaceholder布尔属性映射到drag-placeholder属性property({ type: String, reflect: true }) name: string ; property({ type: Boolean, reflect: true, attribute: drag-placeholder }) umbDragPlaceholder false; // 样式中 // :host([drag-placeholder]) { opacity: 0.2; }当控制器把占位属性加到原条目上时整个条目淡出形成“拖拽占位”的视觉反馈。这个drag-placeholder属性名与控制器源码中placeholderAttr的默认值正好对应因此示例没有配置placeholderAttr也能自动生效。嵌套容器为什么能工作控制器源码机制理解了UmbSorterControllersorter.controller.ts共 1500 余行的几个关键机制示例的行为就都能解释1. 基于 dataTransfer 的“身份家族”识别dragstart时控制器会把两个键写入event.dataTransfertext/umb-sorter-identifier#identifier text/umb-sorter-item-unique#条目的唯一标识目标容器在dragover中执行#obtainIncomingItem只有当dataTransfer.types包含与自身identifier匹配的键时才接收该拖拽并顺带从text/umb-sorter-item-unique#…解析出条目唯一标识。随后控制器会在自己的模型中查找该条目若不存在则尝试onRequestDrop若已存在hasItem为真则拒绝——因此同一个条目不会被重复放进同一容器。这也解释了示例中identifier: string-that-identifies-all-example-sorters的必要性如果每个分组都用默认的Symbol()父容器永远无法识别来自子容器或兄弟容器的拖拽dragover直接被忽略共享同一个字符串后“组内移动”“组间移动”“父子层级间移动”在控制器看来都是同一套协议。2. 事件停止冒泡避免外层容器“抢事件”#itemDraggedOver处理器无论“本容器即当前 drop 目标”还是“本容器成为新的 drop 目标”都会执行e.stopPropagation()。在嵌套结构中这一点至关重要鼠标划过内层容器中的条目时内层容器先处理并阻止事件继续冒泡外层容器不会同时响应从而保证同一时刻只有一个“当前容器”在计算放置位置。3. moveItemInModel本地移动与跨容器移动的统一入口所有放置计算#updateDragMove最终调用#moveElementTo(newIndex)进而调用moveItemInModel(newIndex, fromCtrl)源码约 L1271–L1404。其分支逻辑本地移动fromCtrl this默认实现是对模型数组做splice删除再插入然后触发onChange若配置了performItemMove则交给回调返回false可取消跨容器移动先fromCtrl.removeItem(item)从源容器模型中移除会触发源容器的onChange再在目标容器模型newIndex处插入触发目标容器的onContainerChange带from源控制器引用与onChange并更新静态状态activeSorter/dropSorter/activeIndex指向目标容器纯外部拖放fromCtrl为undefined例如来自其他窗口的原生 drop要求配置requestExternalRemove/requestExternalInsert否则控制台报错并放弃。此外dragend时若dropEffect none如按 ESC 取消控制器会调用moveItemInModel(originalIndex, ...)把条目恢复到拖拽前的位置——这正是originalSorter/originalIndex两个静态字段存在的意义。4. MutationObserver 自动接管动态渲染的条目控制器构造时创建了一个MutationObserver初始化后对容器以{ childList: true, subtree: false }观察凡是新增且匹配itemSelector的节点自动setupItem绑定mousedown/dragstart/dragend、应用ignorerSelector被移除的节点自动destroyItem。因此示例中 Litrepeat随数据变化重新渲染条目时无需手动重新初始化——这是 Web Component 数据驱动渲染与该控制器能配合的根本原因。5. 状态如何沿“嵌套树”回传示例中onChange的写法体现了事件回传链条分组内部模型变化 →onChange更新_value并dispatchEvent(new CustomEvent(change))change事件由分组元素自身派发父级模板直接监听该元素上的change无需冒泡顶层分组 → dashboard 把新数组赋回groupOneItems/groupTwoItemsLit 重渲染后通过.valuesetter 调#sorter.setModel()重新同步控制器嵌套分组 → 父级模板把(e.target as ExampleSorterGroup).value写回item.children下次外层重渲染时同样经valuesetter 同步。配合disable()/enable()、getModel()/hasItem()/getItem()/removeItem()等公开方法控制器就构成了完整的可编程接口。细节与注意事项唯一标识示例用name作为 unique仅适用于数据名称天然唯一的教学场景生产代码务必使用稳定 ID否则跨容器移动会因标识冲突而失败。identifier 的取舍需要跨容器含嵌套容器拖放时才共享 identifier相互隔离的独立列表应保持默认Symbol()可避免互相接收拖拽。占位样式默认drag-placeholder属性即可用 CSS 做出淡出效果如需 class 形式可改用placeholderClass。自动滚动指针靠近滚动容器边缘 50pxautoScrollSensitivity时以每帧 16pxautoScrollSpeed的速度自动滚动源码位于控制器文件头部常量定义约 L5–L6嵌套长列表拖拽时同样生效。源码观察三个示例文件末尾的HTMLElementTagNameMap声明写的是example-sorter-dashboard-nested/example-sorter-group-nested/example-sorter-item-nested与customElement实际注册名不一致从源码结构看这属于复制自嵌套场景时的历史遗留不影响示例运行阅读时以customElement的注册名为准。测试与相关实现控制器的单元测试位于 sorter.controller.test.ts控制器经由 sorter 包的 index.ts 从umbraco-cms/backoffice/sorter导出即示例中import { UmbSorterController } from umbraco-cms/backoffice/sorter的来源示例包的运行方式见 package.json 中的example/example:mock脚本。小结这个 4 个文件的小示例完整覆盖了 Umbraco Backoffice 接入UmbSorterController的标准姿势manifest 注册 → 状态容器元素 → 控制器配置unique 解析、identifier、item/container 选择器、onChange→ 数据驱动的 repeat 渲染 → 占位样式。其嵌套能力的本质是共享 identifier 让所有 Sorter 实例讲同一种 dataTransfer 协议事件 stopPropagation 保证嵌套时层级间互不干扰MutationObserver 保证动态 DOM 与控制器状态自动同步。把这三点理解清楚后无论是内容类型属性里的组件排序、还是自定义 workspace 中的多级资源组织界面都可以按同样的模式实现。赞分享CMS后端【免费下载链接】Umbraco-CMSUmbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.项目地址https://gitcode.com/gh_mirrors/um/Umbraco-CMS点击查看免费下载相关推荐Ant Design Table 拖拽排序进阶基于 dnd-kit 实现带拖拽手柄Handler的行排序Ant Design Table 拖拽排序进阶基于 dnd kit 实现带拖拽手柄Handler的行排序 本篇文章围绕 drag sorting hand前端UI组件设计系统React DnD 可排序列表实战用 useDrag useDrop 在 hover 中实现拖拽排序Simple 示例深度解析React DnD 可排序列表实战用 useDrag useDrop 在 hover 中实现拖拽排序Simple 示例深度解析 React DnD 为前端UI组件rsuite List 组件 collection 分组排序实战基于集合隔离的拖拽排序实现与源码解析rsuite List 组件 collection 分组排序实战基于集合隔离的拖拽排序实现与源码解析 本文基于 rsuite 官方文档 collection.前端UI组件上一篇exa现代ls命令的革命性替代品下一篇Mako 开源项目教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑