mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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 { 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 */
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user