mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
Canvas: Group constraint support (#48563)
This commit is contained in:
parent
38fc0c68e4
commit
66d7105b34
@ -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 */
|
||||
|
@ -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 (
|
||||
<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())}
|
||||
</div>
|
||||
);
|
||||
|
@ -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 (
|
||||
<div key={this.UID} ref={this.setRootRef} style={{ ...this.sizeStyle, ...this.dataStyle }}>
|
||||
{this.elements.map((v) => v.render())}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -55,10 +55,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
||||
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<Props> {
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -57,6 +57,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
|
||||
}
|
||||
const c = setOptionImmutably(options, path, value);
|
||||
scene.currentLayer?.onChange(c);
|
||||
scene.currentLayer?.updateData(scene.context);
|
||||
},
|
||||
}),
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user