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:
@@ -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';
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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> `,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}?`,
|
||||
|
||||
Reference in New Issue
Block a user