mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Implement new constraint system (#47911)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { RegistryItem } from '@grafana/data';
|
||||
import { Anchor, BackgroundConfig, LineConfig, Placement } from './types';
|
||||
import { BackgroundConfig, Constraint, LineConfig, Placement } from './types';
|
||||
import { DimensionContext } from '../dimensions/context';
|
||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface CanvasElementOptions<TConfig = any> {
|
||||
config?: TConfig;
|
||||
|
||||
// Standard options available for all elements
|
||||
anchor?: Anchor; // defaults top, left, width and height
|
||||
constraint?: Constraint; // defaults vertical - top, horizontal - left
|
||||
placement?: Placement;
|
||||
background?: BackgroundConfig;
|
||||
border?: LineConfig;
|
||||
@@ -29,10 +29,6 @@ export interface CanvasElementProps<TConfig = any, TData = any> {
|
||||
// Saved config
|
||||
config: TConfig;
|
||||
|
||||
// Calculated position info
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
// Raw data
|
||||
data?: TData;
|
||||
}
|
||||
@@ -53,6 +49,6 @@ export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryI
|
||||
|
||||
getNewOptions: (options?: CanvasElementOptions) => Omit<CanvasElementOptions<TConfig>, 'type' | 'name'>;
|
||||
|
||||
/** Build the configuraiton UI */
|
||||
/** Build the configuration UI */
|
||||
registerOptionsUI?: PanelOptionsSupplier<CanvasElementOptions<TConfig>>;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const svgStrokePathClass = css`
|
||||
`;
|
||||
|
||||
export function IconDisplay(props: CanvasElementProps) {
|
||||
const { width, height, data } = props;
|
||||
const { data } = props;
|
||||
if (!data?.path) {
|
||||
return null;
|
||||
}
|
||||
@@ -59,8 +59,6 @@ export function IconDisplay(props: CanvasElementProps) {
|
||||
<SVG
|
||||
onClick={onClick}
|
||||
src={data.path}
|
||||
width={width}
|
||||
height={height}
|
||||
style={svgStyle}
|
||||
className={svgStyle.strokeWidth ? svgStrokePathClass : undefined}
|
||||
/>
|
||||
@@ -78,6 +76,8 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
||||
placement: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
...options,
|
||||
config: {
|
||||
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
CanvasElementItem,
|
||||
CanvasElementOptions,
|
||||
canvasElementRegistry,
|
||||
Placement,
|
||||
Anchor,
|
||||
} from 'app/features/canvas';
|
||||
import { DimensionContext } from 'app/features/dimensions';
|
||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||
import { GroupState } from './group';
|
||||
import { LayerElement } from 'app/core/components/Layers/types';
|
||||
import { Scene } from './scene';
|
||||
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
||||
|
||||
let counter = 0;
|
||||
|
||||
@@ -28,23 +27,24 @@ export class ElementState implements LayerElement {
|
||||
div?: HTMLDivElement;
|
||||
|
||||
// Calculated
|
||||
width = 100;
|
||||
height = 100;
|
||||
data?: any; // depends on the type
|
||||
|
||||
// From options, but always set and always valid
|
||||
anchor: Anchor;
|
||||
placement: Placement;
|
||||
|
||||
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
||||
const fallbackName = `Element ${Date.now()}`;
|
||||
if (!options) {
|
||||
this.options = { type: item.id, name: fallbackName };
|
||||
}
|
||||
this.anchor = options.anchor ?? {};
|
||||
this.placement = options.placement ?? {};
|
||||
options.anchor = this.anchor;
|
||||
options.placement = this.placement;
|
||||
|
||||
options.constraint = options.constraint ?? {
|
||||
vertical: VerticalConstraint.Top,
|
||||
horizontal: HorizontalConstraint.Left,
|
||||
};
|
||||
options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0 };
|
||||
this.validatePlacement();
|
||||
this.sizeStyle = {
|
||||
...options.placement,
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
const scene = this.getScene();
|
||||
if (!options.name) {
|
||||
@@ -72,71 +72,106 @@ export class ElementState implements LayerElement {
|
||||
}
|
||||
|
||||
validatePlacement() {
|
||||
const { anchor, placement } = this;
|
||||
if (!(anchor.left || anchor.right)) {
|
||||
anchor.left = true;
|
||||
}
|
||||
if (!(anchor.top || anchor.bottom)) {
|
||||
anchor.top = true;
|
||||
const { constraint, placement } = this.options;
|
||||
const { vertical, horizontal } = constraint ?? {};
|
||||
const updatedPlacement = placement ?? ({} as Placement);
|
||||
|
||||
switch (vertical) {
|
||||
case VerticalConstraint.Top:
|
||||
updatedPlacement.top = updatedPlacement.top ?? 0;
|
||||
updatedPlacement.height = updatedPlacement.height ?? 100;
|
||||
delete updatedPlacement.bottom;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
updatedPlacement.bottom = updatedPlacement.bottom ?? 0;
|
||||
updatedPlacement.height = updatedPlacement.height ?? 100;
|
||||
delete updatedPlacement.top;
|
||||
break;
|
||||
case VerticalConstraint.TopBottom:
|
||||
updatedPlacement.top = updatedPlacement.top ?? 0;
|
||||
updatedPlacement.bottom = updatedPlacement.bottom ?? 0;
|
||||
delete updatedPlacement.height;
|
||||
break;
|
||||
}
|
||||
|
||||
const w = placement.width ?? 100; // this.div ? this.div.clientWidth : this.width;
|
||||
const h = placement.height ?? 100; // this.div ? this.div.clientHeight : this.height;
|
||||
|
||||
if (anchor.top) {
|
||||
if (!placement.top) {
|
||||
placement.top = 0;
|
||||
}
|
||||
if (anchor.bottom) {
|
||||
delete placement.height;
|
||||
} else {
|
||||
placement.height = h;
|
||||
delete placement.bottom;
|
||||
}
|
||||
} else if (anchor.bottom) {
|
||||
if (!placement.bottom) {
|
||||
placement.bottom = 0;
|
||||
}
|
||||
placement.height = h;
|
||||
delete placement.top;
|
||||
switch (horizontal) {
|
||||
case HorizontalConstraint.Left:
|
||||
updatedPlacement.left = updatedPlacement.left ?? 0;
|
||||
updatedPlacement.width = updatedPlacement.width ?? 100;
|
||||
delete updatedPlacement.right;
|
||||
break;
|
||||
case HorizontalConstraint.Right:
|
||||
updatedPlacement.right = updatedPlacement.right ?? 0;
|
||||
updatedPlacement.width = updatedPlacement.width ?? 100;
|
||||
delete updatedPlacement.left;
|
||||
break;
|
||||
case HorizontalConstraint.LeftRight:
|
||||
updatedPlacement.left = updatedPlacement.left ?? 0;
|
||||
updatedPlacement.right = updatedPlacement.right ?? 0;
|
||||
delete updatedPlacement.width;
|
||||
break;
|
||||
}
|
||||
|
||||
if (anchor.left) {
|
||||
if (!placement.left) {
|
||||
placement.left = 0;
|
||||
}
|
||||
if (anchor.right) {
|
||||
delete placement.width;
|
||||
} else {
|
||||
placement.width = w;
|
||||
delete placement.right;
|
||||
}
|
||||
} else if (anchor.right) {
|
||||
if (!placement.right) {
|
||||
placement.right = 0;
|
||||
}
|
||||
placement.width = w;
|
||||
delete placement.left;
|
||||
this.options.placement = updatedPlacement;
|
||||
}
|
||||
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
setPlacementFromConstraint() {
|
||||
const { constraint } = this.options;
|
||||
const { vertical, horizontal } = constraint ?? {};
|
||||
|
||||
this.options.anchor = this.anchor;
|
||||
this.options.placement = this.placement;
|
||||
const elementContainer = this.div && this.div.getBoundingClientRect();
|
||||
const parentContainer = this.div && this.div.parentElement?.getBoundingClientRect();
|
||||
|
||||
const relativeTop =
|
||||
elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.top - parentContainer.top)) : 0;
|
||||
const relativeBottom =
|
||||
elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.bottom - parentContainer.bottom)) : 0;
|
||||
const relativeLeft =
|
||||
elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.left - parentContainer.left)) : 0;
|
||||
const relativeRight =
|
||||
elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.right - parentContainer.right)) : 0;
|
||||
|
||||
const placement = {} as Placement;
|
||||
|
||||
const width = elementContainer?.width ?? 100;
|
||||
const height = elementContainer?.height ?? 100;
|
||||
|
||||
switch (vertical) {
|
||||
case VerticalConstraint.Top:
|
||||
placement.top = relativeTop;
|
||||
placement.height = height;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
placement.bottom = relativeBottom;
|
||||
placement.height = height;
|
||||
break;
|
||||
case VerticalConstraint.TopBottom:
|
||||
placement.top = relativeTop;
|
||||
placement.bottom = relativeBottom;
|
||||
break;
|
||||
}
|
||||
|
||||
// The parent size, need to set our own size based on offsets
|
||||
updateSize(width: number, height: number) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.validatePlacement();
|
||||
switch (horizontal) {
|
||||
case HorizontalConstraint.Left:
|
||||
placement.left = relativeLeft;
|
||||
placement.width = width;
|
||||
break;
|
||||
case HorizontalConstraint.Right:
|
||||
placement.right = relativeRight;
|
||||
placement.width = width;
|
||||
break;
|
||||
case HorizontalConstraint.LeftRight:
|
||||
placement.left = relativeLeft;
|
||||
placement.right = relativeRight;
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the CSS position
|
||||
this.options.placement = placement;
|
||||
this.sizeStyle = {
|
||||
...this.options.placement,
|
||||
position: 'absolute',
|
||||
};
|
||||
this.revId++;
|
||||
}
|
||||
|
||||
updateData(ctx: DimensionContext) {
|
||||
@@ -239,34 +274,55 @@ export class ElementState implements LayerElement {
|
||||
};
|
||||
|
||||
applyDrag = (event: OnDrag) => {
|
||||
const { placement, anchor } = this;
|
||||
const { options } = this;
|
||||
const { placement, constraint } = options;
|
||||
const { vertical, horizontal } = constraint ?? {};
|
||||
|
||||
const deltaX = event.delta[0];
|
||||
const deltaY = event.delta[1];
|
||||
|
||||
const style = event.target.style;
|
||||
if (anchor.top) {
|
||||
placement.top! += deltaY;
|
||||
style.top = `${placement.top}px`;
|
||||
|
||||
const isConstrainedTop = vertical === VerticalConstraint.Top || vertical === VerticalConstraint.TopBottom;
|
||||
const isConstrainedBottom = vertical === VerticalConstraint.Bottom || vertical === VerticalConstraint.TopBottom;
|
||||
const isConstrainedLeft = horizontal === HorizontalConstraint.Left || horizontal === HorizontalConstraint.LeftRight;
|
||||
const isConstrainedRight =
|
||||
horizontal === HorizontalConstraint.Right || horizontal === HorizontalConstraint.LeftRight;
|
||||
|
||||
if (isConstrainedTop) {
|
||||
placement!.top! += deltaY;
|
||||
style.top = `${placement!.top}px`;
|
||||
}
|
||||
if (anchor.bottom) {
|
||||
placement.bottom! -= deltaY;
|
||||
style.bottom = `${placement.bottom}px`;
|
||||
|
||||
if (isConstrainedBottom) {
|
||||
placement!.bottom! -= deltaY;
|
||||
style.bottom = `${placement!.bottom}px`;
|
||||
}
|
||||
if (anchor.left) {
|
||||
placement.left! += deltaX;
|
||||
style.left = `${placement.left}px`;
|
||||
|
||||
if (isConstrainedLeft) {
|
||||
placement!.left! += deltaX;
|
||||
style.left = `${placement!.left}px`;
|
||||
}
|
||||
if (anchor.right) {
|
||||
placement.right! -= deltaX;
|
||||
style.right = `${placement.right}px`;
|
||||
|
||||
if (isConstrainedRight) {
|
||||
placement!.right! -= deltaX;
|
||||
style.right = `${placement!.right}px`;
|
||||
}
|
||||
|
||||
// TODO: Center + Scale
|
||||
};
|
||||
|
||||
// kinda like:
|
||||
// https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
|
||||
applyResize = (event: OnResize) => {
|
||||
const { placement, anchor } = this;
|
||||
const { options } = this;
|
||||
const { placement, constraint } = options;
|
||||
const { vertical, horizontal } = constraint ?? {};
|
||||
|
||||
const top = vertical === VerticalConstraint.Top || vertical === VerticalConstraint.TopBottom;
|
||||
const bottom = vertical === VerticalConstraint.Bottom || vertical === VerticalConstraint.TopBottom;
|
||||
const left = horizontal === HorizontalConstraint.Left || horizontal === HorizontalConstraint.LeftRight;
|
||||
const right = horizontal === HorizontalConstraint.Right || horizontal === HorizontalConstraint.LeftRight;
|
||||
|
||||
const style = event.target.style;
|
||||
const deltaX = event.delta[0];
|
||||
@@ -275,69 +331,62 @@ export class ElementState implements LayerElement {
|
||||
const dirTB = event.direction[1];
|
||||
if (dirLR === 1) {
|
||||
// RIGHT
|
||||
if (anchor.right) {
|
||||
placement.right! -= deltaX;
|
||||
style.right = `${placement.right}px`;
|
||||
if (!anchor.left) {
|
||||
placement.width = event.width;
|
||||
style.width = `${placement.width}px`;
|
||||
if (right) {
|
||||
placement!.right! -= deltaX;
|
||||
style.right = `${placement!.right}px`;
|
||||
if (!left) {
|
||||
placement!.width = event.width;
|
||||
style.width = `${placement!.width}px`;
|
||||
}
|
||||
} else {
|
||||
placement.width! = event.width;
|
||||
style.width = `${placement.width}px`;
|
||||
placement!.width! = event.width;
|
||||
style.width = `${placement!.width}px`;
|
||||
}
|
||||
} else if (dirLR === -1) {
|
||||
// LEFT
|
||||
if (anchor.left) {
|
||||
placement.left! -= deltaX;
|
||||
placement.width! = event.width;
|
||||
style.left = `${placement.left}px`;
|
||||
style.width = `${placement.width}px`;
|
||||
if (left) {
|
||||
placement!.left! -= deltaX;
|
||||
placement!.width! = event.width;
|
||||
style.left = `${placement!.left}px`;
|
||||
style.width = `${placement!.width}px`;
|
||||
} else {
|
||||
placement.width! += deltaX;
|
||||
style.width = `${placement.width}px`;
|
||||
placement!.width! += deltaX;
|
||||
style.width = `${placement!.width}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (dirTB === -1) {
|
||||
// TOP
|
||||
if (anchor.top) {
|
||||
placement.top! -= deltaY;
|
||||
placement.height = event.height;
|
||||
style.top = `${placement.top}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
if (top) {
|
||||
placement!.top! -= deltaY;
|
||||
placement!.height = event.height;
|
||||
style.top = `${placement!.top}px`;
|
||||
style.height = `${placement!.height}px`;
|
||||
} else {
|
||||
placement.height = event.height;
|
||||
style.height = `${placement.height}px`;
|
||||
placement!.height = event.height;
|
||||
style.height = `${placement!.height}px`;
|
||||
}
|
||||
} else if (dirTB === 1) {
|
||||
// BOTTOM
|
||||
if (anchor.bottom) {
|
||||
placement.bottom! -= deltaY;
|
||||
placement.height! = event.height;
|
||||
style.bottom = `${placement.bottom}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
if (bottom) {
|
||||
placement!.bottom! -= deltaY;
|
||||
placement!.height! = event.height;
|
||||
style.bottom = `${placement!.bottom}px`;
|
||||
style.height = `${placement!.height}px`;
|
||||
} else {
|
||||
placement.height! = event.height;
|
||||
style.height = `${placement.height}px`;
|
||||
placement!.height! = event.height;
|
||||
style.height = `${placement!.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
this.width = event.width;
|
||||
this.height = event.height;
|
||||
// TODO: Center + Scale
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item } = this;
|
||||
return (
|
||||
<div key={`${this.UID}`} style={{ ...this.sizeStyle, ...this.dataStyle }} ref={this.initElement}>
|
||||
<item.display
|
||||
key={`${this.UID}/${this.revId}`}
|
||||
config={this.options.config}
|
||||
width={this.width}
|
||||
height={this.height}
|
||||
data={this.data}
|
||||
/>
|
||||
<item.display key={`${this.UID}/${this.revId}`} config={this.options.config} data={this.data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Scene } from './scene';
|
||||
import { RootElement } from './root';
|
||||
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
||||
|
||||
export const groupItemDummy: CanvasElementItem = {
|
||||
id: 'group',
|
||||
@@ -53,27 +54,6 @@ export class GroupState extends ElementState {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The parent size, need to set our own size based on offsets
|
||||
updateSize(width: number, height: number) {
|
||||
super.updateSize(width, height);
|
||||
if (!this.parent) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.sizeStyle.width = width;
|
||||
this.sizeStyle.height = height;
|
||||
}
|
||||
|
||||
// Update children with calculated size
|
||||
for (const elem of this.elements) {
|
||||
elem.updateSize(this.width, this.height);
|
||||
}
|
||||
|
||||
// The group forced to full width (for now)
|
||||
this.sizeStyle.width = width;
|
||||
this.sizeStyle.height = height;
|
||||
this.sizeStyle.position = 'absolute';
|
||||
}
|
||||
|
||||
updateData(ctx: DimensionContext) {
|
||||
super.updateData(ctx);
|
||||
for (const elem of this.elements) {
|
||||
@@ -113,21 +93,50 @@ export class GroupState extends ElementState {
|
||||
return;
|
||||
}
|
||||
const opts = cloneDeep(element.options);
|
||||
if (element.anchor.top) {
|
||||
opts.placement!.top! += 10;
|
||||
|
||||
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;
|
||||
}
|
||||
if (element.anchor.left) {
|
||||
opts.placement!.left! += 10;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
if (placement.bottom == null) {
|
||||
placement.bottom = 100;
|
||||
} else {
|
||||
placement.bottom -= 10;
|
||||
}
|
||||
if (element.anchor.bottom) {
|
||||
opts.placement!.bottom! += 10;
|
||||
}
|
||||
if (element.anchor.right) {
|
||||
opts.placement!.right! += 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;
|
||||
|
||||
const copy = new ElementState(element.item, opts, this);
|
||||
copy.updateSize(element.width, element.height);
|
||||
copy.updateData(this.scene.context);
|
||||
if (updateName) {
|
||||
copy.options.name = this.scene.getNextElementName();
|
||||
|
||||
@@ -5,21 +5,17 @@ import { Scene } from './scene';
|
||||
export class RootElement extends GroupState {
|
||||
constructor(public options: CanvasGroupOptions, public scene: Scene, private changeCallback: () => void) {
|
||||
super(options, scene);
|
||||
|
||||
this.sizeStyle = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
isRoot(): this is RootElement {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The parent size is always fullsize
|
||||
updateSize(width: number, height: number) {
|
||||
super.updateSize(width, height);
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.sizeStyle.width = width;
|
||||
this.sizeStyle.height = height;
|
||||
}
|
||||
|
||||
// root type can not change
|
||||
onChange(options: CanvasElementOptions) {
|
||||
this.revId++;
|
||||
@@ -28,10 +24,10 @@ export class RootElement extends GroupState {
|
||||
}
|
||||
|
||||
getSaveModel(): CanvasGroupOptions {
|
||||
const { placement, anchor, ...rest } = this.options;
|
||||
const { placement, constraint, ...rest } = this.options;
|
||||
|
||||
return {
|
||||
...rest, // everything except placement & anchor
|
||||
...rest, // everything except placement & constraint
|
||||
elements: this.elements.map((v) => v.getSaveModel()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Selecto from 'selecto';
|
||||
import { config } from 'app/core/config';
|
||||
import { GrafanaTheme2, PanelData } from '@grafana/data';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { Anchor, CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG, Placement } from 'app/features/canvas';
|
||||
import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
ResourceDimensionConfig,
|
||||
@@ -112,7 +112,6 @@ export class Scene {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.style = { width, height };
|
||||
this.root.updateSize(width, height);
|
||||
|
||||
if (this.selecto?.getSelectedTargets().length) {
|
||||
this.clearCurrentSelection();
|
||||
@@ -157,44 +156,16 @@ export class Scene {
|
||||
this.save();
|
||||
}
|
||||
|
||||
toggleAnchor(element: ElementState, k: keyof Anchor) {
|
||||
const { div } = element;
|
||||
if (!div) {
|
||||
console.log('Not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const w = element.parent?.width ?? 100;
|
||||
const h = element.parent?.height ?? 100;
|
||||
|
||||
// Get computed position....
|
||||
const info = div.getBoundingClientRect(); // getElementInfo(div, element.parent?.div);
|
||||
console.log('DIV info', div);
|
||||
|
||||
const placement: Placement = {
|
||||
top: info.top,
|
||||
left: info.left,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
bottom: h - info.bottom,
|
||||
right: w - info.right,
|
||||
};
|
||||
|
||||
console.log('PPP', placement);
|
||||
|
||||
// // TODO: needs to recalculate placement based on absolute values...
|
||||
// element.anchor[k] = !Boolean(element.anchor[k]);
|
||||
// element.placement = placement;
|
||||
// element.validatePlacement();
|
||||
// element.revId++;
|
||||
// this.revId++;
|
||||
// this.save();
|
||||
|
||||
this.moved.next(Date.now());
|
||||
}
|
||||
|
||||
save = () => {
|
||||
save = (updateMoveable = false) => {
|
||||
this.onSave(this.root.getSaveModel());
|
||||
|
||||
if (updateMoveable) {
|
||||
setTimeout(() => {
|
||||
if (this.div) {
|
||||
this.initMoveable(true);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
private findElementByTarget = (target: HTMLElement | SVGElement): ElementState | undefined => {
|
||||
@@ -262,8 +233,8 @@ export class Scene {
|
||||
initMoveable = (destroySelecto = false, allowChanges = true) => {
|
||||
const targetElements = this.generateTargetElements(this.root.elements);
|
||||
|
||||
if (destroySelecto) {
|
||||
this.selecto?.destroy();
|
||||
if (destroySelecto && this.selecto) {
|
||||
this.selecto.destroy();
|
||||
}
|
||||
|
||||
this.selecto = new Selecto({
|
||||
@@ -295,9 +266,8 @@ export class Scene {
|
||||
.on('dragEnd', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
|
||||
if (targetedElement && targetedElement.parent) {
|
||||
const parent = targetedElement.parent;
|
||||
targetedElement.updateSize(parent.width, parent.height);
|
||||
if (targetedElement) {
|
||||
targetedElement?.setPlacementFromConstraint();
|
||||
}
|
||||
})
|
||||
.on('resize', (event) => {
|
||||
@@ -311,6 +281,13 @@ export class Scene {
|
||||
targetedElement!.applyResize(event);
|
||||
});
|
||||
this.moved.next(Date.now()); // TODO only on end
|
||||
})
|
||||
.on('resizeEnd', (event) => {
|
||||
const targetedElement = this.findElementByTarget(event.target);
|
||||
|
||||
if (targetedElement) {
|
||||
targetedElement?.setPlacementFromConstraint();
|
||||
}
|
||||
});
|
||||
|
||||
let targets: Array<HTMLElement | SVGElement> = [];
|
||||
|
||||
@@ -10,11 +10,25 @@ export interface Placement {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface Anchor {
|
||||
top?: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
bottom?: boolean;
|
||||
export interface Constraint {
|
||||
horizontal?: HorizontalConstraint;
|
||||
vertical?: VerticalConstraint;
|
||||
}
|
||||
|
||||
export enum HorizontalConstraint {
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
LeftRight = 'leftright',
|
||||
Center = 'center',
|
||||
Scale = 'scale',
|
||||
}
|
||||
|
||||
export enum VerticalConstraint {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
TopBottom = 'topbottom',
|
||||
Center = 'center',
|
||||
Scale = 'scale',
|
||||
}
|
||||
|
||||
export enum BackgroundImageSize {
|
||||
|
||||
@@ -38,7 +38,6 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
||||
const newElementOptions = item.getNewOptions() as CanvasElementOptions;
|
||||
newElementOptions.type = item.id;
|
||||
const newElement = new ElementState(item, newElementOptions, layer);
|
||||
newElement.updateSize(newElement.width, newElement.height);
|
||||
newElement.updateData(layer.scene.context);
|
||||
layer.elements.push(newElement);
|
||||
layer.scene.save();
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Button, Field, HorizontalGroup, InlineField, InlineFieldRow } from '@grafana/ui';
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
|
||||
import { PanelOptions } from '../models.gen';
|
||||
import { useObservable } from 'react-use';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Field, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
|
||||
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
|
||||
import { PanelOptions } from '../models.gen';
|
||||
import { CanvasEditorOptions } from './elementEditor';
|
||||
import { Anchor, Placement } from 'app/features/canvas';
|
||||
import { HorizontalConstraint, Placement, VerticalConstraint } from 'app/features/canvas';
|
||||
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
||||
|
||||
const anchors: Array<keyof Anchor> = ['top', 'left', 'bottom', 'right'];
|
||||
const places: Array<keyof Placement> = ['top', 'left', 'bottom', 'right', 'width', 'height'];
|
||||
|
||||
export const PlacementEditor: FC<StandardEditorProps<any, CanvasEditorOptions, PanelOptions>> = ({ item }) => {
|
||||
const horizontalOptions: Array<SelectableValue<HorizontalConstraint>> = [
|
||||
{ label: 'Left', value: HorizontalConstraint.Left },
|
||||
{ label: 'Right', value: HorizontalConstraint.Right },
|
||||
{ label: 'Left and right', value: HorizontalConstraint.LeftRight },
|
||||
];
|
||||
|
||||
const verticalOptions: Array<SelectableValue<VerticalConstraint>> = [
|
||||
{ label: 'Top', value: VerticalConstraint.Top },
|
||||
{ label: 'Bottom', value: VerticalConstraint.Bottom },
|
||||
{ label: 'Top and bottom', value: VerticalConstraint.TopBottom },
|
||||
];
|
||||
|
||||
export const PlacementEditor: FC<StandardEditorProps<any, CanvasEditorOptions, PanelOptions>> = ({
|
||||
item,
|
||||
onChange,
|
||||
}) => {
|
||||
const settings = item.settings;
|
||||
|
||||
// Will force a rerender whenever the subject changes
|
||||
@@ -26,28 +40,39 @@ export const PlacementEditor: FC<StandardEditorProps<any, CanvasEditorOptions, P
|
||||
if (!element) {
|
||||
return <div>???</div>;
|
||||
}
|
||||
const { placement } = element;
|
||||
const { options } = element;
|
||||
const { placement, constraint: layout } = options;
|
||||
|
||||
const onHorizontalConstraintChange = (h: SelectableValue<HorizontalConstraint>) => {
|
||||
element.options.constraint!.horizontal = h.value;
|
||||
element.setPlacementFromConstraint();
|
||||
settings.scene.revId++;
|
||||
settings.scene.save(true);
|
||||
};
|
||||
|
||||
const onVerticalConstraintChange = (v: SelectableValue<VerticalConstraint>) => {
|
||||
element.options.constraint!.vertical = v.value;
|
||||
element.setPlacementFromConstraint();
|
||||
settings.scene.revId++;
|
||||
settings.scene.save(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HorizontalGroup>
|
||||
{anchors.map((a) => (
|
||||
<Button
|
||||
key={a}
|
||||
size="sm"
|
||||
variant={element.anchor[a] ? 'primary' : 'secondary'}
|
||||
onClick={() => settings.scene.toggleAnchor(element, a)}
|
||||
>
|
||||
{a}
|
||||
</Button>
|
||||
))}
|
||||
</HorizontalGroup>
|
||||
<VerticalGroup>
|
||||
<Select options={verticalOptions} onChange={onVerticalConstraintChange} value={layout?.vertical} />
|
||||
<Select
|
||||
options={horizontalOptions}
|
||||
onChange={onHorizontalConstraintChange}
|
||||
value={options.constraint?.horizontal}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
<br />
|
||||
|
||||
<Field label="Position">
|
||||
<>
|
||||
{places.map((p) => {
|
||||
const v = placement[p];
|
||||
const v = placement![p];
|
||||
if (v == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
||||
category: ['Layout'],
|
||||
id: 'content',
|
||||
path: '__', // not used
|
||||
name: 'Anchor',
|
||||
name: 'Constraints',
|
||||
editor: PlacementEditor,
|
||||
settings: opts,
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
|
||||
category: ['Layout'],
|
||||
id: 'content',
|
||||
path: '__', // not used
|
||||
name: 'Anchor',
|
||||
name: 'Constraints',
|
||||
editor: PlacementEditor,
|
||||
settings: {
|
||||
scene: opts.scene,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ScaleDimensionConfig,
|
||||
TextDimensionConfig,
|
||||
} from 'app/features/dimensions';
|
||||
import { HorizontalConstraint, VerticalConstraint } from 'app/features/canvas';
|
||||
|
||||
interface Props extends PanelProps<PanelOptions> {}
|
||||
|
||||
@@ -36,17 +37,16 @@ export class IconPanel extends Component<Props> {
|
||||
|
||||
updateSize = (props: Props) => {
|
||||
const { width, height } = props;
|
||||
this.element.anchor = {
|
||||
top: true,
|
||||
left: true,
|
||||
this.element.options.constraint = {
|
||||
vertical: VerticalConstraint.Top,
|
||||
horizontal: HorizontalConstraint.Left,
|
||||
};
|
||||
this.element.placement = {
|
||||
this.element.options.placement = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
this.element.updateSize(width, height);
|
||||
};
|
||||
|
||||
dims: DimensionContext = {
|
||||
|
||||
Reference in New Issue
Block a user