Merge pull request #1266 from umbraco/feature/block-grid-editor-take-3

Block Grid Editor Block Type Permissions
This commit is contained in:
Niels Lyngsø
2024-02-22 10:01:12 +01:00
committed by GitHub
18 changed files with 302 additions and 125 deletions

View File

@@ -7,6 +7,7 @@
"ctrls",
"devs",
"Elementable",
"iframes",
"invariantable",
"lucide",
"Niels",

View File

@@ -113,18 +113,20 @@ export const UmbControllerHostMixin = <T extends ClassConstructor>(superClass: T
destroy() {
let ctrl: UmbController | undefined;
//let prev = null;
let prev = null;
// Note: A very important way of doing this loop, as foreach will skip over the next item if the current item is removed.
while ((ctrl = this.#controllers[0])) {
ctrl.destroy();
/*
//This code can help debug if there is some controller that does not destroy properly: (When a controller is destroyed it should remove it self)
// Help developer realize that they made a mistake in code:
if (ctrl === prev) {
console.log('WUPS, we have a controller that does not destroy it self');
debugger;
throw new Error(
`Controller with controller alias: '${ctrl.controllerAlias?.toString()}' and class name: '${
(ctrl as any).constructor.name
}', does not remove it self when destroyed. This can cause memory leaks. Please fix this issue.`,
);
}
prev = ctrl;
*/
}
this.#controllers.length = 0;
}

View File

@@ -674,6 +674,8 @@ export const data: Array<UmbMockDataTypeModel> = [
{
label: 'Mocked Block Type for Block Grid',
contentElementTypeKey: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c',
allowAtRoot: true,
allowInAreas: true,
rowMinSpan: 1,
rowMaxSpan: 2,
columnSpanOptions: [
@@ -720,7 +722,7 @@ export const data: Array<UmbMockDataTypeModel> = [
iconColor: '#FFFDD0',
backgroundColor: '#633f32',
editorSize: 'medium',
icon: 'icon-coffee',
allowInAreas: true,
},
{
@@ -728,37 +730,36 @@ export const data: Array<UmbMockDataTypeModel> = [
contentElementTypeKey: 'headline-umbraco-demo-block-id',
backgroundColor: 'gold',
editorSize: 'medium',
icon: 'icon-edit',
groupKey: 'demo-block-group-id',
allowInAreas: true,
},
{
label: 'Image',
contentElementTypeKey: 'image-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-picture',
groupKey: 'demo-block-group-id',
allowInAreas: true,
},
{
label: 'Rich Text',
contentElementTypeKey: 'rich-text-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-diploma',
groupKey: 'demo-block-group-id',
allowInAreas: true,
},
{
label: 'Two Column Layout',
contentElementTypeKey: 'two-column-layout-umbraco-demo-block-id',
editorSize: 'medium',
icon: 'icon-book-alt',
groupKey: 'demo-block-group-id',
allowAtRoot: false,
},
{
label: 'Test broken group key',
contentElementTypeKey: 'test-block-id',
editorSize: 'medium',
icon: 'icon-war',
groupKey: 'group-id-that-does-not-exist',
allowAtRoot: true,
},
],
},

View File

@@ -143,6 +143,15 @@ export class UmbBlockGridEntriesElement extends UmbLitElement {
onChange: ({ model }) => {
this.#context.setLayouts(model);
},
onRequestMove: ({ item }) => {
return this.#context.allowDrop(item.contentUdi);
},
onDisallowed: () => {
this.setAttribute('disallow-drop', '');
},
onAllowed: () => {
this.removeAttribute('disallow-drop');
},
});
#context = new UmbBlockGridEntriesContext(this);
@@ -241,9 +250,29 @@ export class UmbBlockGridEntriesElement extends UmbLitElement {
UmbTextStyles,
css`
:host {
position: relative;
display: grid;
gap: 1px;
}
:host([disallow-drop])::before {
content: '';
position: absolute;
z-index: 1;
inset: 0;
border: 2px solid var(--uui-color-danger);
border-radius: calc(var(--uui-border-radius) * 2);
pointer-events: none;
}
:host([disallow-drop])::after {
content: '';
position: absolute;
z-index: 1;
inset: 0;
border-radius: calc(var(--uui-border-radius) * 2);
background-color: var(--uui-color-danger);
opacity: 0.2;
pointer-events: none;
}
> div {
display: flex;
flex-direction: column;

View File

@@ -1,7 +1,7 @@
import type { UmbBlockDataType } from '../../block/index.js';
import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '../../block/index.js';
import { UMB_BLOCK_GRID_ENTRY_CONTEXT, type UmbBlockGridWorkspaceData } from '../index.js';
import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../types.js';
import type { UmbBlockGridLayoutModel, UmbBlockGridTypeAreaType, UmbBlockGridTypeModel } from '../types.js';
import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context.js';
import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js';
import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api';
@@ -25,6 +25,8 @@ export class UmbBlockGridEntriesContext
#layoutColumns = new UmbNumberState(undefined);
readonly layoutColumns = this.#layoutColumns.asObservable();
#areaType?: UmbBlockGridTypeAreaType;
//#parentUnique?: string;
#areaKey?: string | null;
@@ -71,8 +73,8 @@ export class UmbBlockGridEntriesContext
const index = routingInfo.index ? parseInt(routingInfo.index) : -1;
return {
data: {
blocks: [],
blockGroups: [],
blocks: this.#retrieveAllowedElementTypes(),
blockGroups: this._manager?.getBlockGroups() ?? [],
openClipboard: routingInfo.view === 'clipboard',
blockOriginData: { index: index },
},
@@ -180,6 +182,7 @@ export class UmbBlockGridEntriesContext
this.observe(
this.#parentEntry.areaType(this.#areaKey),
(areaType) => {
this.#areaType = areaType;
const hostEl = this.getHostElement() as HTMLElement | undefined;
if (!hostEl) return;
hostEl.setAttribute('data-area-alias', areaType?.alias ?? '');
@@ -229,4 +232,61 @@ export class UmbBlockGridEntriesContext
// TODO: Loop through children and delete them as well?
await super.delete(contentUdi);
}
/**
* @internal
* @returns an Array of ElementTypeKeys that are allowed in the current area. Or undefined if not ready jet.
*/
#retrieveAllowedElementTypes() {
if (!this._manager) return [];
if (this.#areaKey) {
// Area entries:
if (!this.#areaType) return [];
if (this.#areaType.specifiedAllowance && this.#areaType.specifiedAllowance.length > 0) {
return this.#areaType.specifiedAllowance
.flatMap((permission) => {
if (permission.groupKey) {
return (
this._manager
?.getBlockTypes()
.filter(
(blockType) => blockType.groupKey === permission.groupKey && blockType.allowInAreas === true,
) ?? []
);
} else if (permission.elementTypeKey) {
return (
this._manager?.getBlockTypes().filter((x) => x.contentElementTypeKey === permission.elementTypeKey) ??
[]
);
}
return [];
})
.filter((v, i, a) => a.find((x) => x.contentElementTypeKey === v.contentElementTypeKey) === undefined);
}
return this._manager.getBlockTypes().filter((x) => x.allowInAreas);
}
// If no AreaKey, then we are representing the items of the root:
// Root entries:
return this._manager.getBlockTypes().filter((x) => x.allowAtRoot);
}
/**
* Check if given contentUdi is allowed in the current area.
* @param contentUdi {string} - The contentUdi of the content to check.
* @returns {boolean} - True if the content is allowed in the current area, otherwise false.
*/
allowDrop(contentUdi: string) {
const content = this._manager?.getContentOf(contentUdi);
if (!content) return false;
return (
this.#retrieveAllowedElementTypes()
.map((x) => x.contentElementTypeKey)
.indexOf(content.contentTypeKey) !== -1
);
}
}

View File

@@ -184,7 +184,6 @@ export class UmbBlockGridEntryContext
const hasRelevantColumnSpanOptions = relevantColumnSpanOptions.length > 1;
const hasRowSpanOptions = minMaxRowSpan[0] !== minMaxRowSpan[1];
const canScale = hasRelevantColumnSpanOptions || hasRowSpanOptions;
console.log('canScale calc:', canScale);
this.#canScale.setValue(canScale);
},

View File

@@ -8,15 +8,15 @@ import {
} from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import {
UMB_BLOCK_GRID_TYPE,
type UmbBlockGridTypeGroupType,
type UmbBlockGridGroupTypeConfiguration,
} from '@umbraco-cms/backoffice/block';
import { UMB_BLOCK_GRID_TYPE, type UmbBlockGridTypeGroupType } from '@umbraco-cms/backoffice/block';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UMB_PROPERTY_DATASET_CONTEXT, type UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
interface MappedGroupWithBlockTypes extends Partial<UmbBlockGridTypeGroupType> {
blocks: Array<UmbBlockTypeWithGroupKey>;
}
/**
* @element umb-property-editor-ui-block-grid-type-configuration
*/
@@ -47,7 +47,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
private _blockGroups: Array<UmbBlockGridTypeGroupType> = [];
@state()
private _mappedValuesAndGroups: Array<UmbBlockGridGroupTypeConfiguration> = [];
private _mappedValuesAndGroups: Array<MappedGroupWithBlockTypes> = [];
@state()
private _workspacePath?: string;

View File

@@ -1,10 +1,10 @@
import type { UmbBlockTypeBaseModel, UmbBlockTypeWithGroupKey } from '../block-type/index.js';
import type { UmbBlockTypeWithGroupKey } from '../block-type/index.js';
import type { UmbBlockLayoutBaseModel, UmbBlockValueType } from '../index.js';
export const UMB_BLOCK_GRID_TYPE = 'block-grid-type';
// Configuration models:
export interface UmbBlockGridTypeModel extends UmbBlockTypeBaseModel {
export interface UmbBlockGridTypeModel extends UmbBlockTypeWithGroupKey {
columnSpanOptions: Array<UmbBlockGridTypeColumnSpanOption>;
allowAtRoot: boolean;
allowInAreas: boolean;
@@ -41,10 +41,6 @@ export interface UmbBlockGridTypeGroupType {
key: string;
}
export interface UmbBlockGridGroupTypeConfiguration extends Partial<UmbBlockGridTypeGroupType> {
blocks: Array<UmbBlockTypeWithGroupKey>;
}
// Content models:
export interface UmbBlockGridValueModel extends UmbBlockValueType<UmbBlockGridLayoutModel> {}

View File

@@ -26,7 +26,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
const index = routingInfo.index ? parseInt(routingInfo.index) : -1;
return {
data: {
blocks: [],
blocks: this._manager?.getBlockTypes() ?? [],
blockGroups: [],
openClipboard: routingInfo.view === 'clipboard',
blockOriginData: { index: index },

View File

@@ -1,10 +1,10 @@
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbBlockListManagerContext } from '../../context/block-list-manager.context.js';
import '../../components/block-list-entry/index.js';
import type { UmbBlockListEntryElement } from '../../components/block-list-entry/index.js';
import type { UmbBlockListLayoutModel, UmbBlockListValueModel } from '../../types.js';
import { UmbBlockListEntriesContext } from '../../context/block-list-entries.context.js';
import { UMB_BLOCK_LIST_PROPERTY_EDITOR_ALIAS } from './manifests.js';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, customElement, property, state, repeat, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
@@ -90,7 +90,7 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
const useInlineEditingAsDefault = config.getValueByAlias<boolean>('useInlineEditingAsDefault');
this.#managerContext.setInlineEditingMode(useInlineEditingAsDefault);
// TODO:
//config.useSingleBlockMode, not done jey
//config.useSingleBlockMode, not done jet
this.style.maxWidth = config.getValueByAlias<string>('maxPropertyWidth') ?? '';
this.#managerContext.setEditorConfiguration(config);

View File

@@ -2,9 +2,7 @@ import {
DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS,
type UmbDocumentTypeItemModel,
} from '@umbraco-cms/backoffice/document-type';
import { UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -18,27 +16,38 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
);
@property({ type: String, attribute: false })
workspacePath?: string;
href?: string;
@property({ type: String, attribute: false })
public get key(): string | undefined {
return this._key;
name?: string;
@property({ type: String, attribute: false })
iconColor?: string;
@property({ type: String, attribute: false })
backgroundColor?: string;
// TODO: support custom icon/image file
@property({ type: String, attribute: false })
public get contentElementTypeKey(): string | undefined {
return this._elementTypeKey;
}
public set key(value: string | undefined) {
this._key = value;
public set contentElementTypeKey(value: string | undefined) {
this._elementTypeKey = value;
if (value) {
this.#itemManager.setUniques([value]);
} else {
this.#itemManager.setUniques([]);
}
}
private _key?: string | undefined;
private _elementTypeKey?: string | undefined;
@state()
_name?: string;
_fallbackName?: string;
@state()
_icon?: string | null;
_fallbackIcon?: string | null;
constructor() {
super();
@@ -46,37 +55,22 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
this.observe(this.#itemManager.items, (items) => {
const item = items[0];
if (item) {
this._icon = item.icon;
this._name = item.name;
console.log('got item', item);
this._fallbackIcon = item.icon;
this._fallbackName = item.name;
}
});
}
#onRequestDelete() {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, async (modalManager) => {
const modalContext = modalManager.open(UMB_CONFIRM_MODAL, {
data: {
color: 'danger',
headline: `Remove ${this._name}?`,
content: 'Are you sure you want to remove this block type?',
confirmLabel: 'Remove',
},
});
await modalContext?.onSubmit();
this.dispatchEvent(new UmbDeleteEvent());
});
}
// TODO: Support image files instead of icons.
render() {
return html`
<uui-card-block-type href="${this.workspacePath}/edit/${this.key}" .name=${this._name ?? ''}>
<uui-icon name=${this._icon ?? ''}></uui-icon>
<uui-action-bar slot="actions">
<uui-button @click=${this.#onRequestDelete} label="Remove block">
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
</uui-action-bar>
<uui-card-block-type
href=${ifDefined(this.href)}
.name=${this.name ?? this._fallbackName ?? ''}
.background=${this.backgroundColor}>
<uui-icon name=${this._fallbackIcon ?? ''} style="color:${this.iconColor}"></uui-icon>
<slot name="actions" slot="actions"> </slot>
</uui-card-block-type>
`;
}

View File

@@ -1,5 +1,9 @@
import type { UmbBlockTypeBaseModel } from '../../types.js';
import { UMB_DOCUMENT_TYPE_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import {
UMB_CONFIRM_MODAL,
UMB_DOCUMENT_TYPE_PICKER_MODAL,
UMB_MODAL_MANAGER_CONTEXT,
} from '@umbraco-cms/backoffice/modal';
import '../block-type-card/index.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -74,18 +78,41 @@ export class UmbInputBlockTypeElement<
return undefined;
}
#onRequestDelete(item: BlockType) {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, async (modalManager) => {
const modalContext = modalManager.open(UMB_CONFIRM_MODAL, {
data: {
color: 'danger',
headline: `Remove [TODO: Get name]?`,
content: 'Are you sure you want to remove this block type?',
confirmLabel: 'Remove',
},
});
await modalContext?.onSubmit();
this.deleteItem(item.contentElementTypeKey);
});
}
render() {
return html`<div>
${repeat(this.value, (block) => block.contentElementTypeKey, this.#renderItem)} ${this.#renderButton()}
</div>`;
}
#renderItem = (item: BlockType) => {
#renderItem = (block: BlockType) => {
return html`
<umb-block-type-card
.workspacePath=${this.workspacePath}
.key=${item.contentElementTypeKey}
@delete=${() => this.deleteItem(item.contentElementTypeKey)}>
.name=${block.label}
.iconColor=${block.iconColor}
.backgroundColor=${block.backgroundColor}
.href="${this.workspacePath}/edit/${block.contentElementTypeKey}"
.contentElementTypeKey=${block.contentElementTypeKey}>
<uui-action-bar slot="actions">
<uui-button @click=${this.#onRequestDelete} label="Remove block">
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
</uui-action-bar>
</umb-block-type-card>
`;
};

View File

@@ -9,12 +9,11 @@ export interface UmbBlockTypeBaseModel {
iconColor?: string;
backgroundColor?: string;
editorSize?: UUIModalSidebarSize;
icon?: string; // remove later
forceHideContentEditorInOverlay: boolean;
}
export interface UmbBlockTypeGroup {
name?: string | null;
name?: string;
key: string;
}

View File

@@ -141,6 +141,13 @@ export abstract class UmbBlockManagerContext<
return this.#settings.asObservablePart((source) => source.find((x) => x.udi === udi));
}
getBlockTypeOf(contentTypeKey: string) {
return this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentTypeKey);
}
getContentOf(contentUdi: string) {
return this.#contents.value.find((x) => x.udi === contentUdi);
}
/*setOneLayout(layoutData: BlockLayoutType) {
return this._layouts.appendOne(layoutData);
}*/

View File

@@ -5,7 +5,7 @@ import type {
UmbBlockTypeGroup,
UmbBlockTypeWithGroupKey,
} from '@umbraco-cms/backoffice/block';
import { css, html, customElement, state, repeat, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import {
UMB_MODAL_CONTEXT,
UmbModalBaseElement,
@@ -58,7 +58,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
const noGroupBlocks = blocks.filter((block) => !blockGroups.find((group) => group.key === block.groupKey));
const grouped = blockGroups.map((group) => ({
name: group.name ?? '',
name: group.name,
blocks: blocks.filter((block) => block.groupKey === group.key),
}));
@@ -87,26 +87,28 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
#renderCreateEmpty() {
return html`
${this._groupedBlocks.map(
(group) => html`
${group.name ? html`<h4>${group.name}</h4>` : nothing}
<div class="blockGroup">
${repeat(
group.blocks,
(block) => block.contentElementTypeKey,
(block) => html`
<uui-card-block-type
name=${ifDefined(block.label)}
background=${ifDefined(block.backgroundColor)}
style="color: ${block.iconColor}"
href="${this._workspacePath}create/${block.contentElementTypeKey}">
<uui-icon .name=${block.icon ?? ''}></uui-icon>
</uui-card-block-type>
`,
)}
</div>
`,
)}
${this._groupedBlocks
? this._groupedBlocks.map(
(group) => html`
${group.name && group.name !== '' ? html`<h4>${group.name}</h4>` : nothing}
<div class="blockGroup">
${repeat(
group.blocks,
(block) => block.contentElementTypeKey,
(block) => html`
<umb-block-type-card
.name=${block.label}
.iconColor=${block.iconColor}
.backgroundColor=${block.backgroundColor}
.contentElementTypeKey=${block.contentElementTypeKey}
.href="${this._workspacePath}create/${block.contentElementTypeKey}">
</umb-block-type-card>
`,
)}
</div>
`,
)
: ''}
`;
}

View File

@@ -324,6 +324,7 @@ export class UmbBlockWorkspaceContext<
};
public destroy(): void {
super.destroy();
this.#layout.destroy();
}
}

View File

@@ -209,7 +209,6 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement)
}
#renderFiles() {
console.log('files', this._files);
return repeat(
this._files,
(path) => path,

View File

@@ -74,27 +74,79 @@ export type resolveVerticalDirectionArgs<T, ElementType extends HTMLElement> = {
type INTERNAL_UmbSorterConfig<T, ElementType extends HTMLElement> = {
getUniqueOfElement: (element: ElementType) => string | null | symbol | number;
getUniqueOfModel: (modeEntry: T) => string | null | symbol | number;
/**
* Optionally define a unique identifier for each sorter experience, all Sorters that uses the same identifier to connect with other sorters.
*/
identifier: string | symbol;
/**
* A query selector for the item element.
*/
itemSelector: string;
disabledItemSelector?: string;
/**
* A selector for the container element, if not defined the host element will be used as container.
*/
containerSelector: string;
/**
* A selector for elements to ignore, elements that should not be draggable when within an draggable item, This defaults to links, images & iframes.
*/
ignorerSelector: string;
/**
* An class to set on the placeholder element.
*/
placeholderClass?: string;
/**
* An attribute to set on the placeholder element.
*/
placeholderAttr?: string;
/**
* The selector to find the draggable element within the item.
*/
draggableSelector?: string;
boundarySelector?: string;
//boundarySelector?: string;
dataTransferResolver?: (dataTransfer: DataTransfer | null, currentItem: T) => void;
onStart?: (argument: { item: T; element: ElementType }) => void;
/**
* This callback is executed every time where is a change to this model, this could be a move, insert or remove.
* But notice its not called if a more specific callback is provided, such would be the performItemMove, performItemInsert or performItemRemove or performItemRemove.
*/
onChange?: (argument: { item: T; model: Array<T> }) => void;
onContainerChange?: (argument: { item: T; element: ElementType }) => void;
/**
* This callback is executed when an item is moved from another container to this container.
*/
onContainerChange?: (argument: { item: T; model: Array<T>; from: UmbSorterController<T, ElementType> }) => void;
onEnd?: (argument: { item: T; element: ElementType }) => void;
itemHasNestedContainersResolver?: (element: HTMLElement) => boolean;
onDisallowed?: () => void;
onAllowed?: () => void;
onRequestDrop?: (argument: { item: T }) => boolean | void;
resolveVerticalDirection?: (argument: resolveVerticalDirectionArgs<T, ElementType>) => void;
/**
* Callback when a item move is disallowed.
* This should make a visual indication for the user to understand that the move is not allowed.
*/
onDisallowed?: (argument: { item: T; element: ElementType }) => void;
/**
* Callback when a item move is allowed.
* This should remove any visual indication of the disallowing, reverting the work of the onDisallowed callback.
*/
onAllowed?: (argument: { item: T; element: ElementType }) => void;
/**
* Callback when user tries to move an item from another Sorter to this Sorter, return true or false to allow or disallow the move.
*/
onRequestMove?: (argument: { item: T }) => boolean;
/**
* This callback is executed when an item is hovered within this container.
* The callback should return true if the item should be placed after based on a vertical logic. Other wise false for horizontal. True is default.
*/
resolveVerticalDirection?: (argument: resolveVerticalDirectionArgs<T, ElementType>) => boolean;
/**
* This callback is executed when an item is moved within this container.
*/
performItemMove?: (argument: { item: T; newIndex: number; oldIndex: number }) => Promise<boolean> | boolean;
/**
* This callback is executed when an item should be inserted into this container.
*/
performItemInsert?: (argument: { item: T; newIndex: number }) => Promise<boolean> | boolean;
/**
* This callback is executed when an item should be removed from this container.
*/
performItemRemove?: (argument: { item: T }) => Promise<boolean> | boolean;
};
@@ -113,6 +165,9 @@ export type UmbSorterConfig<T, ElementType extends HTMLElement = HTMLElement> =
*/
export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElement> implements UmbController {
//
// The sorter who last indicated that it was okay or not okay to drop here:
static lastIndicationSorter?: UmbSorterController<unknown>;
// A sorter that is requested to become the next sorter:
static originalSorter?: UmbSorterController<unknown>;
static originalIndex?: number;
@@ -144,8 +199,6 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
#dragX = 0;
#dragY = 0;
#lastIndicationContainerCtrl: UmbSorterController<T, ElementType> | null = null;
public get controllerAlias() {
// We only support one Sorter Controller pr. Controller Host.
return 'umbSorterController';
@@ -280,7 +333,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
setupIgnorerElements(element, this.#config.ignorerSelector);
}
if (!this.#config.disabledItemSelector || !element.matches('> ' + this.#config.disabledItemSelector)) {
if (!this.#config.disabledItemSelector || !element.matches(this.#config.disabledItemSelector)) {
// Idea: to make sure on does not get initialized twice: if ((element as HTMLElement).draggable === true) return;
(element as HTMLElement).draggable = true;
element.addEventListener('dragstart', this.#handleDragStart);
@@ -744,6 +797,11 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
const newModel = [...this.#model];
newModel.splice(newIndex, 0, item);
this.#model = newModel;
this.#config.onContainerChange?.({
model: newModel,
item,
from: fromCtrl as unknown as UmbSorterController<T, ElementType>,
});
this.#config.onChange?.({ model: newModel, item });
}
@@ -756,30 +814,26 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
}
updateAllowIndication(item: T) {
// TODO: Allow indication.
/*
// Remove old indication:
if (this.#lastIndicationContainerCtrl !== null && this.#lastIndicationContainerCtrl !== controller) {
this.#lastIndicationContainerCtrl.notifyAllowed();
if (UmbSorterController.lastIndicationSorter && UmbSorterController.lastIndicationSorter !== (this as unknown)) {
UmbSorterController.lastIndicationSorter.notifyAllowed();
}
this.#lastIndicationContainerCtrl = controller;
UmbSorterController.lastIndicationSorter = this as unknown as UmbSorterController<unknown>;
if (controller.notifyRequestDrop({ item: item }) === true) {
controller.notifyAllowed();
if (this.notifyRequestDrop({ item: item }) === true) {
this.notifyAllowed();
return true;
}
controller.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
this.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
return false;
*/
return true;
}
removeAllowIndication() {
// Remove old indication:
if (this.#lastIndicationContainerCtrl !== null) {
this.#lastIndicationContainerCtrl.notifyAllowed();
if (UmbSorterController.lastIndicationSorter) {
UmbSorterController.lastIndicationSorter.notifyAllowed();
}
this.#lastIndicationContainerCtrl = null;
UmbSorterController.lastIndicationSorter = undefined;
}
// TODO: Move auto scroll into its own class?
@@ -844,17 +898,23 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
public notifyDisallowed() {
if (this.#config.onDisallowed) {
this.#config.onDisallowed();
this.#config.onDisallowed({
item: UmbSorterController.activeItem,
element: UmbSorterController.activeElement! as ElementType,
});
}
}
public notifyAllowed() {
if (this.#config.onAllowed) {
this.#config.onAllowed();
this.#config.onAllowed({
item: UmbSorterController.activeItem,
element: UmbSorterController.activeElement! as ElementType,
});
}
}
public notifyRequestDrop(data: any) {
if (this.#config.onRequestDrop) {
return this.#config.onRequestDrop(data) || false;
if (this.#config.onRequestMove) {
return this.#config.onRequestMove(data) || false;
}
return true;
}
@@ -865,7 +925,7 @@ export class UmbSorterController<T, ElementType extends HTMLElement = HTMLElemen
this.#handleDragEnd();
}
this.#lastIndicationContainerCtrl = null;
UmbSorterController.lastIndicationSorter = undefined;
// TODO: Clean up items??
this.#observer.disconnect();