Merge pull request #1865 from umbraco/feature/block-grid-create-inline-buttons

Feat: Block Grid: create before and after buttons
This commit is contained in:
Niels Lyngsø
2024-05-23 01:05:40 +02:00
committed by GitHub
9 changed files with 153 additions and 61 deletions

View File

@@ -8,6 +8,7 @@ export * from './json-string-comparison.function.js';
export * from './merge-observables.function.js';
export * from './observe-multiple.function.js';
export * from './partial-update-frozen-array.function.js';
export * from './push-at-to-unique-array.function.js';
export * from './push-to-unique-array.function.js';
export * from './simple-hash-code.function.js';
export * from './strict-equality-memoization.function.js';

View File

@@ -208,7 +208,7 @@ export class UmbBlockGridEntriesElement extends UmbLitElement {
(layoutEntry, index) =>
html`<umb-block-grid-entry
class="umb-block-grid__layout-item"
.index=${index}
index=${index}
.contentUdi=${layoutEntry.contentUdi}
.layout=${layoutEntry}>
</umb-block-grid-entry>`,

View File

@@ -1,6 +1,7 @@
import { UmbBlockGridEntryContext } from '../../context/block-grid-entry.context.js';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, css, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbBlockViewPropsType } from '@umbraco-cms/backoffice/block';
import type { UmbBlockGridLayoutModel } from '@umbraco-cms/backoffice/block-grid';
@@ -14,7 +15,7 @@ import '../block-scale-handler/index.js';
@customElement('umb-block-grid-entry')
export class UmbBlockGridEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement {
//
@property({ type: Number })
@property({ type: Number, reflect: true })
public get index(): number | undefined {
return this.#context.getIndex();
}
@@ -37,6 +38,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
//
#context = new UmbBlockGridEntryContext(this);
#renderTimeout: number | undefined;
@state()
_columnSpan?: number;
@@ -51,7 +53,9 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
// If _createPath is undefined, its because no blocks are allowed to be created here[NL]
@state()
_createPath?: string;
_createBeforePath?: string;
@state()
_createAfterPath?: string;
@state()
_label = '';
@@ -68,6 +72,13 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
@state()
_canScale?: boolean;
@state()
_showInlineCreateBefore?: boolean;
@state()
_showInlineCreateAfter?: boolean;
@state()
_inlineCreateAboveWidth?: string;
// TODO: use this type on the Element Interface for the Manifest.
@state()
_blockViewProps: UmbBlockViewPropsType<UmbBlockGridLayoutModel> = { contentUdi: undefined!, urls: {} }; // Set to undefined cause it will be set before we render.
@@ -110,10 +121,15 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
});
// Paths:
this.observe(this.#context.createPath, (createPath) => {
const oldValue = this._createPath;
this._createPath = createPath;
this.requestUpdate('_createPath', oldValue);
this.observe(this.#context.createBeforePath, (createPath) => {
//const oldValue = this._createBeforePath;
this._createBeforePath = createPath;
//this.requestUpdate('_createPath', oldValue);
});
this.observe(this.#context.createAfterPath, (createPath) => {
//const oldValue = this._createAfterPath;
this._createAfterPath = createPath;
//this.requestUpdate('_createPath', oldValue);
});
this.observe(this.#context.workspaceEditContentPath, (path) => {
this._workspaceEditContentPath = path;
@@ -156,8 +172,52 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
this.setAttribute('data-content-element-type-alias', contentElementTypeAlias);
}
});
this.#callUpdateInlineCreateButtons();
}
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);
if (_changedProperties.has('_blockViewProps') || _changedProperties.has('_columnSpan')) {
this.#callUpdateInlineCreateButtons();
}
}
#callUpdateInlineCreateButtons() {
clearTimeout(this.#renderTimeout);
this.#renderTimeout = setTimeout(this.#updateInlineCreateButtons, 100) as unknown as number;
}
#updateInlineCreateButtons = () => {
// TODO: Could we optimize this, so it wont break?, cause currently we trust blindly that parentElement is '.umb-block-grid__layout-container' [NL]
const layoutContainer = this.parentElement;
if (!layoutContainer) return;
const layoutContainerRect = layoutContainer.getBoundingClientRect();
if (layoutContainerRect.width === 0) {
this._showInlineCreateBefore = false;
this._showInlineCreateAfter = false;
this._inlineCreateAboveWidth = undefined;
this.#renderTimeout = setTimeout(this.#updateInlineCreateButtons, 100) as unknown as number;
return;
}
const layoutItemRect = this.getBoundingClientRect();
if (layoutItemRect.right > layoutContainerRect.right - 5) {
this._showInlineCreateAfter = false;
} else {
this._showInlineCreateAfter = true;
}
if (layoutItemRect.left > layoutContainerRect.left + 5) {
this._showInlineCreateBefore = false;
this._inlineCreateAboveWidth = undefined;
} else {
this._inlineCreateAboveWidth = getComputedStyle(layoutContainer).width;
this._showInlineCreateBefore = true;
}
};
#renderInlineEditBlock() {
return html`<umb-block-grid-block-inline
.contentUdi=${this.contentUdi}
@@ -171,8 +231,13 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
#renderBlock() {
return this.contentUdi
? html`
${this._createPath
? html`<uui-button-inline-create href=${this._createPath}></uui-button-inline-create>`
${this._createBeforePath && this._showInlineCreateBefore
? html`<uui-button-inline-create
href=${this._createBeforePath}
label=${this.localize.term('blockEditor_addBlock')}
style=${this._inlineCreateAboveWidth
? `width: ${this._inlineCreateAboveWidth}`
: ''}></uui-button-inline-create>`
: nothing}
<div class="umb-block-grid__block" part="umb-block-grid__block">
<umb-extension-slot
@@ -204,6 +269,12 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
</umb-block-scale-handler>`
: nothing}
</div>
${this._createAfterPath && this._showInlineCreateAfter
? html`<uui-button-inline-create
vertical
label=${this.localize.term('blockEditor_addBlock')}
href=${this._createAfterPath}></uui-button-inline-create>`
: nothing}
`
: nothing;
}
@@ -223,6 +294,24 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
top: var(--uui-size-2);
right: var(--uui-size-2);
}
uui-button-inline-create {
top: 0px;
position: absolute;
// Avoid showing inline-create in dragging-mode
--umb-block-grid__block--inline-create-button-display--condition: var(--umb-block-grid--dragging-mode) none;
display: var(--umb-block-grid__block--inline-create-button-display--condition);
}
uui-button-inline-create:not([vertical]) {
left: 0;
width: var(--umb-block-grid-editor--inline-create-width, 100%);
}
:host(:not([index='0'])) uui-button-inline-create:not([vertical]) {
top: calc(var(--umb-block-grid--row-gap, 0px) * -0.5);
}
uui-button-inline-create[vertical] {
right: calc(1px - (var(--umb-block-grid--column-gap, 0px) * 0.5));
}
:host([drag-placeholder]) {
opacity: 0.2;

View File

@@ -102,11 +102,12 @@ export class UmbBlockGridEntryContext
columnSpan = this.#calcColumnSpan(columnSpan, this.getRelevantColumnSpanOptions(), layoutColumns);
if (columnSpan === this.getColumnSpan()) return;
//this._layout.update({ columnSpan });
//const contentUdi = this.getContentUdi();
//if (!contentUdi) return;
//this._manager?.updateLayout({ contentUdi, columnSpan });
this._layout.update({ columnSpan });
const layoutValue = this._layout.getValue();
if (!layoutValue) return;
this._layout.setValue({
...layoutValue,
columnSpan,
});
}
/**
* Get the column span of this entry.
@@ -125,11 +126,12 @@ export class UmbBlockGridEntryContext
if (!minMax) return;
rowSpan = Math.max(minMax[0], Math.min(rowSpan, minMax[1]));
if (rowSpan === this.getRowSpan()) return;
//this._layout.update({ rowSpan });
//const contentUdi = this.getContentUdi();
//if (!contentUdi) return;
//this._manager?.updateLayout({ contentUdi, rowSpan });
this._layout.update({ rowSpan });
const layoutValue = this._layout.getValue();
if (!layoutValue) return;
this._layout.setValue({
...layoutValue,
rowSpan,
});
}
/**
* Get the row span of this entry.
@@ -145,7 +147,6 @@ export class UmbBlockGridEntryContext
_gotEntries() {
this.scaleManager.setEntriesContext(this._entries);
if (!this._entries) return;
this.#gotEntriesAndManager();
@@ -185,7 +186,6 @@ export class UmbBlockGridEntryContext
if (!this._entries || !this._manager) return;
// Secure areas fits options:
// NOLZ
this.observe(
observeMultiple([this.areas, this.layoutAreas]),
([areas, layoutAreas]) => {
@@ -193,15 +193,10 @@ export class UmbBlockGridEntryContext
const areasAreIdentical =
areas.length === layoutAreas.length && areas.every((area) => layoutAreas.some((y) => y.key === area.key));
if (areasAreIdentical === false) {
/*
const contentUdi = this.getContentUdi();
if (!contentUdi) return;
this._manager?.updateLayout({
contentUdi,
areas: layoutAreas.map((x) => (areas.find((y) => y.key === x.key) ? x : { key: x.key, items: [] })),
});
*/
this._layout.update({
const layoutValue = this._layout.getValue();
if (!layoutValue) return;
this._layout.setValue({
...layoutValue,
areas: layoutAreas.map((x) => (areas.find((y) => y.key === x.key) ? x : { key: x.key, items: [] })),
});
}
@@ -210,7 +205,6 @@ export class UmbBlockGridEntryContext
);
// Secure columnSpan fits options:
// NOLZ
this.observe(
observeMultiple([this.layout, this.columnSpan, this.relevantColumnSpanOptions, this._entries.layoutColumns]),
([layout, columnSpan, relevantColumnSpanOptions, layoutColumns]) => {
@@ -221,27 +215,30 @@ export class UmbBlockGridEntryContext
layoutColumns,
);
if (newColumnSpan !== columnSpan) {
//const contentUdi = this.getContentUdi();
//if (!contentUdi) return;
//this._manager?.updateLayout({ contentUdi, columnSpan: newColumnSpan });
this._layout.update({ columnSpan: newColumnSpan });
const layoutValue = this._layout.getValue();
if (!layoutValue) return;
this._layout.setValue({
...layoutValue,
columnSpan: newColumnSpan,
});
}
},
'observeColumnSpanValidation',
);
// Secure rowSpan fits options:
// NOLZ
this.observe(
observeMultiple([this.minMaxRowSpan, this.rowSpan]),
([minMax, rowSpan]) => {
if (minMax) {
const newRowSpan = Math.max(minMax[0], Math.min(rowSpan ?? 1, minMax[1]));
if (newRowSpan !== rowSpan) {
//const contentUdi = this.getContentUdi();
//if (!contentUdi) return;
//this._manager!.updateLayout({ contentUdi, rowSpan: newRowSpan });
this._layout.update({ rowSpan: newRowSpan });
const layoutValue = this._layout.getValue();
if (!layoutValue) return;
this._layout.setValue({
...layoutValue,
rowSpan: newRowSpan,
});
}
}
},

View File

@@ -1,7 +1,7 @@
import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../types.js';
import type { UmbBlockGridWorkspaceData } from '../index.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbArrayState, appendToFrozenArray, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState, appendToFrozenArray, pushAtToUniqueArray } from '@umbraco-cms/backoffice/observable-api';
import { type UmbBlockDataType, UmbBlockManagerContext } from '@umbraco-cms/backoffice/block';
import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type';
@@ -71,7 +71,10 @@ export class UmbBlockGridManagerContext<
// Append the layout entry to be inserted and unfreeze the rest of the data:
const areas = currentEntry.areas.map((x) =>
x.key === areaKey
? { ...x, items: appendToFrozenArray(x.items, insert, (x) => x.contentUdi === insert.contentUdi) }
? {
...x,
items: pushAtToUniqueArray([...x.items], insert, (x) => x.contentUdi === insert.contentUdi, index),
}
: x,
);
return appendToFrozenArray(

View File

@@ -128,11 +128,11 @@ export class UmbBlockGridScaleManager extends UmbControllerBase {
let newColumnSpan = Math.max(blockEndCol - blockStartCol, 1);
const spanOptions = this._host.getRelevantColumnSpanOptions();
if (!spanOptions) return;
const columnOptions = this._host.getRelevantColumnSpanOptions();
if (!columnOptions) return;
// Find nearest allowed Column:
const bestColumnSpanOption = closestColumnSpanOption(newColumnSpan, spanOptions, layoutColumns - blockStartCol);
const bestColumnSpanOption = closestColumnSpanOption(newColumnSpan, columnOptions, layoutColumns - blockStartCol);
newColumnSpan = bestColumnSpanOption ?? layoutColumns;
// Find allowed row spans:

View File

@@ -176,6 +176,7 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
(x) => x.contentUdi,
(layoutEntry, index) =>
html`<uui-button-inline-create
label=${this._createButtonLabel}
href=${this._catalogueRouteBuilder?.({ view: 'create', index: index }) ?? ''}></uui-button-inline-create>
<umb-block-list-entry .contentUdi=${layoutEntry.contentUdi} .layout=${layoutEntry}>
</umb-block-list-entry> `,

View File

@@ -121,13 +121,11 @@ export abstract class UmbBlockEntriesContext<
}
if (layout.settingsUdi) {
this._manager?.removeOneSettings(layout.settingsUdi);
this._manager!.removeOneSettings(layout.settingsUdi);
}
this._manager!.removeOneContent(contentUdi);
this._manager?.removeOneContent(contentUdi);
//this._layoutEntries.removeOne(contentUdi);
alert('TODO: Remove layout entry..');
this._layoutEntries.removeOne(contentUdi);
}
//copy
}

View File

@@ -61,8 +61,10 @@ export abstract class UmbBlockEntryContext<
this.#index.setValue(index);
}
#createPath = new UmbStringState(undefined);
readonly createPath = this.#createPath.asObservable();
#createBeforePath = new UmbStringState(undefined);
readonly createBeforePath = this.#createBeforePath.asObservable();
#createAfterPath = new UmbStringState(undefined);
readonly createAfterPath = this.#createAfterPath.asObservable();
#contentElementTypeName = new UmbStringState(undefined);
public readonly contentElementTypeName = this.#contentElementTypeName.asObservable();
@@ -173,7 +175,7 @@ export abstract class UmbBlockEntryContext<
});
this.observe(this.index, () => {
this.#updateCreatePath();
this.#updateCreatePaths();
});
}
@@ -181,16 +183,18 @@ export abstract class UmbBlockEntryContext<
return this._layout.value?.contentUdi;
}
#updateCreatePath() {
#updateCreatePaths() {
const index = this.#index.value;
if (this._entries && index !== undefined) {
this.observe(
observeMultiple([this._entries.catalogueRouteBuilder, this._entries.canCreate]),
([catalogueRouteBuilder, canCreate]) => {
if (catalogueRouteBuilder && canCreate) {
this.#createPath.setValue(this._entries!.getPathForCreateBlock(index));
this.#createBeforePath.setValue(this._entries!.getPathForCreateBlock(index));
this.#createAfterPath.setValue(this._entries!.getPathForCreateBlock(index + 1));
} else {
this.#createPath.setValue(undefined);
this.#createBeforePath.setValue(undefined);
this.#createAfterPath.setValue(undefined);
}
},
'observeRouteBuilderCreate',
@@ -227,7 +231,7 @@ export abstract class UmbBlockEntryContext<
abstract _gotManager(): void;
#gotEntries() {
this.#updateCreatePath();
this.#updateCreatePaths();
this.#observeLayout();
if (this._entries) {
this.observe(
@@ -245,13 +249,11 @@ export abstract class UmbBlockEntryContext<
abstract _gotEntries(): void;
#observeData() {
if (!this._manager) return;
const contentUdi = this._layout.value?.contentUdi;
if (!contentUdi) return;
if (!this._manager || !this.#contentUdi) return;
// observe content:
this.observe(
this._manager.contentOf(contentUdi),
this._manager.contentOf(this.#contentUdi),
(content) => {
this.#content.setValue(content);
},
@@ -358,6 +360,7 @@ export abstract class UmbBlockEntryContext<
async requestDelete() {
const blockName = this.getLabel();
// TODO: Localizations missing [NL]
await umbConfirmModal(this, {
headline: `Delete ${blockName}`,
content: `Are you sure you want to delete this ${blockName}?`,