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); + }; }