pre work on scale handler

This commit is contained in:
Niels Lyngsø
2024-02-19 10:05:44 +01:00
parent 1078cb1f25
commit 4966b8ff94
5 changed files with 335 additions and 6 deletions

View File

@@ -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
<div class="umb-block-grid__block" part="umb-block-grid__block">
<umb-extension-slot
type="blockEditorCustomView"
default-element=${'umb-block-grid-block'}
default-element="umb-block-grid-block"
.props=${this._blockViewProps}
>${this.#renderRefBlock()}</umb-extension-slot
>
@@ -180,6 +181,9 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
<uui-icon name="icon-remove"></uui-icon>
</uui-button>
</uui-action-bar>
<umb-block-scale-handler @mousedown=${(e: MouseEvent) => this.#context.onScaleMouseDown(e)}>
</umb-block-scale-handler>
</div>
`
: '';
@@ -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);
}
`,
];
}

View File

@@ -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`
<div id="handler"></div>
<div id="label">TODO: Label content [NL]</div>
`;
}
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;

View File

@@ -0,0 +1 @@
export * from './block-scale-handler.element.js';

View File

@@ -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);

View File

@@ -95,6 +95,10 @@ export class UmbBlockGridEntryContext extends UmbBlockEntryContext<
#canScale = new UmbBooleanState(false);
readonly canScale = this.#canScale.asObservable();
#runtimeGridColumns: Array<number> = [];
#runtimeGridRows: Array<number> = [];
#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);
};
}