diff --git a/src/Umbraco.Web.UI.Client/.vscode/settings.json b/src/Umbraco.Web.UI.Client/.vscode/settings.json index 661106f3d9..8259117d7e 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/settings.json +++ b/src/Umbraco.Web.UI.Client/.vscode/settings.json @@ -7,6 +7,7 @@ "ctrls", "devs", "Elementable", + "iframes", "invariantable", "lucide", "Niels", diff --git a/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts b/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts index edd76a7eb8..999335647a 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts @@ -113,18 +113,20 @@ export const UmbControllerHostMixin = (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; } diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 2421543b17..428b407cae 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -674,6 +674,8 @@ export const data: Array = [ { 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 = [ iconColor: '#FFFDD0', backgroundColor: '#633f32', editorSize: 'medium', - icon: 'icon-coffee', + allowInAreas: true, }, { @@ -728,37 +730,36 @@ export const data: Array = [ 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, }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index 3ca2bff84e..427469c541 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts index 69f6b5443d..cc9f12f89b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts @@ -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 + ); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts index 1668e9e7bf..42f08053e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts @@ -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); }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts index 04a95ec941..0bc58d1c88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts @@ -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 { + blocks: Array; +} + /** * @element umb-property-editor-ui-block-grid-type-configuration */ @@ -47,7 +47,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement private _blockGroups: Array = []; @state() - private _mappedValuesAndGroups: Array = []; + private _mappedValuesAndGroups: Array = []; @state() private _workspacePath?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts index 52cb98af76..1cb2440873 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts @@ -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; allowAtRoot: boolean; allowInAreas: boolean; @@ -41,10 +41,6 @@ export interface UmbBlockGridTypeGroupType { key: string; } -export interface UmbBlockGridGroupTypeConfiguration extends Partial { - blocks: Array; -} - // Content models: export interface UmbBlockGridValueModel extends UmbBlockValueType {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts index b414bd249c..6cc117c7bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts @@ -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 }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index da6ee918ff..42bc1ae721 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -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('useInlineEditingAsDefault'); this.#managerContext.setInlineEditingMode(useInlineEditingAsDefault); // TODO: - //config.useSingleBlockMode, not done jey + //config.useSingleBlockMode, not done jet this.style.maxWidth = config.getValueByAlias('maxPropertyWidth') ?? ''; this.#managerContext.setEditorConfiguration(config); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts index 38a6e14d17..fa4093cf82 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts @@ -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` - - - - - - - + + + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts index 9cf0faa8d4..8caac68e41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts @@ -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`
${repeat(this.value, (block) => block.contentElementTypeKey, this.#renderItem)} ${this.#renderButton()}
`; } - #renderItem = (item: BlockType) => { + #renderItem = (block: BlockType) => { return html` this.deleteItem(item.contentElementTypeKey)}> + .name=${block.label} + .iconColor=${block.iconColor} + .backgroundColor=${block.backgroundColor} + .href="${this.workspacePath}/edit/${block.contentElementTypeKey}" + .contentElementTypeKey=${block.contentElementTypeKey}> + + + + + `; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts index 6ed47bbbff..9357b666f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 95686bee6a..294595b78f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -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); }*/ diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts index 1beec67023..b31b993fda 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts @@ -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`

${group.name}

` : nothing} -
- ${repeat( - group.blocks, - (block) => block.contentElementTypeKey, - (block) => html` - - - - `, - )} -
- `, - )} + ${this._groupedBlocks + ? this._groupedBlocks.map( + (group) => html` + ${group.name && group.name !== '' ? html`

${group.name}

` : nothing} +
+ ${repeat( + group.blocks, + (block) => block.contentElementTypeKey, + (block) => html` + + + `, + )} +
+ `, + ) + : ''} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index 6b6f663ff6..d8395f3cd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -324,6 +324,7 @@ export class UmbBlockWorkspaceContext< }; public destroy(): void { + super.destroy(); this.#layout.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index 99271bcdcc..aa2979d7f7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -209,7 +209,6 @@ export class UmbInputUploadFieldElement extends FormControlMixin(UmbLitElement) } #renderFiles() { - console.log('files', this._files); return repeat( this._files, (path) => path, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts index 8401ccfa5d..95815ca394 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts @@ -74,27 +74,79 @@ export type resolveVerticalDirectionArgs = { type INTERNAL_UmbSorterConfig = { 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 }) => 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; from: UmbSorterController }) => 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) => 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) => boolean; + /** + * This callback is executed when an item is moved within this container. + */ performItemMove?: (argument: { item: T; newIndex: number; oldIndex: number }) => Promise | boolean; + /** + * This callback is executed when an item should be inserted into this container. + */ performItemInsert?: (argument: { item: T; newIndex: number }) => Promise | boolean; + /** + * This callback is executed when an item should be removed from this container. + */ performItemRemove?: (argument: { item: T }) => Promise | boolean; }; @@ -113,6 +165,9 @@ export type UmbSorterConfig = */ export class UmbSorterController implements UmbController { // + // The sorter who last indicated that it was okay or not okay to drop here: + static lastIndicationSorter?: UmbSorterController; + // A sorter that is requested to become the next sorter: static originalSorter?: UmbSorterController; static originalIndex?: number; @@ -144,8 +199,6 @@ export class UmbSorterController | null = null; - public get controllerAlias() { // We only support one Sorter Controller pr. Controller Host. return 'umbSorterController'; @@ -280,7 +333,7 @@ export class UmbSorterController ' + 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, + }); this.#config.onChange?.({ model: newModel, item }); } @@ -756,30 +814,26 @@ export class UmbSorterController; - 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