umbraco-sorter について
umbraco-sorterは、Umbracoバックオフィスでドラッグ&ドロップによる並べ替えを実装するコーディングスキルです。単一・複数コンテナーや入れ子のリストに対応する設定例を提供します。
主な機能
- 01リスト内の項目の並べ替え
- 02コンテナー間の項目移動
- 03入れ子になった項目の並べ替え
- 04ソーターのコールバックとモデル設定
活用シーン
- 01ブロックエディターの並べ替え可能なリスト作成
- 02ドラッグ操作で並べ替えるコンテンツツリーの構築
- 032つのバックオフィスリスト間での項目移動
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ umbraco/ umbraco-cms-backoffice-skills --skill umbraco-sorterインストール後、Claude Code で/umbraco-sorterを入力して読み込まれたか確認します。
詳細
Umbraco Sorter
これは何ですか?
UmbSorterController は、Umbraco バックオフィスで項目のリストに対するドラッグアンドドロップの並べ替え機能を提供します。コンテナ内での項目の並べ替え、コンテナ間での項目の移動を処理し、ネストされた並べ替えのシナリオをサポートします。これは、ブロックエディター、コンテンツツリー、およびユーザー主導の並べ替えが必要なあらゆる UI に役立ちます。
ドキュメント
実装する前に、常に最新のドキュメントを取得してください。
- Foundation: https://docs.umbraco.com/umbraco-cms/extend-your-project/backoffice-extensions/foundation
- Extension Registry: https://docs.umbraco.com/umbraco-cms/extend-your-project/backoffice-extensions/extending-overview/extension-registry
参照例
Umbraco のソースには動作する例が含まれています。
ネストされたコンテナ: /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/
この例では、子項目を含むことができる項目を使ったネストされた並べ替えを示します。
2 つのコンテナ: /Umbraco-CMS/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/
この例では、2 つの別々のコンテナ間で項目を移動する方法を示します。
関連する Foundation スキル
-
State Management: 順序が変更されたときのリアクティブな更新用
- 参照スキル:
umbraco-state-management
- 参照スキル:
-
Umbraco Element: 並べ替え可能な項目要素を作成するため
- 参照スキル:
umbraco-umbraco-element
- 参照スキル:
ワークフロー
- ドキュメントを取得 - 上記の URL に対して WebFetch を使用します
- 質問する - 単一または複数のコンテナ?ネストされた項目?どのデータモデル?
- ファイルを生成 - コンテナ要素 + 項目要素 + ソーターのセットアップを作成します
- 説明 - 作成したものと並べ替えの仕組みを提示します
基本的なソーターのセットアップ
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>
`;
}
}
ネストされたソーター(子を持つ項目)
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}>
<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;
}
}
2 つのコンテナ(コンテナ間の並べ替え)
@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 を使用します。
UmbSorterController のオプション
| オプション | 型 | 説明 |
|---|---|---|
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 | ドラッグが終了したときに呼び出されます |
主要なメソッド
// 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 クラス
| クラス | 適用先 | タイミング |
|---|---|---|
.umb-sorter-dragging | コンテナ | いずれかの項目がドラッグされている間 |
.umb-sorter-placeholder | プレースホルダー要素 | ドロップ位置を示します |
ベストプラクティス
- 一意の識別子を使用する - 各項目は一意の ID を持つ必要があります
- セレクターを慎重に一致させる -
itemSelectorとcontainerSelectorは DOM と一致する必要があります - 識別子を共有する - 接続されたソーターには同じ
identifierを使用します - ネストされた更新を処理する - 変更をネストされた構造を通じて上位に伝播させます
- repeat ディレクティブを使用する - 適切な DOM 差分処理のために、常にキー関数を伴う
repeat()を使用します - 視覚的なフィードバックを提供する - ドラッグハンドルとドロップゾーンを明確にスタイル設定します
以上です!常に最新のドキュメントを取得し、例を最小限に保ち、完全に動作するコードを生成してください。