umbraco-sorter by umbraco/umbraco-cms-backoffice-skills
npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-sorterUmbSorterController 为 Umbraco 后台中的项目列表提供了拖放排序功能。它可以处理容器内项目的重新排序、项目在容器间的移动,并支持嵌套排序场景。这对于块编辑器、内容树以及任何需要用户驱动排序的 UI 都非常有用。
在实现之前,请始终获取最新的文档:
Umbraco 源代码中包含可运行的示例:
嵌套容器 : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/
此示例演示了嵌套排序,其中的项目可以包含子项目。
两个容器 : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/
此示例展示了在两个独立容器之间移动项目。
状态管理 : 用于在顺序更改时进行响应式更新
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
umbraco-state-managementUmbraco 元素 : 用于创建可排序的项目元素
umbraco-umbraco-elementimport { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { html, customElement, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
interface MyItem {
id: string;
name: string;
}
@customElement('my-sortable-list')
export class MySortableListElement extends UmbLitElement {
#sorter = new UmbSorterController<MyItem, HTMLElement>(this, {
// 从 DOM 元素获取唯一标识符
getUniqueOfElement: (element) => {
return element.getAttribute('data-id') ?? '';
},
// 从数据模型获取唯一标识符
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
// 所有连接的排序器共享的标识符(用于跨容器拖放)
identifier: 'my-sortable-list',
// 可排序项目的 CSS 选择器
itemSelector: '.sortable-item',
// 容器的 CSS 选择器
containerSelector: '.sortable-container',
// 顺序更改时调用
onChange: ({ model }) => {
this._items = model;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('change', { detail: { items: model } }));
},
});
@property({ type: Array, attribute: false })
public get items(): MyItem[] {
return this._items;
}
public set items(value: MyItem[]) {
this._items = value;
this.#sorter.setModel(value);
this.requestUpdate();
}
private _items: MyItem[] = [];
override render() {
return html`
<div class="sortable-container">
${repeat(
this._items,
(item) => item.id,
(item) => html`
<div class="sortable-item" data-id=${item.id}>
${item.name}
</div>
`
)}
</div>
`;
}
}
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { html, customElement, property, repeat, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export interface NestedItem {
name: string;
children?: NestedItem[];
}
@customElement('my-sorter-group')
export class MySorterGroupElement extends UmbLitElement {
#sorter = new UmbSorterController<NestedItem, MySorterItemElement>(this, {
getUniqueOfElement: (element) => element.name,
getUniqueOfModel: (modelEntry) => modelEntry.name,
// 重要:相同的标识符允许项目在所有嵌套组之间移动
identifier: 'my-nested-sorter',
itemSelector: 'my-sorter-item',
containerSelector: '.sorter-container',
onChange: ({ model }) => {
const oldValue = this._value;
this._value = model;
this.requestUpdate('value', oldValue);
this.dispatchEvent(new CustomEvent('change'));
},
});
@property({ type: Array, attribute: false })
public get value(): NestedItem[] {
return this._value ?? [];
}
public set value(value: NestedItem[]) {
this._value = value;
this.#sorter.setModel(value);
this.requestUpdate();
}
private _value?: NestedItem[];
override render() {
return html`
<div class="sorter-container">
${repeat(
this.value,
(item) => item.name,
(item) => html`
<my-sorter-item .name=${item.name}>
<!-- 递归嵌套 -->
<my-sorter-group
.value=${item.children ?? []}
@change=${(e: Event) => {
item.children = (e.target as MySorterGroupElement).value;
}}
></my-sorter-group>
</my-sorter-item>
`
)}
</div>
`;
}
static override styles = css`
:host {
display: block;
min-height: 20px;
border: 1px dashed rgba(122, 122, 122, 0.25);
border-radius: var(--uui-border-radius);
padding: var(--uui-size-space-1);
}
`;
}
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('my-sorter-item')
export class MySorterItemElement extends UmbLitElement {
@property({ type: String })
name = '';
override render() {
return html`
<div class="item-wrapper">
<div class="drag-handle">
<uui-icon name="icon-navigation"></uui-icon>
</div>
<div class="item-content">
<span>${this.name}</span>
<slot name="action"></slot>
</div>
<div class="children">
<slot></slot>
</div>
</div>
`;
}
static override styles = css`
:host {
display: block;
background: var(--uui-color-surface);
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
margin: var(--uui-size-space-1) 0;
}
.item-wrapper {
padding: var(--uui-size-space-3);
}
.drag-handle {
cursor: grab;
display: inline-block;
margin-right: var(--uui-size-space-2);
}
.drag-handle:active {
cursor: grabbing;
}
.children {
margin-left: var(--uui-size-space-5);
margin-top: var(--uui-size-space-2);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
'my-sorter-item': MySorterItemElement;
}
}
@customElement('my-dual-sorter-dashboard')
export class MyDualSorterDashboard extends UmbLitElement {
listOneItems: MyItem[] = [
{ id: '1', name: 'Apple' },
{ id: '2', name: 'Banana' },
];
listTwoItems: MyItem[] = [
{ id: '3', name: 'Carrot' },
{ id: '4', name: 'Date' },
];
override render() {
return html`
<div class="container">
<my-sortable-list
.items=${this.listOneItems}
@change=${(e: CustomEvent) => {
this.listOneItems = e.detail.items;
}}
></my-sortable-list>
<my-sortable-list
.items=${this.listTwoItems}
@change=${(e: CustomEvent) => {
this.listTwoItems = e.detail.items;
}}
></my-sortable-list>
</div>
`;
}
}
关键点 : 两个列表在其 UmbSorterController 中使用相同的 identifier,以实现它们之间的拖放。
| 选项 | 类型 | 描述 |
|---|---|---|
identifier | string | 连接的排序器共享的 ID(启用跨容器拖放) |
itemSelector | string | 可排序项目的 CSS 选择器 |
containerSelector | string | 容器的 CSS 选择器 |
getUniqueOfElement | (element) => string | 从 DOM 元素提取唯一 ID |
getUniqueOfModel | (model) => string | 从数据模型提取唯一 ID |
onChange | ({ model }) => void | 顺序更改时调用 |
onStart | () => void | 拖放开始时调用 |
onEnd | () => void | 拖放结束时调用 |
// 设置模型(当项目在外部更改时调用)
this.#sorter.setModel(items);
// 获取当前模型
const currentItems = this.#sorter.getModel();
// 临时禁用排序
this.#sorter.disable();
// 重新启用排序
this.#sorter.enable();
| 类名 | 应用于 | 时机 |
|---|---|---|
.umb-sorter-dragging | 容器 | 当任何项目被拖放时 |
.umb-sorter-placeholder | 占位元素 | 指示放置位置 |
itemSelector 和 containerSelector 必须与你的 DOM 匹配identifierrepeat() 以进行正确的 DOM 差异比较就是这样!始终获取最新的文档,保持示例简洁,生成完整可运行的代码。
每周安装量
75
代码仓库
GitHub 星标数
17
首次出现
2026年2月4日
安全审计
安装于
github-copilot54
cursor24
opencode23
codex23
gemini-cli20
amp20
The UmbSorterController provides drag-and-drop sorting functionality for lists of items in the Umbraco backoffice. It handles reordering items within a container, moving items between containers, and supports nested sorting scenarios. This is useful for block editors, content trees, and any UI that requires user-driven ordering.
Always fetch the latest docs before implementing:
The Umbraco source includes working examples:
Nested Containers : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/
This example demonstrates nested sorting with items that can contain child items.
Two Containers : /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/
This example shows moving items between two separate containers.
State Management : For reactive updates when order changes
umbraco-state-managementUmbraco Element : For creating sortable item elements
umbraco-umbraco-elementimport { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { html, customElement, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
interface MyItem {
id: string;
name: string;
}
@customElement('my-sortable-list')
export class MySortableListElement extends UmbLitElement {
#sorter = new UmbSorterController<MyItem, HTMLElement>(this, {
// Get unique identifier from DOM element
getUniqueOfElement: (element) => {
return element.getAttribute('data-id') ?? '';
},
// Get unique identifier from data model
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
// Identifier shared by all connected sorters (for cross-container dragging)
identifier: 'my-sortable-list',
// CSS selector for sortable items
itemSelector: '.sortable-item',
// CSS selector for the container
containerSelector: '.sortable-container',
// Called when order changes
onChange: ({ model }) => {
this._items = model;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('change', { detail: { items: model } }));
},
});
@property({ type: Array, attribute: false })
public get items(): MyItem[] {
return this._items;
}
public set items(value: MyItem[]) {
this._items = value;
this.#sorter.setModel(value);
this.requestUpdate();
}
private _items: MyItem[] = [];
override render() {
return html`
<div class="sortable-container">
${repeat(
this._items,
(item) => item.id,
(item) => html`
<div class="sortable-item" data-id=${item.id}>
${item.name}
</div>
`
)}
</div>
`;
}
}
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { html, customElement, property, repeat, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export interface NestedItem {
name: string;
children?: NestedItem[];
}
@customElement('my-sorter-group')
export class MySorterGroupElement extends UmbLitElement {
#sorter = new UmbSorterController<NestedItem, MySorterItemElement>(this, {
getUniqueOfElement: (element) => element.name,
getUniqueOfModel: (modelEntry) => modelEntry.name,
// IMPORTANT: Same identifier allows items to move between all nested groups
identifier: 'my-nested-sorter',
itemSelector: 'my-sorter-item',
containerSelector: '.sorter-container',
onChange: ({ model }) => {
const oldValue = this._value;
this._value = model;
this.requestUpdate('value', oldValue);
this.dispatchEvent(new CustomEvent('change'));
},
});
@property({ type: Array, attribute: false })
public get value(): NestedItem[] {
return this._value ?? [];
}
public set value(value: NestedItem[]) {
this._value = value;
this.#sorter.setModel(value);
this.requestUpdate();
}
private _value?: NestedItem[];
override render() {
return html`
<div class="sorter-container">
${repeat(
this.value,
(item) => item.name,
(item) => html`
<my-sorter-item .name=${item.name}>
<!-- Recursive nesting -->
<my-sorter-group
.value=${item.children ?? []}
@change=${(e: Event) => {
item.children = (e.target as MySorterGroupElement).value;
}}
></my-sorter-group>
</my-sorter-item>
`
)}
</div>
`;
}
static override styles = css`
:host {
display: block;
min-height: 20px;
border: 1px dashed rgba(122, 122, 122, 0.25);
border-radius: var(--uui-border-radius);
padding: var(--uui-size-space-1);
}
`;
}
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('my-sorter-item')
export class MySorterItemElement extends UmbLitElement {
@property({ type: String })
name = '';
override render() {
return html`
<div class="item-wrapper">
<div class="drag-handle">
<uui-icon name="icon-navigation"></uui-icon>
</div>
<div class="item-content">
<span>${this.name}</span>
<slot name="action"></slot>
</div>
<div class="children">
<slot></slot>
</div>
</div>
`;
}
static override styles = css`
:host {
display: block;
background: var(--uui-color-surface);
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
margin: var(--uui-size-space-1) 0;
}
.item-wrapper {
padding: var(--uui-size-space-3);
}
.drag-handle {
cursor: grab;
display: inline-block;
margin-right: var(--uui-size-space-2);
}
.drag-handle:active {
cursor: grabbing;
}
.children {
margin-left: var(--uui-size-space-5);
margin-top: var(--uui-size-space-2);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
'my-sorter-item': MySorterItemElement;
}
}
@customElement('my-dual-sorter-dashboard')
export class MyDualSorterDashboard extends UmbLitElement {
listOneItems: MyItem[] = [
{ id: '1', name: 'Apple' },
{ id: '2', name: 'Banana' },
];
listTwoItems: MyItem[] = [
{ id: '3', name: 'Carrot' },
{ id: '4', name: 'Date' },
];
override render() {
return html`
<div class="container">
<my-sortable-list
.items=${this.listOneItems}
@change=${(e: CustomEvent) => {
this.listOneItems = e.detail.items;
}}
></my-sortable-list>
<my-sortable-list
.items=${this.listTwoItems}
@change=${(e: CustomEvent) => {
this.listTwoItems = e.detail.items;
}}
></my-sortable-list>
</div>
`;
}
}
Key : Both lists use the same identifier in their UmbSorterController to enable dragging between them.
| Option | Type | Description |
|---|---|---|
identifier | string | Shared ID for connected sorters (enables cross-container dragging) |
itemSelector | string | CSS selector for sortable items |
containerSelector | string | CSS selector for the container |
getUniqueOfElement |
// Set the model (call when items change externally)
this.#sorter.setModel(items);
// Get current model
const currentItems = this.#sorter.getModel();
// Disable sorting temporarily
this.#sorter.disable();
// Re-enable sorting
this.#sorter.enable();
| Class | Applied To | When |
|---|---|---|
.umb-sorter-dragging | Container | While any item is being dragged |
.umb-sorter-placeholder | Placeholder element | Indicates drop position |
itemSelector and containerSelector must match your DOMidentifier for connected sortersrepeat() with a key function for proper DOM diffingThat's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
Weekly Installs
75
Repository
GitHub Stars
17
First Seen
Feb 4, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot54
cursor24
opencode23
codex23
gemini-cli20
amp20
Genkit JS 开发指南:AI 应用构建、错误排查与最佳实践
7,700 周安装
微信文章转Markdown工具 - 高效抓取公众号文章并转换为Markdown格式,支持存档与AI处理
215 周安装
Mantine Combobox 组件:React 下拉选择与自动完成的底层原语 | 构建自定义 UI
241 周安装
新西兰商务英语指南:专业写作、毛利语使用与语气规范 | 商务沟通技巧
228 周安装
Browserbase Search API:快速搜索网络并返回结构化结果,无需浏览器会话
247 周安装
图标系统设计规范与最佳实践 - 图标网格、命名规则、可访问性指南
75 周安装
物联网开发技能 | Arduino/ESP32编程、MQTT通信、智能家居与工业物联网解决方案
228 周安装
(element) => string |
| Extract unique ID from DOM element |
getUniqueOfModel | (model) => string | Extract unique ID from data model |
onChange | ({ model }) => void | Called when order changes |
onStart | () => void | Called when dragging starts |
onEnd | () => void | Called when dragging ends |