pre work on scale handler
This commit is contained in:
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './block-scale-handler.element.js';
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user