From 66d7105b34e9f3c7d4cf5e432e51d58561252996 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 3 May 2022 19:51:01 -0700 Subject: [PATCH] Canvas: Group constraint support (#48563) --- .../app/features/canvas/runtime/element.tsx | 21 ++++- public/app/features/canvas/runtime/group.tsx | 84 ++++++++++--------- public/app/features/canvas/runtime/root.tsx | 14 ++++ public/app/features/canvas/runtime/scene.tsx | 50 ++++++++++- .../canvas/editor/LayerElementListEditor.tsx | 9 +- .../panel/canvas/editor/layerEditor.tsx | 1 + 6 files changed, 129 insertions(+), 50 deletions(-) diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 37000ed02bd..dd41cf89d99 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -14,6 +14,7 @@ import { DimensionContext } from 'app/features/dimensions'; import { HorizontalConstraint, Placement, VerticalConstraint } from '../types'; import { GroupState } from './group'; +import { RootElement } from './root'; import { Scene } from './scene'; 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 **/ applyLayoutStylesToDiv() { + if (this.isRoot()) { + // Root supersedes layout engine and is always 100% width + height of panel + return; + } + const { constraint } = this.options; const { vertical, horizontal } = constraint ?? {}; 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 { vertical, horizontal } = constraint ?? {}; - const elementContainer = this.div && this.div.getBoundingClientRect(); - const parentContainer = this.div && this.div.parentElement?.getBoundingClientRect(); + if (!elementContainer) { + elementContainer = this.div && this.div.getBoundingClientRect(); + } + if (!parentContainer) { + parentContainer = this.div && this.div.parentElement?.getBoundingClientRect(); + } const relativeTop = elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.top - parentContainer.top)) : 0; @@ -305,6 +315,11 @@ export class ElementState implements LayerElement { } this.dataStyle = css; + this.applyLayoutStylesToDiv(); + } + + isRoot(): this is RootElement { + return false; } /** Recursively visit all nodes */ diff --git a/public/app/features/canvas/runtime/group.tsx b/public/app/features/canvas/runtime/group.tsx index 35d38155a90..809dfc12fa4 100644 --- a/public/app/features/canvas/runtime/group.tsx +++ b/public/app/features/canvas/runtime/group.tsx @@ -82,7 +82,7 @@ export class GroupState extends ElementState { // ??? or should this be on the element directly? // are actions scoped to layers? - doAction = (action: LayerActionID, element: ElementState, updateName = true) => { + doAction = (action: LayerActionID, element: ElementState, updateName = true, shiftItemsOnDuplicate = true) => { switch (action) { case LayerActionID.Delete: this.elements = this.elements.filter((e) => e !== element); @@ -97,48 +97,50 @@ export class GroupState extends ElementState { } const opts = cloneDeep(element.options); - const { constraint, placement: oldPlacement } = element.options; - const { vertical, horizontal } = constraint ?? {}; - const placement = oldPlacement ?? ({} as Placement); + if (shiftItemsOnDuplicate) { + const { constraint, placement: oldPlacement } = element.options; + const { vertical, horizontal } = constraint ?? {}; + const placement = oldPlacement ?? ({} as Placement); - switch (vertical) { - case VerticalConstraint.Top: - case VerticalConstraint.TopBottom: - if (placement.top == null) { - placement.top = 25; - } else { - placement.top += 10; - } - break; - case VerticalConstraint.Bottom: - if (placement.bottom == null) { - placement.bottom = 100; - } else { - placement.bottom -= 10; - } - break; + switch (vertical) { + case VerticalConstraint.Top: + case VerticalConstraint.TopBottom: + if (placement.top == null) { + placement.top = 25; + } else { + placement.top += 10; + } + break; + case VerticalConstraint.Bottom: + if (placement.bottom == null) { + placement.bottom = 100; + } else { + placement.bottom -= 10; + } + 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); copy.updateData(this.scene.context); if (updateName) { @@ -157,7 +159,7 @@ export class GroupState extends ElementState { render() { return ( -
+
{this.elements.map((v) => v.render())}
); diff --git a/public/app/features/canvas/runtime/root.tsx b/public/app/features/canvas/runtime/root.tsx index 8eebe0c01aa..47b1339356d 100644 --- a/public/app/features/canvas/runtime/root.tsx +++ b/public/app/features/canvas/runtime/root.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas'; import { GroupState } from './group'; @@ -32,4 +34,16 @@ export class RootElement extends GroupState { elements: this.elements.map((v) => v.getSaveModel()), }; } + + setRootRef = (target: HTMLDivElement) => { + this.div = target; + }; + + render() { + return ( +
+ {this.elements.map((v) => v.render())} +
+ ); + } } diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 41db45e95a6..edfcb5689f2 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -26,6 +26,8 @@ import { } from 'app/features/dimensions/utils'; import { LayerActionID } from 'app/plugins/panel/canvas/types'; +import { Placement } from '../types'; + import { ElementState } from './element'; import { GroupState } from './group'; import { RootElement } from './root'; @@ -138,11 +140,19 @@ export class Scene { currentSelectedElements[0].parent ); + const groupPlacement = this.generateGroupContainer(currentSelectedElements); + + newLayer.options.placement = groupPlacement; + currentSelectedElements.forEach((element: ElementState) => { + const elementContainer = element.div?.getBoundingClientRect(); + element.setPlacementFromConstraint(elementContainer, groupPlacement as DOMRect); 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); 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() { let event: MouseEvent = new MouseEvent('click'); this.selecto?.clickTarget(event, this.div); diff --git a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx index 75d36be7784..809495898b5 100644 --- a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx @@ -55,10 +55,7 @@ export class LayerElementListEditor extends PureComponent { let selection: SelectionParams = { targets: [] }; if (item instanceof GroupState) { const targetElements: HTMLDivElement[] = []; - item.elements.forEach((element: ElementState) => { - targetElements.push(element.div!); - }); - + targetElements.push(item?.div!); selection.targets = targetElements; selection.group = item; settings.scene.select(selection); @@ -129,7 +126,9 @@ export class LayerElementListEditor extends PureComponent { this.deleteGroup(); 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); }); }; diff --git a/public/app/plugins/panel/canvas/editor/layerEditor.tsx b/public/app/plugins/panel/canvas/editor/layerEditor.tsx index 9c07fd425c1..dcf2ba29abe 100644 --- a/public/app/plugins/panel/canvas/editor/layerEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/layerEditor.tsx @@ -57,6 +57,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions