diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md new file mode 100644 index 00000000000..17376c66686 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-configuration.md @@ -0,0 +1,327 @@ +# Sorter Configuration Options + +The `UmbSorterController` accepts a configuration object with various options. This article covers all available configuration options. + +## Required Configuration + +### `getUniqueOfElement` + +```typescript +getUniqueOfElement: (element: ElementType) => + string | symbol | number | null | undefined; +``` + +Returns the unique identifier from a DOM element. Return `undefined` or `null` to cancel a move operation. + +**Example:** + +```typescript +getUniqueOfElement: (element) => element.getAttribute("data-id"); +``` + +### `getUniqueOfModel` + +```typescript +getUniqueOfModel: (modelEntry: T) => + string | symbol | number | null | undefined; +``` + +Returns the unique identifier from a model entry. + +**Example:** + +```typescript +getUniqueOfModel: (model) => model.id; +``` + +### `itemSelector` + +```typescript +itemSelector: string; +``` + +A CSS selector that matches the draggable items. + +**Example:** + +```typescript +itemSelector: ".sortable-item"; +``` + +## Optional Configuration + +### `identifier` + +```typescript +identifier?: string | symbol | number +``` + +A unique identifier for the sorter instance. All sorters sharing the same identifier can interact with each other (drag items between containers). If not provided, a new `Symbol()` is used by default. + +**Example:** + +```typescript +identifier: "my-shared-sorter-group"; +``` + +### `containerSelector` + +```typescript +containerSelector?: string +``` + +A CSS selector for the container element. If not provided, the host element is used as the container. + +**Example:** + +```typescript +containerSelector: ".items-container"; +``` + +### `disabledItemSelector` + +```typescript +disabledItemSelector?: string +``` + +A CSS selector for items that cannot be dragged. + +**Example:** + +```typescript +disabledItemSelector: ".disabled-item"; +``` + +### `ignorerSelector` + +```typescript +ignorerSelector?: string +``` + +A CSS selector for elements within items that do not trigger dragging. The default value is `'a,img,iframe,input,textarea,select,option'`. + +**Example:** + +```typescript +ignorerSelector: "a,button,input"; +``` + +### `placeholderClass` + +```typescript +placeholderClass?: string +``` + +CSS class applied to the dragged element during the drag operation. + +**Example:** + +```typescript +placeholderClass: "dragging-placeholder"; +``` + +### `placeholderAttr` + +```typescript +placeholderAttr?: string +``` + +Attribute set on the dragged element during the drag operation. If neither `placeholderClass` nor `placeholderAttr` is provided, the default value is `'drag-placeholder'`. + +**Example:** + +```typescript +placeholderAttr: "data-dragging"; +``` + +### `draggableSelector` + +```typescript +draggableSelector?: string +``` + +CSS selector for the specific element within an item that can be dragged. This is useful for adding a drag handle. + +**Example:** + +```typescript +draggableSelector: ".drag-handle"; +``` + +### `handleSelector` + +```typescript +handleSelector?: string +``` + +CSS selector for the interactive handle within an item. Only this element can initiate the drag operation. + +**Example:** + +```typescript +handleSelector: ".drag-handle-button"; +``` + +## Callback Configuration + +### `onChange` + +```typescript +onChange?: (args: { item: T; model: Array }) => void +``` + +Called when the model changes. This callback is not invoked if more specific callbacks are provided. + +**Example:** + +```typescript +onChange: ({ model, item }) => { + console.log("Model changed:", model); + this._items = model; +}; +``` + +### `onContainerChange` + +```typescript +onContainerChange?: (args: { + item: T; + model: Array; + from: UmbSorterController | undefined +}) => void +``` + +Called when an item is moved from another container to this container. + +**Example:** + +```typescript +onContainerChange: ({ item, model, from }) => { + console.log(`Item ${item.id} moved from another container`); + this._items = model; +}; +``` + +### `onStart` + +```typescript +onStart?: (args: { item: T; element: ElementType }) => void +``` + +Called when a drag operation starts. + +**Example:** + +```typescript +onStart: ({ item }) => { + console.log("Started dragging:", item.name); +}; +``` + +### `onEnd` + +```typescript +onEnd?: (args: { item: T; element: ElementType }) => void +``` + +Called when a drag operation ends. + +**Example:** + +```typescript +onEnd: ({ item }) => { + console.log("Finished dragging:", item.name); +}; +``` + +### `onRequestMove` + +```typescript +onRequestMove?: (args: { item: T }) => boolean +``` + +Called to validate whether an item can be moved into this container. Return `false` to prevent the move operation. + +**Example:** + +```typescript +onRequestMove: ({ item }) => { + return item.type === "allowed-type"; +}; +``` + +### `onDisallowed` / `onAllowed` + +```typescript +onDisallowed?: (args: { item: T; element: ElementType }) => void +onAllowed?: (args: { item: T; element: ElementType }) => void +``` + +Callbacks for providing visual feedback when moves are disallowed or allowed. + +**Example:** + +```typescript +onDisallowed: ({ element }) => { + element.classList.add('drop-not-allowed'); +}, +onAllowed: ({ element }) => { + element.classList.remove('drop-not-allowed'); +} +``` + +## Advanced Callbacks + +### `performItemMove` + +```typescript +performItemMove?: (args: { + item: T; + newIndex: number; + oldIndex: number +}) => Promise | boolean +``` + +Custom handler for moving items within the same container. Return `false` to cancel the move operation. + +**Example:** + +```typescript +performItemMove: async ({ item, newIndex, oldIndex }) => { + await this.saveToServer(item, newIndex); + return true; +}; +``` + +### `performItemInsert` + +```typescript +performItemInsert?: (args: { item: T; newIndex: number }) => Promise | boolean +``` + +Custom handler for inserting items into the container. + +### `performItemRemove` + +```typescript +performItemRemove?: (args: { item: T }) => Promise | boolean +``` + +Custom handler for removing items from the container. + +### `resolvePlacement` + +```typescript +resolvePlacement?: (args: UmbSorterResolvePlacementArgs) => UmbSorterResolvePlacementReturn +``` + +Custom logic for determining where to place an item during a drag operation. Return `true` to place after, `false` to place before, or `null` to cancel. + +**Example:** + +```typescript +resolvePlacement: ({ pointerY, relatedRect }) => { + // Place after if pointer is in bottom half + return pointerY > relatedRect.top + relatedRect.height * 0.5; +}; +``` diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md new file mode 100644 index 00000000000..88781b84094 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-enable-disable.md @@ -0,0 +1,80 @@ +# Enabling and Disabling the Sorter + +The Sorter can be dynamically enabled or disabled based on your application's state. This is useful when toggling between viewing and editing modes. + +## Methods + +### `enable()` + +Enables the sorter, allowing sorting interactions to occur. + +### `disable()` + +Disables the sorter, preventing any sorting interactions. + +## Example + +The following example shows how to toggle the sorter based on a "sort mode" state: + +```typescript +export class UmbContentTypeDesignEditorElement extends UmbLitElement { + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute("data-id"), + getUniqueOfModel: (model) => model.id, + itemSelector: ".sortable-item", + containerSelector: ".sortable-container", + onChange: ({ model }) => { + this._items = model; + }, + }); + + @state() + private _sortModeActive = false; + + // Initially disable the sorter + constructor() { + super(); + this.#sorter.disable(); + } + + #toggleSortMode() { + this._sortModeActive = !this._sortModeActive; + if (this._sortModeActive) { + this.#sorter.enable(); + } else { + this.#sorter.disable(); + } + } + + override render() { + return html` + + ${this._sortModeActive ? "Done" : "Sort"} + + +
+ ${repeat( + this._items, + (item) => item.id, + (item) => html` +
+ ${item.name} +
+ ` + )} +
+ `; + } +} +``` + +## Key Points + +- The sorter is **enabled by default** when instantiated. +- Call `disable()` to prevent sorting interactions. +- Call `enable()` to re-enable sorting. +- These methods are idempotent - calling them multiple times has no additional effect. +- Disabling the sorter removes all drag event listeners, improving performance when sorting is not needed. diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md new file mode 100644 index 00000000000..40500a21d44 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-multiple-containers.md @@ -0,0 +1,125 @@ +# Sorting Across Multiple Containers + +The Sorter supports dragging items between multiple containers by using a shared `identifier`. This allows you to create complex drag-and-drop interfaces. + +You can test this functionality using the examples available in the [Examples and Playground](https://docs.umbraco.com/umbraco-cms/customizing/examples-and-playground). Two examples are available: one for a sorter with nested containers, and another for two sorter containers. + +## Configuration + +To enable cross-container sorting, ensure all sorter instances use the same `identifier`: + +```typescript +const sharedIdentifier = "my-shared-sorter-group"; + +// Container 1 +#sorter1 = new UmbSorterController(this, { + identifier: sharedIdentifier, + // ... other config +}); + +// Container 2 +#sorter2 = new UmbSorterController(this, { + identifier: sharedIdentifier, + // ... other config +}); +``` + +## Example: Two Connected Containers + +This example shows two independent containers that can exchange items: + +```typescript +export type ModelEntryType = { + name: string; +}; + +@customElement("example-sorter-group") +export class ExampleSorterGroup extends UmbElementMixin(LitElement) { + @property({ type: Array, attribute: false }) + public get items(): ModelEntryType[] { + return this._items ?? []; + } + public set items(value: ModelEntryType[]) { + // Only set model initially, not on re-renders + if (this._items !== undefined) return; + this._items = value; + this.#sorter.setModel(this._items); + } + private _items?: ModelEntryType[]; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.name, + getUniqueOfModel: (modelEntry) => modelEntry.name, + // This identifier connects all sorter instances + identifier: "string-that-identifies-all-example-sorters", + itemSelector: "example-sorter-item", + containerSelector: ".sorter-container", + onChange: ({ model }) => { + const oldValue = this._items; + this._items = model; + this.requestUpdate("items", oldValue); + }, + }); + + removeItem = (item: ModelEntryType) => { + this._items = this._items!.filter((r) => r.name !== item.name); + this.#sorter.setModel(this._items); + }; + + override render() { + return html` +
+ ${repeat( + this.items, + (item) => item.name, + (item) => html` + + + + ` + )} +
+ `; + } +} +``` + +### Usage + +```html + + + +``` + +## Key Points + +- **Shared Identifier**: All containers must use the same `identifier` value. +- **Independent Models**: Each container maintains its own model via `setModel()`. +- **Automatic Updates**: The `onChange` callback handles model updates when items are moved between containers. +- **Item Removal**: When setting the model after removing an item, call `setModel()` to synchronize the state. + +## Validation + +Use `onRequestMove` to control which items can be dropped into specific containers: + +```typescript +#sorter = new UmbSorterController(this, { + // ... other config + onRequestMove: ({ item }) => { + // Only allow items of a specific type + return item.type === "allowed-in-this-container"; + }, +}); +``` diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md new file mode 100644 index 00000000000..a21c3524296 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorter-setting-model.md @@ -0,0 +1,245 @@ +# Setting the Sorter Model + +The sorter's model must be set using the `setModel()` method when data changes from an external source. This article covers different scenarios for managing the model. + +## The setModel() Method + +```typescript +setModel(model: Array | undefined): void +``` + +This method updates the sorter's internal model. Call it when: + +- Initial data is loaded +- Data is fetched from a server +- Data is updated programmatically (not via drag-and-drop) +- Items are added or removed from the list + +## Scenario 1: Property Setter with Initial Data + +When data is provided via a property and should only be set once: + +```typescript +@customElement("my-sortable-list") +export class MySortableList extends UmbLitElement { + @property({ type: Array, attribute: false }) + public get items(): ModelEntryType[] { + return this._items ?? []; + } + public set items(value: ModelEntryType[]) { + // Only set model initially, prevent re-setting on re-renders + if (this._items !== undefined) return; + this._items = value; + this.#sorter.setModel(this._items); + } + private _items?: ModelEntryType[]; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute("data-id"), + getUniqueOfModel: (model) => model.id, + itemSelector: ".item", + containerSelector: ".container", + onChange: ({ model }) => { + const oldValue = this._items; + this._items = model; + this.requestUpdate("items", oldValue); + }, + }); +} +``` + +**Why the guard?** The `if (this._items !== undefined) return;` statement prevents re-setting the model when Lit re-renders, which would interfere with drag-and-drop operations. + +## Scenario 2: Fetching Data + +When data is loaded asynchronously from a server: + +```typescript +@customElement("my-async-list") +export class MyAsyncList extends UmbLitElement { + @state() + private _items?: ModelEntryType[]; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute("data-id"), + getUniqueOfModel: (model) => model.id, + itemSelector: ".item", + containerSelector: ".container", + onChange: ({ model }) => { + this._items = model; + }, + }); + + override async connectedCallback() { + super.connectedCallback(); + await this.#loadData(); + } + + async #loadData() { + try { + const response = await fetch("/api/items"); + const items = await response.json(); + this._items = items; + // Set the model after fetching + this.#sorter.setModel(this._items); + } catch (error) { + console.error("Failed to load items:", error); + } + } + + async #refreshData() { + await this.#loadData(); + } + + override render() { + return html` + +
+ ${repeat( + this._items ?? [], + (item) => item.id, + (item) => html` +
${item.name}
+ ` + )} +
+ `; + } +} +``` + +## Scenario 3: Using willUpdate() for Reactive Updates + +When you need to synchronize external changes to the sorter model: + +```typescript +@customElement("my-async-list") +export class MyAsyncList extends UmbLitElement { + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute("data-id"), + getUniqueOfModel: (model) => model.id, + itemSelector: ".item", + containerSelector: ".container", + onChange: ({ model }) => { + const oldValue = this._items; + this._items = model; + this.requestUpdate("_items", oldValue); + }, + }); + + @state() + private _items?: ModelEntryType[]; + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has("_items")) { + // Sync model whenever _items changes + this.#sorter.setModel(this._items); + } + } + + #addItem() { + const newItem: ModelEntryType = { + id: crypto.randomUUID(), + name: `Item ${this._items.length + 1}`, + }; + this._items = [...this._items, newItem]; + } + + #removeItem(item: ModelEntryType) { + this._items = this._items.filter((i) => i.id !== item.id); + } + + override render() { + return html` + +
+ ${repeat( + this._items, + (item) => item.id, + (item) => html` +
+ ${item.name} + +
+ ` + )} +
+ `; + } +} +``` + +**Why use `willUpdate()`?** + +- Centralizes model synchronization in one location +- Automatically synchronizes when `_items` changes +- Works for additions, removals, and external updates +- Cleaner than calling `setModel()` in every method that modifies `_items` + +## Scenario 4: Manual Item Management + +When adding or removing items programmatically: + +```typescript +@customElement("my-managed-list") +export class MyManagedList extends UmbLitElement { + @state() + private _items: ModelEntryType[] = []; + + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute("data-id"), + getUniqueOfModel: (model) => model.id, + itemSelector: ".item", + containerSelector: ".container", + onChange: ({ model }) => { + this._items = model; + }, + }); + + #addItem() { + const newItem: ModelEntryType = { + id: crypto.randomUUID(), + name: `Item ${this._items.length + 1}`, + }; + this._items = [...this._items, newItem]; + // Update sorter model after manual change + this.#sorter.setModel(this._items); + } + + #removeItem(item: ModelEntryType) { + this._items = this._items.filter((i) => i.id !== item.id); + // Update sorter model after manual change + this.#sorter.setModel(this._items); + } + + override render() { + return html` + +
+ ${repeat( + this._items, + (item) => item.id, + (item) => html` +
+ ${item.name} + +
+ ` + )} +
+ `; + } +} +``` + +## Best Practices + +1. **Always call `setModel()` after programmatic changes** to the item array. +2. **Do not call `setModel()` in the `onChange` callback** - this creates an infinite loop. +3. **Use guards in setters** to prevent unnecessary updates during re-renders. +4. **Maintain immutability** - create new arrays instead of mutating existing ones. +5. **Use `willUpdate()`** for reactive updates based on property changes. diff --git a/17/umbraco-cms/customizing/utilities/sorter/sorting.md b/17/umbraco-cms/customizing/utilities/sorter/sorting.md new file mode 100644 index 00000000000..db9a81cbdd3 --- /dev/null +++ b/17/umbraco-cms/customizing/utilities/sorter/sorting.md @@ -0,0 +1,93 @@ +--- +description: Enable sorting elements via drag and drop +--- + +# Sorting + +{% hint style="warning" %} +This page is a work in progress and may undergo further revisions, updates, or amendments. The information contained herein is subject to change without notice. +{% endhint %} + +The Umbraco Sorter enables you to make a list of elements sortable via drag-and-drop interaction. You must set up the sorter once on the element that renders the items to be sorted. As part of the configuration, you provide an `onChange` callback method that is executed every time the sorter changes the data. + +## Configuration + +The following example shows a basic setup of the Sorter: + +```typescript +type ModelEntryType = { + id: string; + name: string; +} + +this.#sorter = new UmbSorterController(this, { + itemSelector: '.sorter-item', + containerSelector: '.sorter-container', + getUniqueOfElement: (element) => { + return element.getAttribute('data-sorter-id'); + }, + getUniqueOfModel: (modelEntry) => { + return modelEntry.id; + }, + onChange: ({ model }) => { + const oldValue = this._items; + this._items = model; + this.requestUpdate('_items', oldValue); + }, +}); +``` + +The configuration properties are: + +* `itemSelector`: A query selector that matches the items that can be dragged. +* `containerSelector`: A query selector that matches the parent element of the items. +* `getUniqueOfElement`: A method that returns the unique identifier of an element. +* `getUniqueOfModel`: A method that returns the unique identifier of a model entry. +* `onChange`: A method to retrieve the changed model. This is called every time the model changes, including when the user is dragging items. + +## Data Model + +The model provided to the Sorter must be an Array. The following example extends the previous example: + +```typescript +const model: Array = [ + { + id: '1', + name: 'First item' + }, + { + id: '2', + name: 'Second item' + }, + { + id: '3', + name: 'Third item' + } +] + +// Set the Model. If you have changes to the model not coming from the Sorter, set the model again: +this.#sorter.setModel(model); +``` + +## Rendering + +The Sorter does not move elements. Instead, it updates the model as the user drags an item. This places higher demands on the rendering of the sortable elements. You must ensure that the rendering reuses the same element despite sorting the data differently. + +Lit provides a render helper method called `repeat` that handles this. The following example shows a render method that continues the previous examples: + +```typescript +render() { + return html` +
+ ${repeat( + this._items, + (item) => item.id, + (item) => + html`
+ ${item.name} +
`, + )} +
+ `; +} +``` diff --git a/17/umbraco-cms/customizing/utilities/sorting.md b/17/umbraco-cms/customizing/utilities/sorting.md deleted file mode 100644 index 1e7449deeee..00000000000 --- a/17/umbraco-cms/customizing/utilities/sorting.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -description: Enable sorting elements via drag and drop ---- - -# Sorting - -{% hint style="warning" %} -This page is a work in progress and may undergo further revisions, updates, or amendments. The information contained herein is subject to change without notice. -{% endhint %} - -The Umbraco Sorter enables you to make a list of elements sortable via drag-and-drop interaction. You have to set up the sorter once on the Element that renders the items to be sorted. As part of the configuration, you shall provide an `onChange` callback method, which will be executed every time the sorter makes a difference to the data. - -### Configuration - -The following example shows a basic setup of the Sorter. - -```typescript - -type ModelEntryType = { - id: string; - name: string; -} - -this.#sorter = new UmbSorterController(this, { - itemSelector: '.sorter-item', - containerSelector: '.sorter-container', - getUniqueOfElement: (element) => { - return element.getAttribute('data-sorter-id'); - }, - getUniqueOfModel: (modelEntry) => { - return modelEntry.id; - }, - onChange: ({ model }) => { - const oldValue = this._items; - this._items = model; - this.requestUpdate('_items', oldValue); - }, -}); -``` - -The properties provided are the following: - -* `itemSelector`: A query selector that matches the items that should be draggable. -* `containerSelector`: A query elector that matches the parent element of the items. -* `getUniqueOfElement`: A method that returns the unique element -* `getUniqueOfModel`: Provide a method that returns the unique of a given model entry -* `onChange`: Provide a method to retrieve the changed model. This is called every time the model is changed, including when the user is dragging around. - -### Data Model - -The model given to the Sorter must be an Array. The following example extends the example from above: - -```typescript - - const model: Array = [ - { - id: 1, - name: 'First item' - }, - { - id: 2, - name: 'second item' - } - { - id: 3, - name: 'Third item' - } - ] - - // Set the Model, if you have changes to the model not coming from the Sorter. Then set the model again: - this.#sorter.setModel(model); -``` - -### Rendering - -The Sorter does not move elements, instead, it updates the model as the user drags an item around. This puts higher pressure on the rendering of the sortable Elements. This means we need to make sure that the rendering re-uses the same element despite sorting the data differently. - -Lit does provide a render helper method called `repeat` that does this for us. The following example shows a render method that continues the work of the examples above: - -```typescript - - - render() { - return html` - - ${repeat( - this._items, - (item) => item.id, - (item) => - html`${item.name} - `, - )} - - `; - } -```