mcpField
U
umbraco-sorter

umbraco-sorter

github.com/umbraco開発27385 回インストール6 週間前に更新
リポジトリ

umbraco-sorter について

umbraco-sorterは、Umbracoバックオフィスでドラッグ&ドロップによる並べ替えを実装するコーディングスキルです。単一・複数コンテナーや入れ子のリストに対応する設定例を提供します。

主な機能

  1. 01リスト内の項目の並べ替え
  2. 02コンテナー間の項目移動
  3. 03入れ子になった項目の並べ替え
  4. 04ソーターのコールバックとモデル設定

活用シーン

  1. 01ブロックエディターの並べ替え可能なリスト作成
  2. 02ドラッグ操作で並べ替えるコンテンツツリーの構築
  3. 032つのバックオフィスリスト間での項目移動

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-sorter

インストール後、Claude Code で/umbraco-sorterを入力して読み込まれたか確認します。

詳細

Umbraco Sorter

これは何ですか?

UmbSorterController は、Umbraco バックオフィスで項目のリストに対するドラッグアンドドロップの並べ替え機能を提供します。コンテナ内での項目の並べ替え、コンテナ間での項目の移動を処理し、ネストされた並べ替えのシナリオをサポートします。これは、ブロックエディター、コンテンツツリー、およびユーザー主導の並べ替えが必要なあらゆる UI に役立ちます。

ドキュメント

実装する前に、常に最新のドキュメントを取得してください。

参照例

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

ワークフロー

  1. ドキュメントを取得 - 上記の URL に対して WebFetch を使用します
  2. 質問する - 単一または複数のコンテナ?ネストされた項目?どのデータモデル?
  3. ファイルを生成 - コンテナ要素 + 項目要素 + ソーターのセットアップを作成します
  4. 説明 - 作成したものと並べ替えの仕組みを提示します

基本的なソーターのセットアップ

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 のオプション

オプション型説明
identifierstring接続されたソーターの共有 ID(コンテナ間のドラッグを有効にします)
itemSelectorstring並べ替え可能な項目の CSS セレクター
containerSelectorstringコンテナの CSS セレクター
getUniqueOfElement(element) => stringDOM 要素から一意の 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プレースホルダー要素ドロップ位置を示します

ベストプラクティス

  1. 一意の識別子を使用する - 各項目は一意の ID を持つ必要があります
  2. セレクターを慎重に一致させる - itemSelector と containerSelector は DOM と一致する必要があります
  3. 識別子を共有する - 接続されたソーターには同じ identifier を使用します
  4. ネストされた更新を処理する - 変更をネストされた構造を通じて上位に伝播させます
  5. repeat ディレクティブを使用する - 適切な DOM 差分処理のために、常にキー関数を伴う repeat() を使用します
  6. 視覚的なフィードバックを提供する - ドラッグハンドルとドロップゾーンを明確にスタイル設定します

以上です!常に最新のドキュメントを取得し、例を最小限に保ち、完全に動作するコードを生成してください。

コメント