umbraco-sorter
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUmbraco Sorter
Umbraco 排序器
What is it?
什么是UmbSorterController?
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.
UmbSorterController为Umbraco后台中的列表项提供拖拽排序功能。它支持在容器内重新排序项、在容器间移动项,还能处理嵌套排序场景。这一功能在块编辑器、内容树以及任何需要用户自定义排序的UI中都十分实用。
Documentation
文档参考
Always fetch the latest docs before implementing:
Reference Examples
参考示例
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.
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/该示例展示了如何在两个独立容器间移动项。
Related Foundation Skills
相关基础技能
-
State Management: For reactive updates when order changes
- Reference skill:
umbraco-state-management
- Reference skill:
-
Umbraco Element: For creating sortable item elements
- Reference skill:
umbraco-umbraco-element
- Reference skill:
-
状态管理:用于在排序变化时实现响应式更新
- 参考技能:
umbraco-state-management
- 参考技能:
-
Umbraco元素:用于创建可排序的项元素
- 参考技能:
umbraco-umbraco-element
- 参考技能:
Workflow
实现流程
- Fetch docs - Use WebFetch on the URLs above
- Ask questions - Single or multiple containers? Nested items? What data model?
- Generate files - Create container element + item element + sorter setup
- Explain - Show what was created and how sorting works
- 获取文档 - 通过WebFetch访问上述URL获取文档
- 明确需求 - 是单容器还是多容器?是否有嵌套项?数据模型是什么?
- 生成文件 - 创建容器元素、项元素并完成排序器配置
- 解释说明 - 展示创建的内容及排序功能的工作原理
Basic Sorter Setup
基础排序器配置
typescript
import { 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>
`;
}
}typescript
import { 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>
`;
}
}Nested Sorter (Items with Children)
嵌套排序器(包含子项的项)
typescript
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);
}
`;
}typescript
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);
}
`;
}Sortable Item Element
可排序项元素
typescript
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;
}
}typescript
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;
}
}Two Containers (Cross-Container Sorting)
双容器(跨容器排序)
typescript
@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 in their UmbSorterController to enable dragging between them.
identifiertypescript
@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需使用相同的,以支持跨容器拖拽。
identifierUmbSorterController Options
UmbSorterController配置选项
| Option | Type | Description |
|---|---|---|
| | Shared ID for connected sorters (enables cross-container dragging) |
| | CSS selector for sortable items |
| | CSS selector for the container |
| | Extract unique ID from DOM element |
| | Extract unique ID from data model |
| | Called when order changes |
| | Called when dragging starts |
| | Called when dragging ends |
| 选项 | 类型 | 描述 |
|---|---|---|
| | 关联排序器的共享ID(支持跨容器拖拽) |
| | 可排序项的CSS选择器 |
| | 容器的CSS选择器 |
| | 从DOM元素中提取唯一ID |
| | 从数据模型中提取唯一ID |
| | 排序变化时触发的回调 |
| | 拖拽开始时触发的回调 |
| | 拖拽结束时触发的回调 |
Key Methods
核心方法
typescript
// 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();typescript
// 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();CSS Classes Applied During Drag
拖拽过程中应用的CSS类
| Class | Applied To | When |
|---|---|---|
| Container | While any item is being dragged |
| Placeholder element | Indicates drop position |
| 类名 | 应用对象 | 触发时机 |
|---|---|---|
| 容器 | 当有项被拖拽时 |
| 占位元素 | 指示放置位置时 |
Best Practices
最佳实践
- Use unique identifiers - Each item must have a unique ID
- Match selectors carefully - and
itemSelectormust match your DOMcontainerSelector - Share identifier - Use same for connected sorters
identifier - Handle nested updates - Propagate changes up through nested structures
- Use repeat directive - Always use with a key function for proper DOM diffing
repeat() - Provide visual feedback - Style drag handles and drop zones clearly
That's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
- 使用唯一标识符 - 每个项必须拥有唯一ID
- 精准匹配选择器 - 和
itemSelector必须与DOM结构匹配containerSelector - 共享标识符 - 关联的排序器使用相同的
identifier - 处理嵌套更新 - 在嵌套结构中向上传递变化
- 使用repeat指令 - 始终配合键函数使用以实现正确的DOM差异对比
repeat() - 提供视觉反馈 - 清晰设置拖拽手柄和放置区域的样式
以上就是全部内容!请务必获取最新文档,保持示例简洁,生成完整可运行的代码。