Canvas: Group constraint support (#48563)

This commit is contained in:
Nathan Marrs 2022-05-03 19:51:01 -07:00 committed by GitHub
parent 38fc0c68e4
commit 66d7105b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 50 deletions

View File

@ -14,6 +14,7 @@ import { DimensionContext } from 'app/features/dimensions';
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types'; import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
import { GroupState } from './group'; import { GroupState } from './group';
import { RootElement } from './root';
import { Scene } from './scene'; import { Scene } from './scene';
let counter = 0; let counter = 0;
@ -68,6 +69,11 @@ export class ElementState implements LayerElement {
/** Use the configured options to update CSS style properties directly on the wrapper div **/ /** Use the configured options to update CSS style properties directly on the wrapper div **/
applyLayoutStylesToDiv() { applyLayoutStylesToDiv() {
if (this.isRoot()) {
// Root supersedes layout engine and is always 100% width + height of panel
return;
}
const { constraint } = this.options; const { constraint } = this.options;
const { vertical, horizontal } = constraint ?? {}; const { vertical, horizontal } = constraint ?? {};
const placement = this.options.placement ?? ({} as Placement); const placement = this.options.placement ?? ({} as Placement);
@ -170,12 +176,16 @@ export class ElementState implements LayerElement {
} }
} }
setPlacementFromConstraint() { setPlacementFromConstraint(elementContainer?: DOMRect, parentContainer?: DOMRect) {
const { constraint } = this.options; const { constraint } = this.options;
const { vertical, horizontal } = constraint ?? {}; const { vertical, horizontal } = constraint ?? {};
const elementContainer = this.div && this.div.getBoundingClientRect(); if (!elementContainer) {
const parentContainer = this.div && this.div.parentElement?.getBoundingClientRect(); elementContainer = this.div && this.div.getBoundingClientRect();
}
if (!parentContainer) {
parentContainer = this.div && this.div.parentElement?.getBoundingClientRect();
}
const relativeTop = const relativeTop =
elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.top - parentContainer.top)) : 0; elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.top - parentContainer.top)) : 0;
@ -305,6 +315,11 @@ export class ElementState implements LayerElement {
} }
this.dataStyle = css; this.dataStyle = css;
this.applyLayoutStylesToDiv();
}
isRoot(): this is RootElement {
return false;
} }
/** Recursively visit all nodes */ /** Recursively visit all nodes */

View File

@ -82,7 +82,7 @@ export class GroupState extends ElementState {
// ??? or should this be on the element directly? // ??? or should this be on the element directly?
// are actions scoped to layers? // are actions scoped to layers?
doAction = (action: LayerActionID, element: ElementState, updateName = true) => { doAction = (action: LayerActionID, element: ElementState, updateName = true, shiftItemsOnDuplicate = true) => {
switch (action) { switch (action) {
case LayerActionID.Delete: case LayerActionID.Delete:
this.elements = this.elements.filter((e) => e !== element); this.elements = this.elements.filter((e) => e !== element);
@ -97,48 +97,50 @@ export class GroupState extends ElementState {
} }
const opts = cloneDeep(element.options); const opts = cloneDeep(element.options);
const { constraint, placement: oldPlacement } = element.options; if (shiftItemsOnDuplicate) {
const { vertical, horizontal } = constraint ?? {}; const { constraint, placement: oldPlacement } = element.options;
const placement = oldPlacement ?? ({} as Placement); const { vertical, horizontal } = constraint ?? {};
const placement = oldPlacement ?? ({} as Placement);
switch (vertical) { switch (vertical) {
case VerticalConstraint.Top: case VerticalConstraint.Top:
case VerticalConstraint.TopBottom: case VerticalConstraint.TopBottom:
if (placement.top == null) { if (placement.top == null) {
placement.top = 25; placement.top = 25;
} else { } else {
placement.top += 10; placement.top += 10;
} }
break; break;
case VerticalConstraint.Bottom: case VerticalConstraint.Bottom:
if (placement.bottom == null) { if (placement.bottom == null) {
placement.bottom = 100; placement.bottom = 100;
} else { } else {
placement.bottom -= 10; placement.bottom -= 10;
} }
break; break;
}
switch (horizontal) {
case HorizontalConstraint.Left:
case HorizontalConstraint.LeftRight:
if (placement.left == null) {
placement.left = 50;
} else {
placement.left += 10;
}
break;
case HorizontalConstraint.Right:
if (placement.right == null) {
placement.right = 50;
} else {
placement.right -= 10;
}
break;
}
opts.placement = placement;
} }
switch (horizontal) {
case HorizontalConstraint.Left:
case HorizontalConstraint.LeftRight:
if (placement.left == null) {
placement.left = 50;
} else {
placement.left += 10;
}
break;
case HorizontalConstraint.Right:
if (placement.right == null) {
placement.right = 50;
} else {
placement.right -= 10;
}
break;
}
opts.placement = placement;
const copy = new ElementState(element.item, opts, this); const copy = new ElementState(element.item, opts, this);
copy.updateData(this.scene.context); copy.updateData(this.scene.context);
if (updateName) { if (updateName) {
@ -157,7 +159,7 @@ export class GroupState extends ElementState {
render() { render() {
return ( return (
<div key={`${this.UID}/${this.revId}`} style={{ ...this.sizeStyle, ...this.dataStyle }}> <div key={this.UID} ref={this.initElement} style={{ overflow: 'hidden' }}>
{this.elements.map((v) => v.render())} {this.elements.map((v) => v.render())}
</div> </div>
); );

View File

@ -1,3 +1,5 @@
import React from 'react';
import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas'; import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas';
import { GroupState } from './group'; import { GroupState } from './group';
@ -32,4 +34,16 @@ export class RootElement extends GroupState {
elements: this.elements.map((v) => v.getSaveModel()), elements: this.elements.map((v) => v.getSaveModel()),
}; };
} }
setRootRef = (target: HTMLDivElement) => {
this.div = target;
};
render() {
return (
<div key={this.UID} ref={this.setRootRef} style={{ ...this.sizeStyle, ...this.dataStyle }}>
{this.elements.map((v) => v.render())}
</div>
);
}
} }

View File

@ -26,6 +26,8 @@ import {
} from 'app/features/dimensions/utils'; } from 'app/features/dimensions/utils';
import { LayerActionID } from 'app/plugins/panel/canvas/types'; import { LayerActionID } from 'app/plugins/panel/canvas/types';
import { Placement } from '../types';
import { ElementState } from './element'; import { ElementState } from './element';
import { GroupState } from './group'; import { GroupState } from './group';
import { RootElement } from './root'; import { RootElement } from './root';
@ -138,11 +140,19 @@ export class Scene {
currentSelectedElements[0].parent currentSelectedElements[0].parent
); );
const groupPlacement = this.generateGroupContainer(currentSelectedElements);
newLayer.options.placement = groupPlacement;
currentSelectedElements.forEach((element: ElementState) => { currentSelectedElements.forEach((element: ElementState) => {
const elementContainer = element.div?.getBoundingClientRect();
element.setPlacementFromConstraint(elementContainer, groupPlacement as DOMRect);
currentLayer.doAction(LayerActionID.Delete, element); currentLayer.doAction(LayerActionID.Delete, element);
newLayer.doAction(LayerActionID.Duplicate, element, false); newLayer.doAction(LayerActionID.Duplicate, element, false, false);
}); });
newLayer.setPlacementFromConstraint(groupPlacement as DOMRect, currentLayer.div?.getBoundingClientRect());
currentLayer.elements.push(newLayer); currentLayer.elements.push(newLayer);
this.byName.set(newLayer.getName(), newLayer); this.byName.set(newLayer.getName(), newLayer);
@ -151,6 +161,44 @@ export class Scene {
}); });
} }
private generateGroupContainer = (elements: ElementState[]): Placement => {
let minTop = Infinity;
let minLeft = Infinity;
let maxRight = 0;
let maxBottom = 0;
elements.forEach((element: ElementState) => {
const elementContainer = element.div?.getBoundingClientRect();
if (!elementContainer) {
return;
}
if (minTop > elementContainer.top) {
minTop = elementContainer.top;
}
if (minLeft > elementContainer.left) {
minLeft = elementContainer.left;
}
if (maxRight < elementContainer.right) {
maxRight = elementContainer.right;
}
if (maxBottom < elementContainer.bottom) {
maxBottom = elementContainer.bottom;
}
});
return {
top: minTop,
left: minLeft,
width: maxRight - minLeft,
height: maxBottom - minTop,
};
};
clearCurrentSelection() { clearCurrentSelection() {
let event: MouseEvent = new MouseEvent('click'); let event: MouseEvent = new MouseEvent('click');
this.selecto?.clickTarget(event, this.div); this.selecto?.clickTarget(event, this.div);

View File

@ -55,10 +55,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
let selection: SelectionParams = { targets: [] }; let selection: SelectionParams = { targets: [] };
if (item instanceof GroupState) { if (item instanceof GroupState) {
const targetElements: HTMLDivElement[] = []; const targetElements: HTMLDivElement[] = [];
item.elements.forEach((element: ElementState) => { targetElements.push(item?.div!);
targetElements.push(element.div!);
});
selection.targets = targetElements; selection.targets = targetElements;
selection.group = item; selection.group = item;
settings.scene.select(selection); settings.scene.select(selection);
@ -129,7 +126,9 @@ export class LayerElementListEditor extends PureComponent<Props> {
this.deleteGroup(); this.deleteGroup();
layer.elements.forEach((element: ElementState) => { layer.elements.forEach((element: ElementState) => {
layer.parent?.doAction(LayerActionID.Duplicate, element, false); const elementContainer = element.div?.getBoundingClientRect();
element.setPlacementFromConstraint(elementContainer, layer.parent?.div?.getBoundingClientRect());
layer.parent?.doAction(LayerActionID.Duplicate, element, false, false);
}); });
}; };

View File

@ -57,6 +57,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
} }
const c = setOptionImmutably(options, path, value); const c = setOptionImmutably(options, path, value);
scene.currentLayer?.onChange(c); scene.currentLayer?.onChange(c);
scene.currentLayer?.updateData(scene.context);
}, },
}), }),