Merge branch 'main' into improvement/model-remapping

This commit is contained in:
Mads Rasmussen
2024-02-02 15:16:41 +01:00
27 changed files with 599 additions and 675 deletions

View File

@@ -0,0 +1,3 @@
# Property Dataset Dashboard Example
This example demonstrates how to set up the Sorter and how it can be used in nested setups.

View File

@@ -0,0 +1,15 @@
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
type: 'dashboard',
name: 'Example Sorter Dashboard',
alias: 'example.dashboard.dataset',
element: () => import('./sorter-dashboard.js'),
weight: 900,
meta: {
label: 'Sorter example',
pathname: 'sorter-example',
},
},
];

View File

@@ -0,0 +1,89 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { ModelEntryType } from './sorter-group.js';
import './sorter-group.js';
@customElement('example-sorter-dashboard')
export class ExampleSorterDashboard extends UmbElementMixin(LitElement) {
groupOneItems: ModelEntryType[] = [
{
name: 'Apple',
children: [
{
name: 'Juice',
},
{
name: 'Milk',
},
],
},
{
name: 'Banana',
children: [],
},
{
name: 'Pear',
},
{
name: 'Pineapple',
},
{
name: 'Lemon',
children: [
{
name: 'Cola',
},
{
name: 'Pepsi',
},
],
},
];
groupTwoItems: ModelEntryType[] = [
{
name: 'DXP',
},
{
name: 'H5YR',
},
{
name: 'UUI',
},
];
render() {
return html`
<uui-box class="uui-text">
<div class="outer-wrapper">
<h5>Notice this example still only support single group of Sorter.</h5>
<example-sorter-group .initialItems=${this.groupOneItems}></example-sorter-group>
<example-sorter-group .initialItems=${this.groupTwoItems}></example-sorter-group>
</div>
</uui-box>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
.outer-wrapper {
display: flex;
}
`,
];
}
export default ExampleSorterDashboard;
declare global {
interface HTMLElementTagNameMap {
'example-sorter-dashboard-nested': ExampleSorterDashboard;
}
}

View File

@@ -0,0 +1,108 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, LitElement, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import './sorter-item.js';
import ExampleSorterItem from './sorter-item.js';
export type ModelEntryType = {
name: string;
children?: ModelEntryType[];
};
@customElement('example-sorter-group')
export class ExampleSorterGroup extends UmbElementMixin(LitElement) {
@property({ type: Array, attribute: false })
public get initialItems(): ModelEntryType[] {
return this.items;
}
public set initialItems(value: ModelEntryType[]) {
// Only want to set the model initially, cause any re-render should not set this again.
if (this._items !== undefined) return;
this.items = value;
}
@property({ type: Array, attribute: false })
public get items(): ModelEntryType[] {
return this._items ?? [];
}
public set items(value: ModelEntryType[]) {
const oldValue = this._items;
this._items = value;
this.#sorter.setModel(this._items);
this.requestUpdate('items', oldValue);
}
private _items?: ModelEntryType[];
#sorter = new UmbSorterController<ModelEntryType, ExampleSorterItem>(this, {
getUniqueOfElement: (element) => {
return element.name;
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.name;
},
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);
};
render() {
return html`
<div class="sorter-container">
${repeat(
this.items,
(item) => item.name,
(item) =>
html`<example-sorter-item name=${item.name}>
<button slot="action" @click=${() => this.removeItem(item)}>Delete</button>
${item.children ? html`<example-sorter-group .initialItems=${item.children}></example-sorter-group>` : ''}
</example-sorter-item>`,
)}
</div>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
width: 100%;
}
.sorter-placeholder {
opacity: 0.2;
}
.sorter-container {
min-height: 20px;
}
example-sorter-group {
display: block;
width: 100%;
border: 1px dashed rgba(122, 122, 122, 0.25);
border-radius: calc(var(--uui-border-radius) * 2);
padding: var(--uui-size-space-1);
}
`,
];
}
export default ExampleSorterGroup;
declare global {
interface HTMLElementTagNameMap {
'example-sorter-group-nested': ExampleSorterGroup;
}
}

View File

@@ -0,0 +1,58 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, LitElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
@customElement('example-sorter-item')
export class ExampleSorterItem extends UmbElementMixin(LitElement) {
@property({ type: String, reflect: true })
name: string = '';
@property({ type: Boolean, reflect: true, attribute: 'drag-placeholder' })
umbDragPlaceholder = false;
render() {
return html`
<div>
${this.name}
<img src="https://picsum.photos/seed/${this.name}/400/400" style="width:120px;" />
<slot name="action"></slot>
</div>
<slot></slot>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
margin-bottom: 3px;
}
:host([drag-placeholder]) {
opacity: 0.2;
}
div {
display: flex;
align-items: center;
justify-content: space-between;
}
slot:not([name]) {
// go on new line:
}
`,
];
}
export default ExampleSorterItem;
declare global {
interface HTMLElementTagNameMap {
'example-sorter-item-nested': ExampleSorterItem;
}
}

View File

@@ -1,5 +1,3 @@
# Property Dataset Dashboard Example
This example demonstrates the how to setup the Sorter.
This example can still NOT sort between two groups. This will come later.
This example demonstrates how to set up the Sorter, and how it can be linked with another Sorter.

View File

@@ -40,7 +40,6 @@ export class ExampleSorterDashboard extends UmbElementMixin(LitElement) {
return html`
<uui-box class="uui-text">
<div class="outer-wrapper">
<h5>Notice this example still only support single group of Sorter.</h5>
<example-sorter-group .items=${this.groupOneItems}></example-sorter-group>
<example-sorter-group .items=${this.groupTwoItems}></example-sorter-group>
</div>

View File

@@ -10,73 +10,42 @@ export type ModelEntryType = {
name: string;
};
const SORTER_CONFIG: UmbSorterConfig<ModelEntryType, ExampleSorterItem> = {
compareElementToModel: (element, model) => {
return element.name === model.name;
},
querySelectModelToElement: (container, modelEntry) => {
return container.querySelector("example-sorter-item[name='" + modelEntry.name + "']");
},
identifier: 'string-that-identifies-all-example-sorters',
itemSelector: 'example-sorter-item',
containerSelector: '.sorter-container',
};
@customElement('example-sorter-group')
export class ExampleSorterGroup extends UmbElementMixin(LitElement) {
//
// Property that is used to set the model of the sorter.
@property({ type: Array, attribute: false })
public get items(): ModelEntryType[] {
return this._items;
return this._items ?? [];
}
public set items(value: ModelEntryType[]) {
// Only want to set the model initially via this one, as this is the initial model, cause any re-render should not set this data again.
if (this._items !== undefined) return;
this._items = value;
this.#sorter.setModel(this._items);
}
private _items: ModelEntryType[] = [];
private _items?: ModelEntryType[];
#sorter = new UmbSorterController<ModelEntryType, ExampleSorterItem>(this, {
...SORTER_CONFIG,
/*performItemInsert: ({ item, newIndex }) => {
const oldValue = this._items;
//console.log('inserted', item.name, 'at', newIndex, ' ', this._items.map((x) => x.name).join(', '));
const newItems = [...this._items];
newItems.splice(newIndex, 0, item);
this.items = newItems;
this.requestUpdate('_items', oldValue);
return true;
getUniqueOfElement: (element) => {
return element.name;
},
performItemRemove: ({ item }) => {
const oldValue = this._items;
//console.log('removed', item.name, 'at', indexToMove, ' ', this._items.map((x) => x.name).join(', '));
const indexToMove = this._items.findIndex((x) => x.name === item.name);
const newItems = [...this._items];
newItems.splice(indexToMove, 1);
this.items = newItems;
this.requestUpdate('_items', oldValue);
return true;
getUniqueOfModel: (modelEntry) => {
return modelEntry.name;
},
performItemMove: ({ item, newIndex, oldIndex }) => {
const oldValue = this._items;
//console.log('move', item.name, 'from', oldIndex, 'to', newIndex, ' ', this._items.map((x) => x.name).join(', '));
const newItems = [...this._items];
newItems.splice(oldIndex, 1);
if (oldIndex <= newIndex) {
newIndex--;
}
newItems.splice(newIndex, 0, item);
this.items = newItems;
this.requestUpdate('_items', oldValue);
return true;
},*/
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);
this.requestUpdate('items', oldValue);
},
});
removeItem = (item: ModelEntryType) => {
this.items = this._items.filter((r) => r.name !== item.name);
this._items = this._items!.filter((r) => r.name !== item.name);
this.#sorter.setModel(this._items);
};
render() {
@@ -87,7 +56,7 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) {
(item) => item.name,
(item) =>
html`<example-sorter-item name=${item.name}>
<button @click=${() => this.removeItem(item)}>Delete</button>
<button slot="action" @click=${() => this.removeItem(item)}>Delete</button>
</example-sorter-item>`,
)}
</div>
@@ -105,6 +74,10 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) {
.sorter-placeholder {
opacity: 0.2;
}
.sorter-container {
min-height: 20px;
}
`,
];
}

View File

@@ -13,8 +13,11 @@ export class ExampleSorterItem extends UmbElementMixin(LitElement) {
render() {
return html`
${this.name}
<img src="https://picsum.photos/seed/${this.name}/400/400" style="width:120px;" />
<div>
${this.name}
<img src="https://picsum.photos/seed/${this.name}/400/400" style="width:120px;" />
<slot name="action"></slot>
</div>
<slot></slot>
`;
}
@@ -23,9 +26,7 @@ export class ExampleSorterItem extends UmbElementMixin(LitElement) {
UmbTextStyles,
css`
:host {
display: flex;
align-items: center;
justify-content: space-between;
display: block;
padding: var(--uui-size-layout-1);
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
@@ -34,6 +35,16 @@ export class ExampleSorterItem extends UmbElementMixin(LitElement) {
:host([drag-placeholder]) {
opacity: 0.2;
}
div {
display: flex;
align-items: center;
justify-content: space-between;
}
slot:not([name]) {
// go on new line:
}
`,
];
}

View File

@@ -21,11 +21,11 @@ export interface UmbBlockListLayoutModel extends UmbBlockLayoutBaseModel {}
export interface UmbBlockListValueModel extends UmbBlockValueType<UmbBlockListLayoutModel> {}
const SORTER_CONFIG: UmbSorterConfig<UmbBlockListLayoutModel, UmbPropertyEditorUIBlockListBlockElement> = {
compareElementToModel: (element, model) => {
return element.getAttribute('data-udi') === model.contentUdi;
getUniqueOfElement: (element) => {
return element.getAttribute('data-udi');
},
querySelectModelToElement: (container, modelEntry) => {
return container.querySelector("umb-property-editor-ui-block-list-block[data-udi='" + modelEntry.contentUdi + "']");
getUniqueOfModel: (modelEntry) => {
return modelEntry.contentUdi;
},
identifier: 'block-list-editor',
itemSelector: 'umb-property-editor-ui-block-list-block',

View File

@@ -1,12 +1,8 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { CSSResultGroup} from '@umbraco-cms/backoffice/external/lit';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat, query } from '@umbraco-cms/backoffice/external/lit';
import type {
UmbNotificationHandler,
UmbNotificationContext} from '@umbraco-cms/backoffice/notification';
import {
UMB_NOTIFICATION_CONTEXT,
} from '@umbraco-cms/backoffice/notification';
import type { UmbNotificationHandler, UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-backoffice-notification-container')
@@ -38,11 +34,11 @@ export class UmbBackofficeNotificationContainerElement extends UmbLitElement {
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._notificationsElement?.hidePopover();
this._notificationsElement?.hidePopover?.(); // To prevent issues in FireFox I added `?.` before `()` [NL]
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._notificationsElement?.showPopover();
this._notificationsElement?.showPopover?.(); // To prevent issues in FireFox I added `?.` before `()` [NL]
});
}

View File

@@ -16,12 +16,12 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
const SORTER_CONFIG: UmbSorterConfig<UmbSwatchDetails> = {
compareElementToModel: (element: HTMLElement, model: UmbSwatchDetails) => {
return element.getAttribute('data-sort-entry-id') === model.value;
const SORTER_CONFIG: UmbSorterConfig<UmbSwatchDetails, UmbMultipleColorPickerItemInputElement> = {
getUniqueOfElement: (element) => {
return element.value.toString();
},
querySelectModelToElement: (container: HTMLElement, modelEntry: UmbSwatchDetails) => {
return container.querySelector('[data-sort-entry-id=' + modelEntry.value + ']');
getUniqueOfModel: (modelEntry) => {
return modelEntry.value;
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-multiple-color-picker-item-input',
@@ -192,7 +192,6 @@ export class UmbMultipleColorPickerInputElement extends FormControlMixin(UmbLitE
html` <umb-multiple-color-picker-item-input
?showLabels=${this.showLabels}
value=${item.value}
data-sort-entry-id=${item.value}
label=${ifDefined(item.label)}
name="item-${index}"
@change=${(event: UmbChangeEvent) => this.#onChange(event, index)}

View File

@@ -4,7 +4,7 @@ import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbSorterConfig} from '@umbraco-cms/backoffice/sorter';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
export type MultipleTextStringValue = Array<MultipleTextStringValueItem>;
@@ -14,11 +14,11 @@ export interface MultipleTextStringValueItem {
}
const SORTER_CONFIG: UmbSorterConfig<MultipleTextStringValueItem> = {
compareElementToModel: (element: HTMLElement, model: MultipleTextStringValueItem) => {
return element.getAttribute('data-sort-entry-id') === model.value;
getUniqueOfElement: (element) => {
return element.getAttribute('data-sort-entry-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: MultipleTextStringValueItem) => {
return container.querySelector('[data-sort-entry-id=' + modelEntry.value + ']');
getUniqueOfModel: (modelEntry) => {
return modelEntry.value;
},
identifier: 'Umb.SorterIdentifier.ColorEditor',
itemSelector: 'umb-input-multiple-text-string-item',

View File

@@ -40,10 +40,6 @@ function getParentScrollElement(el: Element, includeSelf: boolean) {
return null;
}
function preventDragOver(e: Event) {
e.preventDefault();
}
function setupIgnorerElements(element: HTMLElement, ignorerSelectors: string) {
ignorerSelectors.split(',').forEach(function (criteria) {
element.querySelectorAll(criteria.trim()).forEach(setupPreventEvent);
@@ -62,9 +58,9 @@ function destroyPreventEvent(element: Element) {
}
type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
compareElementToModel: (el: ElementType, modelEntry: T) => boolean;
querySelectModelToElement: (container: HTMLElement, modelEntry: T) => ElementType | null;
identifier: string;
getUniqueOfElement: (element: ElementType) => string | null | symbol | number;
getUniqueOfModel: (modeEntry: T) => string | null | symbol | number;
identifier: string | symbol;
itemSelector: string;
disabledItemSelector?: string;
containerSelector: string;
@@ -101,9 +97,9 @@ type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
// External type with some properties optional, as they have defaults:
export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> = Omit<
INTERNAL_UmbSorterConfig<T, ElementType>,
'ignorerSelector' | 'containerSelector'
'ignorerSelector' | 'containerSelector' | 'identifier'
> &
Partial<Pick<INTERNAL_UmbSorterConfig<T, ElementType>, 'ignorerSelector' | 'containerSelector'>>;
Partial<Pick<INTERNAL_UmbSorterConfig<T, ElementType>, 'ignorerSelector' | 'containerSelector' | 'identifier'>>;
/**
* @export
@@ -112,6 +108,23 @@ export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> =
* @description This controller can make user able to sort items.
*/
export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElement> implements UmbController {
//
// A sorter that is requested to become the next sorter:
static originalSorter?: UmbSorterController<unknown>;
static originalIndex?: number;
// A sorter that is requested to become the next sorter:
static dropSorter?: UmbSorterController<unknown>;
// The sorter of which the element is located within:
static activeSorter?: UmbSorterController<unknown>;
// Information about the current dragged item/element:
static activeIndex?: number;
static activeItem?: any;
static activeElement?: HTMLElement;
static activeDragElement?: Element;
#host;
#config: INTERNAL_UmbSorterConfig<T, ElementType>;
#observer;
@@ -120,24 +133,20 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
#rqaId?: number;
#containerElement!: HTMLElement;
#currentContainerCtrl: UmbSorterController<T, ElementType> = this;
#currentContainerElement: Element | null = null;
#useContainerShadowRoot?: boolean;
#scrollElement?: Element | null;
#currentElement?: ElementType;
#currentDragElement?: Element;
#currentDragRect?: DOMRect;
#currentItem?: T;
#currentIndex?: number;
#dragX = 0;
#dragY = 0;
#lastIndicationContainerCtrl: UmbSorterController<T, ElementType> | null = null;
public get controllerAlias() {
// We only support one Sorter Controller pr. Controller Host.
return 'umbSorterController';
}
public get identifier() {
return this.#config.identifier;
}
@@ -145,6 +154,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#host = host;
// Set defaults:
config.identifier ??= Symbol();
config.ignorerSelector ??= 'a, img, iframe';
if (!config.placeholderClass && !config.placeholderAttr) {
config.placeholderAttr = 'drag-placeholder';
@@ -176,6 +186,14 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#model = model;
}
hasItem(unique: string) {
return this.#model.find((x) => this.#config.getUniqueOfModel(x) === unique) !== undefined;
}
getItem(unique: string) {
return this.#model.find((x) => this.#config.getUniqueOfModel(x) === unique);
}
hostConnected() {
requestAnimationFrame(this._onFirstRender);
}
@@ -188,19 +206,13 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#containerElement = containerEl as HTMLElement;
this.#useContainerShadowRoot = this.#containerElement === this.#host;
if (!this.#currentContainerElement || this.#currentContainerElement === this.#containerElement) {
this.#currentContainerElement = containerEl;
}
// Only look at the shadowRoot if the containerElement is host.
const containerElement = this.#useContainerShadowRoot
? this.#containerElement.shadowRoot ?? this.#containerElement
: this.#containerElement;
containerElement.addEventListener('dragover', preventDragOver);
containerElement.addEventListener('dragover', this._itemDraggedOver as unknown as EventListener);
(this.#containerElement as any)['__umbBlockGridSorterController'] = () => {
return this;
};
// TODO: Do we need to handle dragleave?
this.#observer.disconnect();
@@ -215,15 +227,50 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
});
};
hostDisconnected() {
// TODO: Clean up??
// TODO: Is there more clean up to do??
this.#observer.disconnect();
if (this.#containerElement) {
(this.#containerElement as any)['__umbBlockGridSorterController'] = undefined;
this.#containerElement.removeEventListener('dragover', preventDragOver);
// Only look at the shadowRoot if the containerElement is host.
const containerElement = this.#useContainerShadowRoot
? this.#containerElement.shadowRoot ?? this.#containerElement
: this.#containerElement;
containerElement.removeEventListener('dragover', this._itemDraggedOver as unknown as EventListener);
(this.#containerElement as any) = undefined;
}
}
_itemDraggedOver = (e: DragEvent) => {
//if(UmbSorterController.activeSorter === this) return;
const dropSorter = UmbSorterController.dropSorter as unknown as UmbSorterController<T, ElementType>;
if (!dropSorter || dropSorter.identifier !== this.identifier) return;
if (dropSorter === this) {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'move';
}
// Do nothing as we are the active sorter.
this.#handleDragMove(e);
// Maybe we need to stop the event in this case.
// Do not bubble up to parent sorters:
e.stopPropagation();
return;
} else {
// TODO: Check if dropping here is okay..
// If so lets set the approaching sorter:
UmbSorterController.dropSorter = this as unknown as UmbSorterController<unknown>;
// Do not bubble up to parent sorters:
e.stopPropagation();
}
};
setupItem(element: ElementType) {
if (this.#config.ignorerSelector) {
setupIgnorerElements(element, this.#config.ignorerSelector);
@@ -232,12 +279,15 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
if (!this.#config.disabledItemSelector || !element.matches(this.#config.disabledItemSelector)) {
element.draggable = true;
element.addEventListener('dragstart', this.#handleDragStart);
element.addEventListener('dragend', this.#handleDragEnd);
}
// If we have a currentItem and the element matches, we should set the currentElement to this element.
if (this.#currentItem && this.#config.compareElementToModel(element, this.#currentItem)) {
if (this.#currentElement !== element) {
console.log('THIS ACTUALLY HAPPENED... NOTICE THIS!');
if (
UmbSorterController.activeItem &&
this.#config.getUniqueOfElement(element) === this.#config.getUniqueOfModel(UmbSorterController.activeItem)
) {
if (UmbSorterController.activeElement !== element) {
this.#setCurrentElement(element);
}
}
@@ -249,33 +299,36 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
element.removeEventListener('dragstart', this.#handleDragStart);
// We are not ready to remove the dragend or drop, as this is might be the active one just moving container:
//element.removeEventListener('dragend', this.#handleDragEnd);
//element.addEventListener('drop', this.#handleDrop);
}
#setupPlaceholderStyle() {
if (this.#config.placeholderClass) {
this.#currentElement?.classList.add(this.#config.placeholderClass);
UmbSorterController.activeElement?.classList.add(this.#config.placeholderClass);
}
if (this.#config.placeholderAttr) {
this.#currentElement?.setAttribute(this.#config.placeholderAttr, '');
UmbSorterController.activeElement?.setAttribute(this.#config.placeholderAttr, '');
}
}
#removePlaceholderStyle() {
if (this.#config.placeholderClass) {
this.#currentElement?.classList.remove(this.#config.placeholderClass);
UmbSorterController.activeElement?.classList.remove(this.#config.placeholderClass);
}
if (this.#config.placeholderAttr) {
this.#currentElement?.removeAttribute(this.#config.placeholderAttr);
UmbSorterController.activeElement?.removeAttribute(this.#config.placeholderAttr);
}
}
#setCurrentElement(element: ElementType) {
this.#currentElement = element;
UmbSorterController.activeElement = element;
this.#currentDragElement = this.#config.draggableSelector
UmbSorterController.activeDragElement = this.#config.draggableSelector
? element.querySelector(this.#config.draggableSelector) ?? undefined
: element;
if (!this.#currentDragElement) {
if (!UmbSorterController.activeDragElement) {
throw new Error(
'Could not find drag element, query was made with the `draggableSelector` of "' +
this.#config.draggableSelector +
@@ -291,14 +344,15 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
const element = (event.target as HTMLElement).closest(this.#config.itemSelector);
if (!element) return;
if (this.#currentElement && this.#currentElement !== element) {
if (UmbSorterController.activeElement && UmbSorterController.activeElement !== element) {
// TODO: Remove this console log at one point.
console.log("drag start realized that something was already active, so we'll end it. -------!!!!#€#%#€");
this.#handleDragEnd();
}
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data.
event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped.
event.dataTransfer.effectAllowed = 'all'; // copyMove when we enhance the drag with clipboard data.// defaults to 'all'
}
if (!this.#scrollElement) {
@@ -306,56 +360,74 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
this.#setCurrentElement(element as ElementType);
this.#currentDragRect = this.#currentDragElement?.getBoundingClientRect();
this.#currentItem = this.getItemOfElement(this.#currentElement!);
if (!this.#currentItem) {
UmbSorterController.activeItem = this.getItemOfElement(UmbSorterController.activeElement! as ElementType);
UmbSorterController.originalSorter = this as unknown as UmbSorterController<unknown>;
UmbSorterController.originalIndex = this.#model.indexOf(UmbSorterController.activeItem);
if (!UmbSorterController.activeItem) {
console.error('Could not find item related to this element.');
return;
}
// Get the current index of the item:
this.#currentIndex = this.#model.indexOf(this.#currentItem);
UmbSorterController.activeIndex = this.#model.indexOf(UmbSorterController.activeItem as T);
this.#currentElement!.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
UmbSorterController.activeElement!.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
if (this.#config.dataTransferResolver) {
this.#config.dataTransferResolver(event.dataTransfer, this.#currentItem);
this.#config.dataTransferResolver(event.dataTransfer, UmbSorterController.activeItem as T);
}
if (this.#config.onStart) {
this.#config.onStart({ item: this.#currentItem, element: this.#currentElement! });
this.#config.onStart({
item: UmbSorterController.activeItem,
element: UmbSorterController.activeElement! as ElementType,
});
}
window.addEventListener('dragover', this.#handleDragMove);
window.addEventListener('dragend', this.#handleDragEnd);
// Assuming we can only drag one thing at the time.
UmbSorterController.activeSorter = this as unknown as UmbSorterController<unknown>;
UmbSorterController.dropSorter = this as unknown as UmbSorterController<unknown>;
// We must wait one frame before changing the look of the block.
this.#rqaId = requestAnimationFrame(() => {
// It should be okay to use the same rqaId, as the move does not, or is okay not, to happen on first frame/drag-move.
this.#rqaId = undefined;
if (this.#currentElement) {
this.#currentElement.style.transform = '';
this.#setupPlaceholderStyle();
if (UmbSorterController.activeElement) {
UmbSorterController.activeElement.style.transform = '';
}
});
return true;
};
#handleDragEnd = async () => {
window.removeEventListener('dragover', this.#handleDragMove);
window.removeEventListener('dragend', this.#handleDragEnd);
#handleDragEnd = async (event?: DragEvent) => {
// If not good drop, revert model?
if (!this.#currentElement || !this.#currentItem) {
if (!UmbSorterController.activeElement || !UmbSorterController.activeItem) {
return;
}
this.#currentElement.style.transform = '';
if (UmbSorterController.originalSorter && event?.dataTransfer != null && event.dataTransfer.dropEffect === 'none') {
// Revert move, to start position.
UmbSorterController.originalSorter.moveItemInModel(
UmbSorterController.originalIndex ?? 0,
UmbSorterController.activeSorter!,
);
}
UmbSorterController.activeElement.style.transform = '';
this.#removePlaceholderStyle();
this.#stopAutoScroll();
this.removeAllowIndication();
if (this.#config.onEnd) {
this.#config.onEnd({ item: this.#currentItem, element: this.#currentElement });
this.#config.onEnd({
item: UmbSorterController.activeItem,
element: UmbSorterController.activeElement as ElementType,
});
}
if (this.#rqaId) {
@@ -363,19 +435,19 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#rqaId = undefined;
}
this.#currentContainerElement = this.#containerElement;
this.#currentContainerCtrl = this;
this.#currentItem = undefined;
this.#currentElement = undefined;
this.#currentDragElement = undefined;
this.#currentDragRect = undefined;
UmbSorterController.activeItem = undefined;
UmbSorterController.activeElement = undefined;
UmbSorterController.activeDragElement = undefined;
UmbSorterController.activeSorter = undefined;
UmbSorterController.dropSorter = undefined;
UmbSorterController.originalIndex = undefined;
UmbSorterController.originalSorter = undefined;
this.#dragX = 0;
this.#dragY = 0;
};
#handleDragMove = (event: DragEvent) => {
if (!this.#currentElement) {
#handleDragMove(event: DragEvent) {
if (!UmbSorterController.activeElement) {
return;
}
@@ -394,77 +466,37 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.handleAutoScroll(this.#dragX, this.#dragY);
this.#currentDragRect = this.#currentDragElement!.getBoundingClientRect();
const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, this.#currentDragRect);
const activeDragRect = UmbSorterController.activeDragElement!.getBoundingClientRect();
const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, activeDragRect);
if (!insideCurrentRect) {
if (this.#rqaId === undefined) {
this.#rqaId = requestAnimationFrame(this.#updateDragMove);
}
}
}
};
}
#updateDragMove = () => {
this.#rqaId = undefined;
if (!this.#currentElement || !this.#currentContainerElement || !this.#currentItem) {
if (!UmbSorterController.activeElement || !UmbSorterController.activeItem) {
return;
}
const currentElementRect = this.#currentElement.getBoundingClientRect();
// Maybe no need to check this twice, like we do it before the RAF an inside it, I think its fine to choose one of them.
const currentElementRect = UmbSorterController.activeElement.getBoundingClientRect();
const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, currentElementRect);
if (insideCurrentRect) {
return;
}
let toBeCurrentContainerCtrl: UmbSorterController<T, ElementType> | undefined = undefined;
// If we have a boundarySelector, try it. If we didn't get anything fall back to currentContainerElement:
const currentBoundaryElement =
(this.#config.boundarySelector
? this.#currentContainerElement.closest(this.#config.boundarySelector)
: this.#currentContainerElement) ?? this.#currentContainerElement;
const currentBoundaryRect = currentBoundaryElement.getBoundingClientRect();
const currentContainerHasItems = this.#currentContainerCtrl.hasOtherItemsThan(this.#currentItem);
// if empty we will be move likely to accept an item (add 20px to the bounding box)
// If we have items we must be 10px within the container to accept the move.
const offsetEdge = currentContainerHasItems ? -10 : 20;
if (!isWithinRect(this.#dragX, this.#dragY, currentBoundaryRect, offsetEdge)) {
// we are outside the current container boundary, so lets see if there is a parent we can move to.
const parentNode = this.#currentContainerElement.parentNode;
if (parentNode && this.#config.containerSelector) {
// TODO: support multiple parent shadowDOMs?
const parentContainer = (parentNode as ShadowRoot).host
? (parentNode as ShadowRoot).host.closest(this.#config.containerSelector)
: (parentNode as HTMLElement).closest(this.#config.containerSelector);
if (parentContainer) {
const parentContainerCtrl = (parentContainer as any)['__umbBlockGridSorterController']();
if (parentContainerCtrl.unique === this.controllerAlias) {
this.#currentContainerElement = parentContainer as Element;
toBeCurrentContainerCtrl = parentContainerCtrl;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
element: this.#currentElement,
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
}
}
}
}
const containerElement = this.#useContainerShadowRoot
? this.#currentContainerElement.shadowRoot ?? this.#currentContainerElement
: this.#currentContainerElement;
? this.#containerElement.shadowRoot ?? this.#containerElement
: this.#containerElement;
// We want to retrieve the children of the container, every time to ensure we got the right order and index
const orderedContainerElements = Array.from(containerElement.querySelectorAll(this.#config.itemSelector));
const currentContainerRect = this.#currentContainerElement.getBoundingClientRect();
const currentContainerRect = this.#containerElement.getBoundingClientRect();
// gather elements on the same row.
const elementsInSameRow = [];
@@ -476,7 +508,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
const dragElement = this.#config.draggableSelector ? el.querySelector(this.#config.draggableSelector) : el;
if (dragElement) {
const dragElementRect = dragElement.getBoundingClientRect();
if (el !== this.#currentElement) {
if (el !== UmbSorterController.activeElement) {
elementsInSameRow.push({ el: el, dragRect: dragElementRect });
} else {
placeholderIsInThisRow = true;
@@ -502,65 +534,21 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
if (foundEl) {
// If we are on top or closest to our self, we should not do anything.
if (foundEl === this.#currentElement) {
if (foundEl === UmbSorterController.activeElement) {
return;
}
const isInsideFound = isWithinRect(this.#dragX, this.#dragY, foundElDragRect, 0);
// If we are inside the found element, lets look for sub containers.
// use the itemHasNestedContainersResolver, if not configured fallback to looking for the existence of a container via DOM.
// TODO: Ability to look into shadowDOMs for sub containers?
if (
isInsideFound && this.#config.itemHasNestedContainersResolver
? this.#config.itemHasNestedContainersResolver(foundEl)
: (foundEl as HTMLElement).querySelector(this.#config.containerSelector)
) {
// Find all sub containers:
const subLayouts = (foundEl as HTMLElement).querySelectorAll(this.#config.containerSelector);
for (const subLayoutEl of subLayouts) {
// Use boundary element or fallback to container element.
const subBoundaryElement =
(this.#config.boundarySelector ? subLayoutEl.closest(this.#config.boundarySelector) : subLayoutEl) ||
subLayoutEl;
const subBoundaryRect = subBoundaryElement.getBoundingClientRect();
const subContainerHasItems = subLayoutEl.querySelector(
this.#config.itemSelector + ':not(.' + this.#config.placeholderClass + ')',
);
// gather elements on the same row.
const subOffsetEdge = subContainerHasItems ? -10 : 20;
if (isWithinRect(this.#dragX, this.#dragY, subBoundaryRect, subOffsetEdge)) {
const subCtrl = (subLayoutEl as any)['__umbBlockGridSorterController']();
if (subCtrl.unique === this.controllerAlias) {
this.#currentContainerElement = subLayoutEl as HTMLElement;
toBeCurrentContainerCtrl = subCtrl;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
element: this.#currentElement,
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
this.#updateDragMove();
return;
}
}
}
}
// Indication if drop is good:
if (
this.updateAllowIndication(toBeCurrentContainerCtrl ?? this.#currentContainerCtrl, this.#currentItem) === false
) {
if (this.updateAllowIndication(UmbSorterController.activeItem) === false) {
return;
}
const verticalDirection = this.#config.resolveVerticalDirection
? this.#config.resolveVerticalDirection({
containerElement: this.#currentContainerElement,
containerElement: this.#containerElement,
containerRect: currentContainerRect,
item: this.#currentItem,
element: this.#currentElement,
item: UmbSorterController.activeItem,
element: UmbSorterController.activeElement as ElementType,
elementRect: currentElementRect,
relatedElement: foundEl,
relatedRect: foundElDragRect,
@@ -599,49 +587,55 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
const foundElIndex = orderedContainerElements.indexOf(foundEl);
const newIndex = placeAfter ? foundElIndex + 1 : foundElIndex;
this.#moveElementTo(toBeCurrentContainerCtrl, newIndex);
this.#moveElementTo(newIndex);
return;
}
// We skipped the above part cause we are above or below container:
// We skipped the above part cause we are above or below container, or within an empty container:
// Indication if drop is good:
if (
this.updateAllowIndication(toBeCurrentContainerCtrl ?? this.#currentContainerCtrl, this.#currentItem) === false
) {
if (this.updateAllowIndication(UmbSorterController.activeItem) === false) {
return;
}
if (this.#dragY < currentContainerRect.top) {
this.#moveElementTo(toBeCurrentContainerCtrl, 0);
if (this.#model.length === 0) {
// Here is no items, so we should just move into the top of the container.
this.#moveElementTo(0);
} else if (this.#dragY < currentContainerRect.top) {
this.#moveElementTo(0);
} else if (this.#dragY > currentContainerRect.bottom) {
this.#moveElementTo(toBeCurrentContainerCtrl, -1);
this.#moveElementTo(-1);
}
};
async #moveElementTo(containerCtrl: UmbSorterController<T, ElementType> | undefined, newIndex: number) {
if (!this.#currentElement) {
//
async #moveElementTo(newIndex: number) {
if (!UmbSorterController.activeElement || !UmbSorterController.activeSorter) {
return;
}
containerCtrl ??= this as UmbSorterController<T, ElementType>;
const requestingSorter = UmbSorterController.dropSorter;
if (!requestingSorter) {
throw new Error('Could not find requestingSorter');
}
// If same container and same index, do nothing:
if (this.#currentContainerCtrl === containerCtrl && this.#currentIndex === newIndex) return;
if (requestingSorter === UmbSorterController.activeSorter && UmbSorterController.activeIndex === newIndex) return;
if (await containerCtrl.moveItemInModel(newIndex, this.#currentElement, this.#currentContainerCtrl)) {
this.#currentContainerCtrl = containerCtrl;
this.#currentIndex = newIndex;
}
await requestingSorter.moveItemInModel(newIndex, UmbSorterController.activeSorter);
}
/** Management methods: */
public getItemOfElement(element: ElementType) {
if (!element) {
return undefined;
throw new Error('Element was not defined');
}
return this.#model.find((entry: T) => this.#config.compareElementToModel(element, entry));
const elementUnique = this.#config.getUniqueOfElement(element);
if (!elementUnique) {
throw new Error('Could not find unique of element');
}
return this.#model.find((entry: T) => elementUnique === this.#config.getUniqueOfModel(entry));
}
public async removeItem(item: T) {
@@ -663,13 +657,34 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
return false;
}
/*
public async insertItem(item: T, newIndex: number = 0) {
if (!item) {
return false;
}
if (this.#config.performItemInsert) {
const result = await this.#config.performItemInsert({ item, newIndex });
if (result === false) {
return false;
}
} else {
const newModel = [...this.#model];
newModel.splice(newIndex, 0, item);
this.#model = newModel;
this.#config.onChange?.({ model: newModel, item });
}
return false;
}
*/
public hasOtherItemsThan(item: T) {
return this.#model.filter((x) => x !== item).length > 0;
}
public async moveItemInModel(newIndex: number, element: ElementType, fromCtrl: UmbSorterController<T, ElementType>) {
const item = fromCtrl.getItemOfElement(element);
// TODO: Could get item via attr.
public async moveItemInModel(newIndex: number, fromCtrl: UmbSorterController<unknown>) {
const item = UmbSorterController.activeItem;
if (!item) {
console.error('Could not find item of sync item');
return false;
@@ -678,13 +693,17 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
return false;
}
const localMove = fromCtrl === this;
const localMove = fromCtrl === (this as any);
if (localMove) {
// Local move:
// TODO: Maybe this should be replaceable/configurable:
const oldIndex = this.#model.indexOf(item);
if (oldIndex === -1) {
console.error('Could not find item in model');
return false;
}
if (this.#config.performItemMove) {
const result = await this.#config.performItemMove({ item, newIndex, oldIndex });
@@ -701,6 +720,8 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#model = newModel;
this.#config.onChange?.({ model: newModel, item });
}
UmbSorterController.activeIndex = newIndex;
} else {
// Not a local move:
@@ -720,12 +741,18 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#model = newModel;
this.#config.onChange?.({ model: newModel, item });
}
// If everything went well, we can set new activeSorter to this:
UmbSorterController.activeSorter = this as unknown as UmbSorterController<unknown>;
UmbSorterController.activeIndex = newIndex;
}
return true;
}
updateAllowIndication(controller: UmbSorterController<T, ElementType>, item: T) {
updateAllowIndication(item: T) {
// TODO: Allow indication.
/*
// Remove old indication:
if (this.#lastIndicationContainerCtrl !== null && this.#lastIndicationContainerCtrl !== controller) {
this.#lastIndicationContainerCtrl.notifyAllowed();
@@ -739,6 +766,8 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
controller.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
return false;
*/
return true;
}
removeAllowIndication() {
// Remove old indication:
@@ -827,7 +856,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
destroy() {
// Do something when host element is destroyed.
if (this.#currentElement) {
if (UmbSorterController.activeElement) {
this.#handleDragEnd();
}

View File

@@ -1,194 +0,0 @@
import { Canvas, Meta } from '@storybook/addon-docs';
import * as LocalizeStories from './sorter.stories';
<Meta title="API/Drag and Drop/Intro" />
# Drag and Drop
Drag and Drop can be done by using the `UmbSorterController`
To get started using drag and drop, finish the following steps:
- Preparing the model
- Setting the configuration
- Registering the controller
#### Preparing the model
The SorterController needs a model to know what item it is we are dealing with.
```typescript
type MySortEntryType = {
id: string;
value: string;
};
const awesomeModel: Array<MySortEntryType> = [
{
id: '0',
value: 'Entry 0',
},
{
id: '1',
value: 'Entry 1',
},
{
id: '2',
value: 'Entry 2',
},
];
```
#### Setting the configuration
When you know the model of which that is being sorted, you can set up the configuration.
The configuration has a lot of optional options, but the required ones are:
- compareElementToModel()
- querySelectModelToElement()
- identifier
- itemSelector
- containerSelector
It can be set up as follows:
```typescript
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
type MySortEntryType = {...};
const awesomeModel: Array<MySortEntryType> = [...];
const MY_SORTER_CONFIG: UmbSorterConfig<MySortEntryType> = {
compareElementToModel: (element: HTMLElement, model: MySortEntryType) => {
return element.getAttribute('data-sort-entry-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: MySortEntryType) => {
return container.querySelector('[data-sort-entry-id=' + modelEntry.id + ']');
},
identifier: 'test-sorter',
itemSelector: 'li',
containerSelector: 'ul',
};
export class MyElement extends UmbElementMixin(LitElement) {
render() {
return html`
<ul>
${awesomeModel.map(
(entry) =>
html`<li data-sort-entry-id="${entry.id}">
<span>${entry.value}</span>
</li>`,
)}
</ul>
`;
}
}
```
#### Registering the controller
When the model and configuration are available we can register the controller and tell the controller what model we are using.
```typescript
import { UmbSorterController, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
type MySortEntryType = {...}
const awesomeModel: Array<MySortEntryType> = [...]
const MY_SORTER_CONFIG: UmbSorterConfig<MySortEntryType> = {...}
export class MyElement extends UmbElementMixin(LitElement) {
#sorter = new UmbSorterController(this, {...MY_SORTER_CONFIG,
onChange: ({ model }) => {
const oldValue = this.awesomeModel;
this.awesomeModel = model;
this.requestUpdate('awesomeModel', oldValue);
},
});
constructor() {
this.#sorter.setModel(awesomeModel);
}
render() {
return html`
<ul>
${awesomeModel.map(
(entry) =>
html`<li data-sort-entry-id="${entry.id}">
<span>${entry.value}</span>
</li>`,
)}
</ul>
`;
}
}
```
### Placeholder
While dragging an entry, the entry will get an additional class that can be styled.
The class is by default `--umb-sorter-placeholder` but can be changed via the configuration to a different value.
```typescript
const MY_SORTER_CONFIG: UmbSorterConfig<MySortEntryType> = {
...
placeholderClass: 'dragging-now',
};
```
```typescript
static styles = [
css`
li {
display:relative;
}
li.dragging-now span {
visibility: hidden;
}
li.dragging-now::after {
content: '';
position: absolute;
inset: 0px;
border: 1px dashed grey;
}
`,
];
```
### Horizontal sorting
By default, the sorter controller will sort vertically. You can sort your model horizontally by setting the `resolveVerticalDirection` to return false.
```typescript
const MY_SORTER_CONFIG: UmbSorterConfig<MySortEntryType> = {
...
resolveVerticalDirection: () => return false,
};
```
### Performing logic when using the controller (TODO: Better title)
Let's say your model has a property sortOrder that you would like to update when the entry is being sorted.
You can add your code logic in the configuration option `performItemInsert` and `performItemRemove`
```typescript
export class MyElement extends UmbElementMixin(LitElement) {
#sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
performItemInsert: ({ item, newIndex }) => {
// Insert logic that updates the model, so the item gets the new index in the model.
return true;
},
performItemRemove: () => {
// Insert logic that updates the model, so the item gets removed from the model.
return true;
},
});
}
```

View File

@@ -1,26 +0,0 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import type UmbTestSorterControllerElement from './test-sorter-controller.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './test-sorter-controller.element.js';
const meta: Meta<UmbTestSorterControllerElement> = {
title: 'API/Drag and Drop/Sorter',
component: 'test-my-sorter-controller',
decorators: [
(Story) => {
return html`<div
style="margin:2rem auto; width: 50%; min-height: 350px; padding: 20px; box-sizing: border-box; background:white;">
<p>
<strong>Drag and drop the items to sort them.</strong>
</p>
${Story()}
</div>`;
},
],
};
export default meta;
type Story = StoryObj<UmbTestSorterControllerElement>;
export const Default: Story = {};

View File

@@ -1,131 +0,0 @@
import type { UmbSorterConfig} from '../sorter.controller.js';
import { UmbSorterController } from '../sorter.controller.js';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
type SortEntryType = {
id: string;
value: string;
};
const SORTER_CONFIG: UmbSorterConfig<SortEntryType> = {
compareElementToModel: (element: HTMLElement, model: SortEntryType) => {
return element.getAttribute('data-sort-entry-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: SortEntryType) => {
return container.querySelector('[data-sort-entry-id=' + modelEntry.id + ']');
},
identifier: 'test-sorter',
itemSelector: 'li',
containerSelector: 'ul',
};
const model: Array<SortEntryType> = [
{
id: '0',
value: 'Entry 0',
},
{
id: '1',
value: 'Entry 1',
},
{
id: '2',
value: 'Entry 2',
},
];
@customElement('test-my-sorter-controller')
export default class UmbTestSorterControllerElement extends UmbLitElement {
public sorter;
@state()
private vertical = true;
@state()
private _items: Array<SortEntryType> = [...model];
constructor() {
super();
this.sorter = new UmbSorterController(this, {
...SORTER_CONFIG,
resolveVerticalDirection: () => {
this.vertical ? true : false;
},
onChange: ({ model }) => {
const oldValue = this._items;
this._items = model;
this.requestUpdate('_items', oldValue);
},
});
this.sorter.setModel(model);
}
#toggle() {
this.vertical = !this.vertical;
}
render() {
return html`
<uui-button label="Change direction" look="outline" color="positive" @click=${this.#toggle}>
Horizontal/Vertical
</uui-button>
<ul class="${this.vertical ? 'vertical' : 'horizontal'}">
${this._items.map(
(entry) =>
html`<li class="item" data-sort-entry-id="${entry.id}">
<span><uui-icon name="icon-wand"></uui-icon>${entry.value}</span>
</li>`,
)}
</ul>
`;
}
static styles = [
css`
:host {
display: block;
box-sizing: border-box;
}
ul {
display: flex;
flex-direction: column;
gap: 5px;
list-style: none;
padding: 0;
margin: 10px 0;
}
ul.horizontal {
flex-direction: row;
}
li {
cursor: grab;
position: relative;
flex: 1;
border-radius: var(--uui-border-radius);
}
li span {
display: flex;
align-items: center;
gap: 5px;
padding: 10px;
background-color: rgba(0, 255, 0, 0.3);
}
li.--umb-sorter-placeholder span {
visibility: hidden;
}
li.--umb-sorter-placeholder::after {
content: '';
position: absolute;
inset: 0px;
border-radius: var(--uui-border-radius);
border: 1px dashed var(--uui-color-divider-emphasis);
}
`,
];
}

View File

@@ -1,15 +1,12 @@
import type {
DocumentTypePropertyTypeContainerResponseModel,
PropertyTypeContainerModelBaseModel,
} from '@umbraco-cms/backoffice/backend-api';
import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG_HORIZONTAL: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeContainerResponseModel) => {
return element.getAttribute('data-umb-tabs-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-tabs-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector(`[data-umb-tabs-id='` + modelEntry.id + `']`);
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-tabs-sorter',
itemSelector: '[data-umb-tabs-id]',
@@ -21,11 +18,11 @@ const SORTER_CONFIG_HORIZONTAL: UmbSorterConfig<PropertyTypeContainerModelBaseMo
};
const SORTER_CONFIG_VERTICAL: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeContainerResponseModel) => {
return element.getAttribute('data-umb-property-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-property-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector(`[data-umb-property-id='` + modelEntry.id + `']`);
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',

View File

@@ -12,11 +12,11 @@ import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
const SORTER_CONFIG: UmbSorterConfig<UmbPropertyTypeModel> = {
compareElementToModel: (element: HTMLElement, model: UmbPropertyTypeModel) => {
return element.getAttribute('data-umb-property-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-property-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: UmbPropertyTypeModel) => {
return container.querySelector('[data-umb-property-id=' + modelEntry.id + ']');
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',

View File

@@ -12,11 +12,11 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import './document-type-workspace-view-edit-properties.element.js';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: PropertyTypeContainerModelBaseModel) => {
return element.getAttribute('data-umb-group-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-group-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector('data-umb-group-id=[' + modelEntry.id + ']');
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-group-sorter',
itemSelector: '[data-umb-group-id]',

View File

@@ -6,10 +6,7 @@ import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/ext
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type {
DocumentTypePropertyTypeContainerResponseModel,
PropertyTypeContainerModelBaseModel,
} from '@umbraco-cms/backoffice/backend-api';
import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
@@ -20,11 +17,11 @@ import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeContainerResponseModel) => {
return element.getAttribute('data-umb-tabs-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-tabs-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector(`[data-umb-tabs-id='` + modelEntry.id + `']`);
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-tabs-sorter',
itemSelector: '[data-umb-tabs-id]',

View File

@@ -9,10 +9,12 @@ import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffi
import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document';
const SORTER_CONFIG: UmbSorterConfig<string> = {
compareElementToModel: (element, model) => {
return element.getAttribute('detail') === model;
getUniqueOfElement: (element) => {
return element.getAttribute('detail');
},
getUniqueOfModel: (modelEntry) => {
return modelEntry;
},
querySelectModelToElement: () => null,
identifier: 'Umb.SorterIdentifier.InputDocument',
itemSelector: 'uui-ref-node',
containerSelector: 'uui-ref-list',

View File

@@ -12,11 +12,11 @@ import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
const SORTER_CONFIG: UmbSorterConfig<UmbPropertyTypeModel> = {
compareElementToModel: (element: HTMLElement, model: UmbPropertyTypeModel) => {
return element.getAttribute('data-umb-property-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-tabs-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: UmbPropertyTypeModel) => {
return container.querySelector('[data-umb-property-id=' + modelEntry.id + ']');
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',

View File

@@ -12,11 +12,11 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import './media-type-workspace-view-edit-properties.element.js';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: PropertyTypeContainerModelBaseModel) => {
return element.getAttribute('data-umb-group-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-group-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector('data-umb-group-id=[' + modelEntry.id + ']');
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-group-sorter',
itemSelector: '[data-umb-group-id]',

View File

@@ -6,10 +6,7 @@ import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/ext
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type {
MediaTypePropertyTypeContainerResponseModel,
PropertyTypeContainerModelBaseModel,
} from '@umbraco-cms/backoffice/backend-api';
import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
@@ -20,11 +17,11 @@ import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: MediaTypePropertyTypeContainerResponseModel) => {
return element.getAttribute('data-umb-tabs-id') === model.id;
getUniqueOfElement: (element) => {
return element.getAttribute('data-umb-tabs-id');
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector(`[data-umb-tabs-id='` + modelEntry.id + `']`);
getUniqueOfModel: (modelEntry) => {
return modelEntry.id;
},
identifier: 'content-type-tabs-sorter',
itemSelector: '[data-umb-tabs-id]',

View File

@@ -8,10 +8,12 @@ import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbra
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<string> = {
compareElementToModel: (element, model) => {
return element.getAttribute('detail') === model;
getUniqueOfElement: (element) => {
return element.getAttribute('detail');
},
getUniqueOfModel: (modelEntry) => {
return modelEntry;
},
querySelectModelToElement: () => null,
identifier: 'Umb.SorterIdentifier.InputMedia',
itemSelector: 'uui-card-media',
containerSelector: '.container',

View File

@@ -7,10 +7,12 @@ import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbra
import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<string> = {
compareElementToModel: (element, model) => {
return element.getAttribute('detail') === model;
getUniqueOfElement: (element) => {
return element.getAttribute('detail');
},
getUniqueOfModel: (modelEntry) => {
return modelEntry;
},
querySelectModelToElement: () => null,
identifier: 'Umb.SorterIdentifier.InputMember',
itemSelector: 'uui-ref-node',
containerSelector: 'uui-ref-list',