diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts
index 31fc167586..cc523d72d8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts
@@ -1,8 +1,9 @@
-import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbBlockGridEntryContext } from '../../context/block-grid-entry.context.js';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, css, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import '../block-grid-block-view/index.js';
+import '../block-scale-handler/index.js';
import type { UmbBlockGridLayoutModel, UmbBlockViewPropsType } from '@umbraco-cms/backoffice/block';
/**
@@ -161,7 +162,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
${this.#renderRefBlock()}
@@ -180,6 +181,9 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
+
+ this.#context.onScaleMouseDown(e)}>
+
`
: '';
@@ -204,6 +208,29 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
:host([drag-placeholder]) {
opacity: 0.2;
}
+
+ :host(::after) {
+ content: '';
+ position: absolute;
+ z-index: 1;
+ pointer-events: none;
+ display: none;
+ inset: 0;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ box-shadow:
+ 0 0 0 1px rgba(255, 255, 255, 0.7),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.7);
+
+ transition: border-color 240ms ease-in;
+ }
+
+ :host(:hover::after) {
+ // TODO: Look at the feature I out-commented here, what was that suppose to do [NL]:
+ //display: var(--umb-block-grid--block-ui-display, block);
+ display: block;
+ border-color: var(--uui-color-interactive);
+ }
`,
];
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/block-scale-handler.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/block-scale-handler.element.ts
index 60deff9d28..91fc79bb54 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/block-scale-handler.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/block-scale-handler.element.ts
@@ -8,11 +8,83 @@ import '../block-grid-block-view/index.js';
*/
@customElement('umb-block-scale-handler')
export class UmbBlockGridScaleHandlerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
+ //
render() {
- return html``;
+ return html`
+
+ TODO: Label content [NL]
+ `;
}
- static styles = [css``];
+ static styles = [
+ css`
+ :host() {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ }
+
+ #handler {
+ pointer-events: all;
+ cursor: nwse-resize;
+ position: absolute;
+ // TODO: Look at the feature I out-commented here, what was that supose to do [NL]:
+ //display: var(--umb-block-grid--block-ui-display, block);
+ display: block;
+ z-index: 10;
+ bottom: -4px;
+ right: -4px;
+ width: 8px;
+ height: 8px;
+ padding: 0;
+ background-color: var(--uui-color-surface);
+ border: var(--uui-color-interative) solid 1px;
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7);
+ opacity: 0;
+ transition: opacity 120ms;
+ }
+ #handler:focus {
+ opacity: 1;
+ }
+ #handler::after {
+ content: '';
+ position: absolute;
+ inset: -10px;
+ background-color: rgba(0, 0, 0, 0);
+ }
+ #handler:focus + #label,
+ #handler:hover + #label {
+ opacity: 1;
+ }
+
+ #label {
+ position: absolute;
+ display: block;
+ left: 100%;
+ margin-left: 6px;
+ margin-top: 6px;
+ z-index: 2;
+
+ background-color: white;
+ color: black;
+ font-size: 12px;
+ padding: 0px 6px;
+ border-radius: 3px;
+ opacity: 0;
+ transition: opacity 120ms;
+
+ pointer-events: none;
+ white-space: nowrap;
+ }
+
+ :host([scale-mode]) > #handler {
+ opacity: 1;
+ }
+ :host([scale-mode]) > #label {
+ opacity: 1;
+ }
+ `,
+ ];
}
export default UmbBlockGridScaleHandlerElement;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts
new file mode 100644
index 0000000000..3d6b5076ae
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts
@@ -0,0 +1 @@
+export * from './block-scale-handler.element.js';
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 c843170ebb..519c66fc2e 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
@@ -43,6 +43,10 @@ export class UmbBlockGridEntriesContext extends UmbBlockEntriesContext<
return this.#layoutColumns.getValue();
}
+ getLayoutContainerElement() {
+ return this.getHostElement().querySelector('.umb-block-grid__layout-container');
+ }
+
constructor(host: UmbControllerHost) {
super(host, UMB_BLOCK_GRID_MANAGER_CONTEXT);
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 f6f8b2fd6c..85ce77cd31 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
@@ -95,6 +95,10 @@ export class UmbBlockGridEntryContext extends UmbBlockEntryContext<
#canScale = new UmbBooleanState(false);
readonly canScale = this.#canScale.asObservable();
+ #runtimeGridColumns: Array = [];
+ #runtimeGridRows: Array = [];
+ #lockedGridRows = 0;
+
readonly showContentEdit = this._blockType.asObservablePart((x) => !x?.forceHideContentEditorInOverlay);
constructor(host: UmbControllerHost) {
@@ -136,20 +140,37 @@ export class UmbBlockGridEntryContext extends UmbBlockEntryContext<
if (!this._entries) return;
const layoutColumns = this._entries.getLayoutColumns();
if (!layoutColumns) return;
+ /*
const oldColumnSpan = this._layout.getValue()?.columnSpan;
if (!oldColumnSpan) {
// Some fallback solution, to reset it so something that makes sense.
return;
}
+ */
columnSpan = Math.max(1, Math.min(columnSpan, layoutColumns));
+ /*
const columnSpanOptions = this.#relevantColumnSpanOptions.getValue();
if (columnSpanOptions.length > 0) {
columnSpan = closestColumnSpanOption(columnSpan, columnSpanOptions, layoutColumns) ?? layoutColumns;
- }
+ }*/
this._layout.update({ columnSpan });
}
+ getColumnSpan() {
+ return this._layout.getValue()?.columnSpan;
+ }
+
+ setRowSpan(rowSpan: number) {
+ const blockType = this._blockType.getValue();
+ if (!blockType) return;
+ rowSpan = Math.max(blockType.rowMinSpan, Math.min(rowSpan, blockType.rowMaxSpan));
+ this._layout.update({ rowSpan });
+ }
+
+ getRowSpan() {
+ return this._layout.getValue()?.rowSpan;
+ }
_gotManager() {
if (this._manager) {
@@ -172,7 +193,6 @@ export class UmbBlockGridEntryContext extends UmbBlockEntryContext<
combineLatest([this.minMaxRowSpan, this.columnSpanOptions, this._entries.layoutColumns]),
([minMaxRowSpan, columnSpanOptions, layoutColumns]) => {
if (!layoutColumns) return;
- console.log('calc', columnSpanOptions, layoutColumns);
const relevantColumnSpanOptions = columnSpanOptions
? columnSpanOptions
.filter((x) => x.columnSpan <= layoutColumns)
@@ -189,4 +209,209 @@ export class UmbBlockGridEntryContext extends UmbBlockEntryContext<
'observeScaleOptions',
);
}
+
+ // Scaling feature:
+
+ #updateGridData(
+ layoutContainer: HTMLElement,
+ layoutContainerRect: DOMRect,
+ layoutItemRect: DOMRect,
+ updateRowTemplate: boolean,
+ ) {
+ if (!this._entries) return;
+
+ const computedStyles = window.getComputedStyle(layoutContainer);
+
+ const columnGap = Number(computedStyles.columnGap.split('px')[0]) || 0;
+ const rowGap = Number(computedStyles.rowGap.split('px')[0]) || 0;
+
+ let gridColumns = computedStyles.gridTemplateColumns
+ .trim()
+ .split('px')
+ .map((x) => Number(x));
+ let gridRows = computedStyles.gridTemplateRows
+ .trim()
+ .split('px')
+ .map((x) => Number(x));
+
+ // remove empties:
+ gridColumns = gridColumns.filter((n) => n > 0);
+ gridRows = gridRows.filter((n) => n > 0);
+
+ // We use this code to lock the templateRows, while scaling. otherwise scaling Rows is too crazy.
+ if (updateRowTemplate || gridRows.length > this.#lockedGridRows) {
+ this.#lockedGridRows = gridRows.length;
+ layoutContainer.style.gridTemplateRows = computedStyles.gridTemplateRows;
+ }
+
+ // add gaps:
+ const gridColumnsLen = gridColumns.length;
+ gridColumns = gridColumns.map((n, i) => (gridColumnsLen === i ? n : n + columnGap));
+ const gridRowsLen = gridRows.length;
+ gridRows = gridRows.map((n, i) => (gridRowsLen === i ? n : n + rowGap));
+
+ // ensure all columns are there.
+ // This will also ensure handling non-css-grid mode,
+ // use container width divided by amount of columns( or the item width divided by its amount of columnSpan)
+ let amountOfColumnsInWeightMap = gridColumns.length;
+ const layoutColumns = this._entries.getLayoutColumns() ?? 0;
+ const amountOfUnknownColumns = layoutColumns - amountOfColumnsInWeightMap;
+ if (amountOfUnknownColumns > 0) {
+ const accumulatedValue = getAccumulatedValueOfIndex(amountOfColumnsInWeightMap, gridColumns) || 0;
+ const missingColumnWidth = (layoutContainerRect.width - accumulatedValue) / amountOfUnknownColumns;
+ while (amountOfColumnsInWeightMap++ < layoutColumns) {
+ gridColumns.push(missingColumnWidth);
+ }
+ }
+
+ // Handle non css grid mode for Rows:
+ // use item height divided by rowSpan to identify row heights.
+ if (gridRows.length === 0) {
+ // Push its own height twice, to give something to scale with.
+ gridRows.push(layoutItemRect.top - layoutContainerRect.top);
+
+ let i = 0;
+ const itemSingleRowHeight = layoutItemRect.height;
+ while (i++ < (this.getRowSpan() ?? 0)) {
+ gridRows.push(itemSingleRowHeight);
+ }
+ }
+
+ // add a few extra rows, so there is something to extend too.
+ // Add extra options for the ability to extend beyond current content:
+ gridRows.push(50);
+ gridRows.push(50);
+ gridRows.push(50);
+ gridRows.push(50);
+ gridRows.push(50);
+
+ this.#runtimeGridColumns = gridColumns;
+ this.#runtimeGridRows = gridRows;
+ }
+
+ // TODO: Rename to calc something.
+ #getNewSpans(startX: number, startY: number, endX: number, endY: number) {
+ const layoutColumns = this._entries?.getLayoutColumns();
+ if (!layoutColumns) return;
+
+ const blockStartCol = Math.round(getInterpolatedIndexOfPositionInWeightMap(startX, this.#runtimeGridColumns));
+ const blockStartRow = Math.round(getInterpolatedIndexOfPositionInWeightMap(startY, this.#runtimeGridRows));
+ const blockEndCol = getInterpolatedIndexOfPositionInWeightMap(endX, this.#runtimeGridColumns);
+ const blockEndRow = getInterpolatedIndexOfPositionInWeightMap(endY, this.#runtimeGridRows);
+
+ let newColumnSpan = Math.max(blockEndCol - blockStartCol, 1);
+
+ // Find nearest allowed Column:
+ const bestColumnSpanOption = closestColumnSpanOption(
+ newColumnSpan,
+ this.#relevantColumnSpanOptions.getValue(),
+ layoutColumns - blockStartCol,
+ );
+ newColumnSpan = bestColumnSpanOption ?? layoutColumns;
+
+ let newRowSpan = Math.round(Math.max(blockEndRow - blockStartRow, this._blockType.getValue()?.rowMinSpan || 1));
+ const rowMaxSpan = this._blockType.getValue()?.rowMaxSpan;
+ if (rowMaxSpan != null) {
+ newRowSpan = Math.min(newRowSpan, rowMaxSpan);
+ }
+
+ return { columnSpan: newColumnSpan, rowSpan: newRowSpan, startCol: blockStartCol, startRow: blockStartRow };
+ }
+
+ public onScaleMouseDown(event: MouseEvent) {
+ const layoutContainer = this._entries?.getLayoutContainerElement() as HTMLElement;
+ if (!layoutContainer) {
+ return;
+ }
+ event.preventDefault();
+
+ console.log('onScaleMouseDown');
+
+ //this.#isScaleMode = true;
+
+ window.addEventListener('mousemove', this.onScaleMouseMove);
+ window.addEventListener('mouseup', this.onScaleMouseUp);
+ window.addEventListener('mouseleave', this.onScaleMouseUp);
+
+ const layoutItemRect = this.getHostElement().getBoundingClientRect();
+ this.#updateGridData(layoutContainer, layoutContainer.getBoundingClientRect(), layoutItemRect, true);
+
+ /*
+ scaleBoxBackdropEl = document.createElement('div');
+ scaleBoxBackdropEl.className = 'umb-block-grid__scalebox-backdrop';
+ layoutContainer.appendChild(scaleBoxBackdropEl);
+ */
+ }
+
+ onScaleMouseMove = (e: MouseEvent) => {
+ const layoutContainer = this._entries?.getLayoutContainerElement() as HTMLElement;
+ if (!layoutContainer) {
+ return;
+ }
+ const layoutContainerRect = layoutContainer.getBoundingClientRect();
+ const layoutItemRect = this.getHostElement().getBoundingClientRect();
+
+ const startX = layoutItemRect.left - layoutContainerRect.left;
+ const startY = layoutItemRect.top - layoutContainerRect.top;
+ const endX = e.clientX - layoutContainerRect.left;
+ const endY = e.clientY - layoutContainerRect.top;
+
+ const newSpans = this.#getNewSpans(startX, startY, endX, endY);
+ if (!newSpans) return;
+
+ const updateRowTemplate = this.getColumnSpan() !== newSpans.columnSpan;
+
+ if (updateRowTemplate) {
+ // If we like to update we need to first remove the lock, make the browser render onces and then update.
+ (layoutContainer as HTMLElement).style.gridTemplateRows = '';
+ }
+ //cancelAnimationFrame(raf);
+ //raf = requestAnimationFrame(() => {
+ // As mentioned above we need to wait until the browser has rendered DOM without the lock of gridTemplateRows.
+ this.#updateGridData(layoutContainer, layoutContainerRect, layoutItemRect, updateRowTemplate);
+ //});
+
+ // update as we go:
+ this.setColumnSpan(newSpans.columnSpan);
+ this.setRowSpan(newSpans.rowSpan);
+ };
+
+ onScaleMouseUp = (e: MouseEvent) => {
+ const layoutContainer = this._entries?.getLayoutContainerElement() as HTMLElement;
+ if (!layoutContainer) {
+ return;
+ }
+ //cancelAnimationFrame(raf);
+
+ // Remove listeners:
+ window.removeEventListener('mousemove', this.onScaleMouseMove);
+ window.removeEventListener('mouseup', this.onScaleMouseUp);
+ window.removeEventListener('mouseleave', this.onScaleMouseUp);
+
+ const layoutContainerRect = layoutContainer.getBoundingClientRect();
+ const layoutItemRect = this.getHostElement().getBoundingClientRect();
+
+ const startX = layoutItemRect.left - layoutContainerRect.left;
+ const startY = layoutItemRect.top - layoutContainerRect.top;
+ const endX = e.clientX - layoutContainerRect.left;
+ const endY = e.clientY - layoutContainerRect.top;
+ const newSpans = this.#getNewSpans(startX, startY, endX, endY);
+
+ // release the lock of gridTemplateRows:
+ //layoutContainer.removeChild(scaleBoxBackdropEl);
+ //this.scaleBoxBackdropEl = null;
+ layoutContainer.style.gridTemplateRows = '';
+ //this.#isScaleMode = false;
+
+ // Clean up variables:
+ //this.layoutContainer = null;
+ //this.gridColumns = [];
+ //this.gridRows = [];
+ this.#lockedGridRows = 0;
+
+ if (!newSpans) return;
+ // Update block size:
+ this.setColumnSpan(newSpans.columnSpan);
+ this.setRowSpan(newSpans.rowSpan);
+ };
}