diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 3e0d257163..e5503b23bc 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -2484,8 +2484,8 @@ export default {
confirmDeleteBlockTypeMessage: 'Are you sure you want to delete the block configuration %0%?',
confirmDeleteBlockTypeNotice:
'The content of this block will still be present, editing of this content\n will no longer be available and will be shown as unsupported content.\n ',
- confirmDeleteBlockGroupMessage:
- 'Are you sure you want to delete group %0% and all the Block configurations of this?',
+ confirmDeleteBlockGroupTitle: 'Delete group?',
+ confirmDeleteBlockGroupMessage: 'Are you sure you want to delete group %0%?',
confirmDeleteBlockGroupNotice:
'The content of these Blocks will still be present, editing of this content\n will no longer be available and will be shown as unsupported content.\n ',
blockConfigurationOverlayTitle: "Configuration of '%0%'",
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 8fe49c37ef..1afbe47805 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
@@ -10,7 +10,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, customElement, state, repeat, css, property, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import '../block-grid-entry/index.js';
-import { UmbSorterController, type UmbSorterConfig, type resolvePlacementArgs } from '@umbraco-cms/backoffice/sorter';
+import {
+ UmbSorterController,
+ type UmbSorterConfig,
+ type UmbSorterResolvePlacementArgs,
+} from '@umbraco-cms/backoffice/sorter';
import {
UmbFormControlMixin,
UmbFormControlValidator,
@@ -23,7 +27,9 @@ import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models';
* @param args
* @returns { null | true }
*/
-function resolvePlacementAsGrid(args: resolvePlacementArgs) {
+function resolvePlacementAsBlockGrid(
+ args: UmbSorterResolvePlacementArgs,
+) {
// If this has areas, we do not want to move, unless we are at the edge
if (args.relatedModel.areas?.length > 0 && isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10)) {
return null;
@@ -80,9 +86,16 @@ function resolvePlacementAsGrid(args: resolvePlacementArgs gridColumnNumber;
+ const verticalDirection = relatedStartCol + foundElColumns + currentElementColumns > gridColumnNumber;
+ return verticalDirection;
+ /*
+ let placeAfter = args.horizontalPlaceAfter;
+
+ return {
+ verticalDirection,
+ placeAfter,
+ };*/
}
// --------------------------
@@ -96,7 +109,7 @@ const SORTER_CONFIG: UmbSorterConfig {
return modelEntry.contentKey;
},
- resolvePlacement: resolvePlacementAsGrid,
+ resolvePlacement: resolvePlacementAsBlockGrid,
identifier: 'block-grid-editor',
itemSelector: 'umb-block-grid-entry',
containerSelector: '.umb-block-grid__layout-container',
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 ae743a577d..9c3f2f9276 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
@@ -1,9 +1,7 @@
-import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '../../../block-type/index.js';
-import '../../../block-type/components/input-block-type/index.js';
-import {
- type UmbPropertyEditorUiElement,
- UmbPropertyValueChangeEvent,
- type UmbPropertyEditorConfigCollection,
+import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type';
+import type {
+ UmbPropertyEditorUiElement,
+ UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
import {
html,
@@ -30,6 +28,8 @@ import {
} from '@umbraco-cms/backoffice/property';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
+import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
interface MappedGroupWithBlockTypes extends UmbBlockGridTypeGroupType {
blocks: Array;
@@ -43,7 +43,6 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
- #moveData?: Array;
#sorter = new UmbSorterController(this, {
getUniqueOfElement: (element) => element.getAttribute('data-umb-group-key'),
getUniqueOfModel: (modelEntry) => modelEntry.key!,
@@ -104,8 +103,14 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
this.#datasetContext = context;
- //this.#observeBlocks();
- this.#observeBlockGroups();
+ this.observe(
+ await this.#datasetContext.propertyValueByAlias('blockGroups'),
+ (value) => {
+ this.#blockGroups = (value as Array) ?? [];
+ this.#mapValuesToBlockGroups();
+ },
+ '_observeBlockGroups',
+ );
});
this.#blockTypeWorkspaceModalRegistration = new UmbModalRouteRegistrationController(
@@ -119,24 +124,6 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
});
}
- async #observeBlockGroups() {
- if (!this.#datasetContext) return;
- this.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {
- this.#blockGroups = (value as Array) ?? [];
- this.#mapValuesToBlockGroups();
- });
- }
- // TODO: No need for this, we just got the value via the value property.. [NL]
- /*
- async #observeBlocks() {
- if (!this.#datasetContext) return;
- this.observe(await this.#datasetContext.propertyValueByAlias('blocks'), (value) => {
- this.value = (value as Array) ?? [];
- this.#mapValuesToBlockGroups();
- });
- }
- */
-
#mapValuesToBlockGroups() {
if (!this.#blockGroups) return;
// Map blocks that are not in any group, or in a group that does not exist
@@ -152,63 +139,60 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
this.#sorter.setModel(this._groupsWithBlockTypes);
}
- #onDelete(e: CustomEvent, groupKey?: string) {
- const updatedValues = (e.target as UmbInputBlockTypeElement).value.map((value) => ({ ...value, groupKey }));
- const filteredValues = this.#value.filter((value) => value.groupKey !== groupKey);
- this.value = [...filteredValues, ...updatedValues];
- this.dispatchEvent(new UmbPropertyValueChangeEvent());
- }
-
- async #onChange(e: CustomEvent) {
+ async #onChange(e: Event, groupKey?: string) {
e.stopPropagation();
const element = e.target as UmbInputBlockTypeElement;
- const value = element.value;
+ const value = element.value.map((x) => ({ ...x, groupKey }));
- if (!e.detail?.moveComplete) {
- // Container change, store data of the new group...
- const newGroupKey = element.getAttribute('data-umb-group-key');
- const movedItem = e.detail?.item as UmbBlockTypeWithGroupKey;
- // Check if item moved back to original group...
- if (movedItem.groupKey === newGroupKey) {
- this.#moveData = undefined;
- } else {
- this.#moveData = value.map((block) => ({ ...block, groupKey: newGroupKey }));
- }
- } else if (e.detail?.moveComplete) {
- // Move complete, get the blocks that were in an untouched group
- const blocks = this.#value
- .filter((block) => !value.find((value) => value.contentElementTypeKey === block.contentElementTypeKey))
- .filter(
- (block) => !this.#moveData?.find((value) => value.contentElementTypeKey === block.contentElementTypeKey),
- );
-
- this.value = this.#moveData ? [...blocks, ...value, ...this.#moveData] : [...blocks, ...value];
- this.dispatchEvent(new UmbPropertyValueChangeEvent());
- this.#moveData = undefined;
+ if (groupKey) {
+ // Update the specific group:
+ this._groupsWithBlockTypes = this._groupsWithBlockTypes.map((group) => {
+ if (group.key === groupKey) {
+ return { ...group, blocks: value };
+ }
+ return group;
+ });
+ } else {
+ // Update the not grouped blocks:
+ this._notGroupedBlockTypes = value;
}
+
+ this.#updateValue();
+ }
+
+ #updateValue() {
+ this.value = [...this._notGroupedBlockTypes, ...this._groupsWithBlockTypes.flatMap((group) => group.blocks)];
+ this.dispatchEvent(new UmbChangeEvent());
+ }
+
+ #updateBlockGroupsValue(groups: Array) {
+ this.#datasetContext?.setPropertyValue('blockGroups', groups);
}
#onCreate(e: CustomEvent, groupKey?: string) {
const selectedElementType = e.detail.contentElementTypeKey;
if (selectedElementType) {
- this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? null));
+ this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + (groupKey ?? 'null'));
}
}
// TODO: Implement confirm dialog [NL]
- #deleteGroup(groupKey: string) {
- // TODO: make one method for updating the blockGroupsDataSetValue: [NL]
- // This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before.
- this.#datasetContext?.setPropertyValue(
- 'blockGroups',
- this.#blockGroups?.filter((group) => group.key !== groupKey),
- );
-
+ async #deleteGroup(groupKey: string) {
+ const groupName = this.#blockGroups?.find((group) => group.key === groupKey)?.name ?? '';
+ await umbConfirmModal(this, {
+ headline: '#blockEditor_confirmDeleteBlockGroupTitle',
+ content: this.localize.term('#blockEditor_confirmDeleteBlockGroupMessage', [groupName]),
+ color: 'danger',
+ confirmLabel: '#general_delete',
+ });
// If a group is deleted, Move the blocks to no group:
this.value = this.#value.map((block) => (block.groupKey === groupKey ? { ...block, groupKey: undefined } : block));
+ if (this.#blockGroups) {
+ this.#updateBlockGroupsValue(this.#blockGroups.filter((group) => group.key !== groupKey));
+ }
}
- #changeGroupName(e: UUIInputEvent, groupKey: string) {
+ #onGroupNameChange(e: UUIInputEvent, groupKey: string) {
const groupName = e.target.value as string;
// TODO: make one method for updating the blockGroupsDataSetValue: [NL]
this.#datasetContext?.setPropertyValue(
@@ -224,9 +208,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
.propertyAlias=${this._alias}
.value=${this._notGroupedBlockTypes}
.workspacePath=${this._workspacePath}
- @change=${this.#onChange}
- @create=${(e: CustomEvent) => this.#onCreate(e, undefined)}
- @delete=${(e: CustomEvent) => this.#onDelete(e, undefined)}>`
+ @change=${(e: Event) => this.#onChange(e, undefined)}
+ @create=${(e: CustomEvent) => this.#onCreate(e, undefined)}>`
: ''}
${repeat(
this._groupsWithBlockTypes,
@@ -239,9 +222,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
.propertyAlias=${this._alias + '_' + group.key}
.value=${group.blocks}
.workspacePath=${this._workspacePath}
- @change=${this.#onChange}
- @create=${(e: CustomEvent) => this.#onCreate(e, group.key)}
- @delete=${(e: CustomEvent) => this.#onDelete(e, group.key)}>
+ @change=${(e: Event) => this.#onChange(e, group.key)}
+ @create=${(e: CustomEvent) => this.#onCreate(e, group.key)}>
`,
)}
`;
@@ -253,7 +235,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
auto-width
label="Group"
.value=${groupName ?? ''}
- @change=${(e: UUIInputEvent) => this.#changeGroupName(e, groupKey)}>
+ @change=${(e: UUIInputEvent) => this.#onGroupNameChange(e, groupKey)}>
this.#deleteGroup(groupKey)}>
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 3854cea921..5664722449 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
@@ -6,12 +6,14 @@ import { css, html, customElement, property, state, repeat } from '@umbraco-cms/
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
-import { UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import {
UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT,
UMB_DOCUMENT_TYPE_PICKER_MODAL,
+ type UmbDocumentTypePickerModalData,
+ type UmbDocumentTypePickerModalValue,
} from '@umbraco-cms/backoffice/document-type';
-import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
+import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
import '../block-type-card/index.js';
@@ -27,32 +29,40 @@ export class UmbInputBlockTypeElement<
itemSelector: 'umb-block-type-card',
identifier: 'umb-block-type-sorter',
containerSelector: '#blocks',
+ resolvePlacement: UmbSorterResolvePlacementAsGrid,
+ onContainerChange: ({ item, model }) => {
+ this.dispatchEvent(new CustomEvent('container-change', { detail: { item, model } }));
+ },
onChange: ({ model }) => {
- this._items = model;
+ this._value = model;
+ this.dispatchEvent(new UmbChangeEvent());
},
- onContainerChange: ({ model, item }) => {
- this._items = model;
- this.dispatchEvent(new CustomEvent('change', { detail: { item } }));
- },
- onEnd: () => {
+ /*onEnd: () => {
// TODO: Investigate if onEnd is called when a container move has been performed, if not then I would say it should be. [NL]
- this.dispatchEvent(new CustomEvent('change', { detail: { moveComplete: true } }));
- },
+ this.dispatchEvent(new UmbChangeEvent());
+ },*/
});
+ #elementPickerModal: UmbModalRouteRegistrationController<
+ UmbDocumentTypePickerModalData,
+ UmbDocumentTypePickerModalValue
+ >;
@property({ type: Array, attribute: false })
public set value(items) {
- this._items = items ?? [];
- this.#sorter.setModel(this._items);
+ this._value = items ?? [];
+ // Make sure the block types are unique on contentTypeElementKey:
+ this._value = this._value.filter(
+ (value, index, self) => self.findIndex((x) => x.contentElementTypeKey === value.contentElementTypeKey) === index,
+ );
+ this.#sorter.setModel(this._value);
}
public get value() {
- return this._items;
+ return this._value;
}
- /** @deprecated will be removed in v17 */
@property({ type: String })
public set propertyAlias(value: string | undefined) {
- //this.#elementPickerModal.setUniquePathValue('propertyAlias', value);
+ this.#elementPickerModal.setUniquePathValue('propertyAlias', value);
}
public get propertyAlias(): string | undefined {
return undefined;
@@ -65,7 +75,7 @@ export class UmbInputBlockTypeElement<
private _pickerPath?: string;
@state()
- private _items: Array = [];
+ private _value: Array = [];
// TODO: Seems no need to have these initially, then can be retrieved inside the `create` method. [NL]
#datasetContext?: UmbPropertyDatasetContext;
@@ -84,7 +94,8 @@ export class UmbInputBlockTypeElement<
);
});
- new UmbModalRouteRegistrationController(this, UMB_DOCUMENT_TYPE_PICKER_MODAL)
+ this.#elementPickerModal = new UmbModalRouteRegistrationController(this, UMB_DOCUMENT_TYPE_PICKER_MODAL)
+ .addUniquePaths(['propertyAlias'])
.onSetup(() => {
return {
data: {
@@ -123,8 +134,8 @@ export class UmbInputBlockTypeElement<
}
deleteItem(contentElementTypeKey: string) {
- this.value = this.value.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
- this.dispatchEvent(new UmbDeleteEvent());
+ this._value = this.value.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
+ this.dispatchEvent(new UmbChangeEvent());
}
async #onRequestDelete(item: BlockType) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts
index dceee84e17..1825a1ce2d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts
@@ -1,8 +1,8 @@
-import { UmbRefItemElement } from '@umbraco-cms/backoffice/components';
import type {
UmbEntityCreateOptionActionListModalData,
UmbEntityCreateOptionActionListModalValue,
} from './entity-create-option-action-list-modal.token.js';
+import { UmbRefItemElement } from '@umbraco-cms/backoffice/components';
import type { ManifestEntityCreateOptionAction } from '@umbraco-cms/backoffice/entity-create-option-action';
import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts
index 24c7a48086..d14228cb56 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts
@@ -1,4 +1,4 @@
-import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
+import { html, customElement, property, css, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbConfirmModalData, UmbConfirmModalValue, UmbModalContext } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element';
@@ -21,20 +21,20 @@ export class UmbConfirmModalElement extends UmbLitElement {
override render() {
return html`
-
- ${this.data?.content}
+
+ ${unsafeHTML(this.localize.string(this.data?.content))}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/index.ts
index 70db439d67..8f373e2f08 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/index.ts
@@ -1 +1,2 @@
export * from './sorter.controller.js';
+export * from './replacement-resolver-as-grid.function.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/replacement-resolver-as-grid.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/replacement-resolver-as-grid.function.ts
new file mode 100644
index 0000000000..968f5c485a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/replacement-resolver-as-grid.function.ts
@@ -0,0 +1,20 @@
+import type { UmbSorterResolvePlacementArgs } from './sorter.controller.js';
+
+/**
+ * This function is used to resolve the placement of an item in a simple grid layout.
+ * @param args
+ * @returns { null | true }
+ */
+export function UmbSorterResolvePlacementAsGrid(args: UmbSorterResolvePlacementArgs) {
+ // If we are part of the same Sorter model
+ if (args.itemIndex !== null && args.relatedIndex !== null) {
+ // and the pointer is within the related rect
+ if (args.relatedRect.left < args.pointerX && args.relatedRect.right > args.pointerX) {
+ // Then we control the placeAfter property, making the active-drag-element allow to be placed at a spot already when just hovering that spot. (This only works when items have the same size)
+ return {
+ placeAfter: args.itemIndex < args.relatedIndex,
+ };
+ }
+ }
+ return false;
+}
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 fa218872da..b90c7c1614 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
@@ -80,21 +80,39 @@ function destroyPreventEvent(element: Element) {
//element.removeAttribute('draggable');
}
-export type resolvePlacementArgs = {
+export type UmbSorterResolvePlacementReturn =
+ | boolean
+ | null
+ | {
+ placeAfter: boolean;
+ verticalDirection?: boolean;
+ };
+
+export type UmbSorterResolvePlacementArgs = {
containerElement: Element;
containerRect: DOMRect;
item: T;
+ itemIndex: number | null;
element: ElementType;
elementRect: DOMRect;
relatedElement: ElementType;
relatedModel: T;
relatedRect: DOMRect;
+ relatedIndex: number | null;
placeholderIsInThisRow: boolean;
horizontalPlaceAfter: boolean;
pointerX: number;
pointerY: number;
};
+/**
+ * @deprecated will be removed in v.17, use `UmbSorterResolvePlacementArgs`
+ */
+export type resolvePlacementArgs = UmbSorterResolvePlacementArgs<
+ T,
+ ElementType
+>;
+
type UniqueType = string | symbol | number;
/**
@@ -180,7 +198,7 @@ type INTERNAL_UmbSorterConfig = {
* }
* }
*/
- resolvePlacement?: (argument: resolvePlacementArgs) => boolean | null;
+ resolvePlacement?: (argument: UmbSorterResolvePlacementArgs) => UmbSorterResolvePlacementReturn;
/**
* This callback is executed when an item is moved within this container.
*/
@@ -248,7 +266,7 @@ export class UmbSorterController = [];
- #rqaId?: number;
+ static rqaId?: number;
#containerElement!: HTMLElement;
#useContainerShadowRoot?: boolean;
@@ -260,7 +278,7 @@ export class UmbSorterController();
+ #elements = Array();
public get identifier() {
return this.#config.identifier;
@@ -330,7 +348,6 @@ export class UmbSorterController): void {
if (this.#model) {
- // TODO: Some updates might need to be done, as the model is about to change? Do make the changes after setting the model?.. [NL]
this.#model = model;
}
}
@@ -409,7 +426,7 @@ export class UmbSorterController this.destroyItem(item));
+ this.#elements.forEach((item) => this.destroyItem(item));
}
#itemDraggedOver = (e: DragEvent) => {
@@ -433,7 +450,11 @@ export class UmbSorterController;
@@ -476,8 +497,8 @@ export class UmbSorterController x !== element);
+ this.#elements = this.#elements.filter((x) => x !== element);
}
#setupPlaceholderStyle() {
@@ -586,9 +607,9 @@ export class UmbSorterController;
// We must wait one frame before changing the look of the block.
- this.#rqaId = requestAnimationFrame(() => {
+ UmbSorterController.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;
+ UmbSorterController.rqaId = undefined;
if (UmbSorterController.activeElement) {
UmbSorterController.activeElement.style.transform = '';
}
@@ -650,9 +671,9 @@ export class UmbSorterController {
- this.#rqaId = undefined;
+ UmbSorterController.rqaId = undefined;
if (!UmbSorterController.activeElement || !UmbSorterController.activeItem) {
return;
}
+ if ((UmbSorterController.dropSorter as any) !== this) {
+ throw new Error('Drop sorter is not this sorter');
+ }
+
// 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);
@@ -744,6 +769,10 @@ export class UmbSorterController {
const centerX = sameRow.dragRect.left + sameRow.dragRect.width * 0.5;
const distance = Math.abs(this.#dragX - centerX);
+ /*const distance = Math.min(
+ Math.abs(this.#dragX - sameRow.dragRect.left),
+ Math.abs(this.#dragX - sameRow.dragRect.right),
+ );*/
if (distance < lastDistance) {
foundEl = sameRow.el as HTMLElement;
foundElDragRect = sameRow.dragRect;
@@ -752,6 +781,11 @@ export class UmbSorterController relatedIndex && foundElDragRect.right - widthDiff > this.#dragX) {
+ // If we are located after and we are just enough over to get located before, then lets do it already.
+ placeAfter = false;
+ }
+ }
+
+ const placementResult: UmbSorterResolvePlacementReturn = this.#config.resolvePlacement
? this.#config.resolvePlacement({
containerElement: this.#containerElement,
containerRect: currentContainerRect,
item: UmbSorterController.activeItem,
+ itemIndex: activeIndex,
element: UmbSorterController.activeElement as ElementType,
elementRect: currentElementRect,
relatedElement: foundEl,
relatedModel: foundModel,
relatedRect: foundElDragRect,
+ relatedIndex: relatedIndex,
placeholderIsInThisRow: placeholderIsInThisRow,
horizontalPlaceAfter: placeAfter,
pointerX: this.#dragX,
@@ -785,20 +833,41 @@ export class UmbSorterController foundElDragRect.top + foundElDragRect.height * 0.5;
+ let verticalDirection = true;
+ if (typeof placementResult === 'object') {
+ verticalDirection = placementResult.verticalDirection ?? false;
+ placeAfter = placementResult.placeAfter;
+ } else {
+ verticalDirection = placementResult ?? false;
+ if (verticalDirection === true) {
+ // Lets check if we should place after or before, based on a vertical algortihm.
+ placeAfter = this.#dragY > foundElDragRect.top + foundElDragRect.height * 0.5;
+
+ // There is room for improvements, if we are in the same model:
+ if (activeIndex !== null && relatedIndex !== null) {
+ // We have both indexes, aka. both elements are in this list.
+ const heightDiff = Math.max(foundElDragRect.height - currentElementRect.height, 0);
+ if (activeIndex < relatedIndex && this.#dragY > foundElDragRect.top + heightDiff) {
+ // If active is located above and we are just enough above to get located after, then lets do it already.
+ placeAfter = true;
+ } else if (activeIndex > relatedIndex && this.#dragY < foundElDragRect.bottom - heightDiff) {
+ // If active is located below and we are just enough above to get located before, then lets do it already.
+ placeAfter = false;
+ }
+ }
+ }
}
- if (verticalDirection) {
+ if (verticalDirection === true) {
let el;
if (placeAfter === false) {
let lastLeft = foundElDragRect.left;
- elementsInSameRow.findIndex((x) => {
+ elementsInSameRow.map((x) => {
if (x.dragRect.left < lastLeft) {
lastLeft = x.dragRect.left;
el = x.el;
@@ -806,7 +875,7 @@ export class UmbSorterController {
+ elementsInSameRow.map((x) => {
if (x.dragRect.right > lastRight) {
lastRight = x.dragRect.right;
el = x.el;
@@ -824,7 +893,7 @@ export class UmbSorterController currentContainerRect.bottom) {
this.#moveElementTo(-1);
+ } else {
+ // There was no target, but we are still inside. aka. in a vertical gap/gutter/blankspace.
+ if (this.#model.length > 1 && activeIndex !== null) {
+ const belowActive = this.#dragY > currentElementRect.bottom;
+
+ const foundTarget =
+ belowActive === false
+ ? this.#findIndexToMoveTo(0, activeIndex)
+ : this.#findIndexToMoveTo(activeIndex, this.#model.length);
+
+ if (foundTarget) {
+ this.#moveElementTo(foundTarget);
+ }
+ }
}
};
+ #findIndexToMoveTo(a: number, b: number): number | undefined {
+ if (a === b) {
+ return a;
+ }
+ const halfWay = a + Math.round((b - a) * 0.5);
+
+ // if we hit one of the points, then lets just move to the other point.
+ if (halfWay === a || halfWay === b) {
+ return b;
+ }
+
+ const belowHalf = this.#isPointerBelowTargetElement(halfWay);
+ if (belowHalf === null) {
+ throw new Error('Could not determine if below target');
+ }
+
+ if (belowHalf) {
+ return this.#findIndexToMoveTo(halfWay, b);
+ } else {
+ return this.#findIndexToMoveTo(a, halfWay);
+ }
+ }
+
+ #isPointerBelowTargetElement(targetIndex: number) {
+ if (targetIndex > 0 && targetIndex < this.#model.length) {
+ const element = this.getElementOfItem(this.#model[targetIndex]);
+ if (element) {
+ // Below this one == true, otherwise false.
+ return this.#dragY > element?.getBoundingClientRect().bottom;
+ }
+ }
+ return null;
+ }
+
//
async #moveElementTo(newIndex: number) {
if (!UmbSorterController.activeElement || !UmbSorterController.activeSorter) {
@@ -851,6 +968,9 @@ export class UmbSorterController elementUnique === this.#config.getUniqueOfModel(entry));
}
+ public getElementOfItem(item: T) {
+ const unique = this.#config.getUniqueOfModel(item);
+ if (unique === undefined) {
+ console.error('Sorter could not find unique of item', item);
+ //throw new Error('Sorter could not find unique of item');
+ return;
+ }
+ return this.#elements.find((element) => unique === this.#config.getUniqueOfElement(element));
+ }
+
public async removeItem(item: T) {
if (!item) {
return false;
@@ -891,32 +1021,11 @@ export class UmbSorterController x !== item).length > 0;
}
- // TODO: Could get item via attr.
public async moveItemInModel(newIndex: number, fromCtrl: UmbSorterController) {
if (!UmbSorterController.activeItem) {
console.error('There is no active item to move');
@@ -940,34 +1049,8 @@ export class UmbSorterController;
UmbSorterController.activeIndex = newIndex;
}
+ return true;
+ }
+
+ if (localMove) {
+ // Local move:
+
+ const oldIndex = this.#model.indexOf(item);
+ if (oldIndex === -1) {
+ console.error('Could not find item in model when performing internal move', this.getHostElement(), this.#model);
+ return false;
+ }
+
+ if (this.#config.performItemMove) {
+ const result = await this.#config.performItemMove({ item, newIndex, oldIndex });
+ if (result === false) {
+ return false;
+ }
+ } else {
+ const newModel = [...this.#model];
+ newModel.splice(oldIndex, 1);
+ if (oldIndex <= newIndex) {
+ newIndex--;
+ }
+ newModel.splice(newIndex, 0, item);
+ this.#model = newModel;
+ this.#config.onChange?.({ model: newModel, item });
+ }
+
+ UmbSorterController.activeIndex = newIndex;
}
return true;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts
index 7ccb3235bf..dc3b81e63c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts
@@ -14,7 +14,7 @@ import { splitStringToArray } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
-import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
+import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
@@ -33,8 +33,7 @@ export class UmbInputMediaElement extends UmbFormControlMixin false,
+ resolvePlacement: UmbSorterResolvePlacementAsGrid,
onChange: ({ model }) => {
this.selection = model;
this.#sortCards(model);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts
index efa2e06e9d..cd9abf6633 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts
@@ -8,7 +8,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
-import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
+import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
@@ -25,9 +25,7 @@ type UmbRichMediaCardModel = {
isTrashed?: boolean;
};
-const elementName = 'umb-input-rich-media';
-
-@customElement(elementName)
+@customElement('umb-input-rich-media')
export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, '') {
#sorter = new UmbSorterController(this, {
getUniqueOfElement: (element) => {
@@ -39,9 +37,8 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement,
identifier: 'Umb.SorterIdentifier.InputRichMedia',
itemSelector: 'uui-card-media',
containerSelector: '.container',
- // TODO: This component probably needs some grid-like logic for resolve placement... [LI]
- // TODO: You can also use verticalDirection? [NL]
- resolvePlacement: () => false,
+ //resolvePlacement: (args) => args.pointerX < args.relatedRect.left + args.relatedRect.width * 0.5,
+ resolvePlacement: UmbSorterResolvePlacementAsGrid,
onChange: ({ model }) => {
this.#items = model;
this.#sortCards(model);
@@ -454,6 +451,6 @@ export default UmbInputRichMediaElement;
declare global {
interface HTMLElementTagNameMap {
- [elementName]: UmbInputRichMediaElement;
+ 'umb-input-rich-media': UmbInputRichMediaElement;
}
}