2021-09-02 12:01:08 -05:00
|
|
|
import React, { CSSProperties } from 'react';
|
2021-10-12 12:52:15 -05:00
|
|
|
import { OnDrag, OnResize } from 'react-moveable/declaration/types';
|
|
|
|
|
2021-09-02 12:01:08 -05:00
|
|
|
import {
|
|
|
|
BackgroundImageSize,
|
|
|
|
CanvasElementItem,
|
|
|
|
CanvasElementOptions,
|
|
|
|
canvasElementRegistry,
|
|
|
|
} from 'app/features/canvas';
|
|
|
|
import { DimensionContext } from 'app/features/dimensions';
|
|
|
|
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
|
|
|
import { GroupState } from './group';
|
2021-12-02 17:54:45 -06:00
|
|
|
import { LayerElement } from 'app/core/components/Layers/types';
|
2021-12-06 23:04:58 -06:00
|
|
|
import { Scene } from './scene';
|
2022-04-20 11:59:49 -05:00
|
|
|
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
2021-09-02 12:01:08 -05:00
|
|
|
|
2021-10-13 15:12:16 -05:00
|
|
|
let counter = 0;
|
2021-09-02 12:01:08 -05:00
|
|
|
|
2021-12-02 17:54:45 -06:00
|
|
|
export class ElementState implements LayerElement {
|
2021-12-06 23:04:58 -06:00
|
|
|
// UID necessary for moveable to work (for now)
|
2021-09-02 12:01:08 -05:00
|
|
|
readonly UID = counter++;
|
|
|
|
revId = 0;
|
2021-10-12 12:52:15 -05:00
|
|
|
sizeStyle: CSSProperties = {};
|
|
|
|
dataStyle: CSSProperties = {};
|
|
|
|
|
|
|
|
// Filled in by ref
|
|
|
|
div?: HTMLDivElement;
|
2021-09-02 12:01:08 -05:00
|
|
|
|
|
|
|
// Calculated
|
|
|
|
data?: any; // depends on the type
|
|
|
|
|
|
|
|
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
2021-12-06 23:04:58 -06:00
|
|
|
const fallbackName = `Element ${Date.now()}`;
|
2021-09-02 12:01:08 -05:00
|
|
|
if (!options) {
|
2021-12-06 23:04:58 -06:00
|
|
|
this.options = { type: item.id, name: fallbackName };
|
2021-09-02 12:01:08 -05:00
|
|
|
}
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
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',
|
|
|
|
};
|
2021-12-02 17:54:45 -06:00
|
|
|
|
2021-12-06 23:04:58 -06:00
|
|
|
const scene = this.getScene();
|
2021-12-02 17:54:45 -06:00
|
|
|
if (!options.name) {
|
2021-12-06 23:04:58 -06:00
|
|
|
const newName = scene?.getNextElementName();
|
|
|
|
options.name = newName ?? fallbackName;
|
2021-12-02 17:54:45 -06:00
|
|
|
}
|
2021-12-06 23:04:58 -06:00
|
|
|
scene?.byName.set(options.name, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
private getScene(): Scene | undefined {
|
|
|
|
let trav = this.parent;
|
|
|
|
while (trav) {
|
|
|
|
if (trav.isRoot()) {
|
|
|
|
return trav.scene;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
trav = trav.parent;
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
2021-12-02 17:54:45 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
getName() {
|
|
|
|
return this.options.name;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
validatePlacement() {
|
2022-04-20 11:59:49 -05:00
|
|
|
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;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
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;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
this.options.placement = updatedPlacement;
|
|
|
|
}
|
2021-10-13 15:12:16 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
setPlacementFromConstraint() {
|
|
|
|
const { constraint } = this.options;
|
|
|
|
const { vertical, horizontal } = constraint ?? {};
|
2021-10-13 15:12:16 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
const elementContainer = this.div && this.div.getBoundingClientRect();
|
|
|
|
const parentContainer = this.div && this.div.parentElement?.getBoundingClientRect();
|
2021-10-13 15:12:16 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
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;
|
2021-10-13 15:12:16 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
const placement = {} as Placement;
|
2021-09-02 12:01:08 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2021-09-02 12:01:08 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
this.options.placement = placement;
|
2021-10-12 12:52:15 -05:00
|
|
|
this.sizeStyle = {
|
|
|
|
...this.options.placement,
|
|
|
|
position: 'absolute',
|
2021-09-02 12:01:08 -05:00
|
|
|
};
|
2022-04-20 11:59:49 -05:00
|
|
|
this.revId++;
|
2021-09-02 12:01:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
updateData(ctx: DimensionContext) {
|
|
|
|
if (this.item.prepareData) {
|
|
|
|
this.data = this.item.prepareData(ctx, this.options.config);
|
|
|
|
this.revId++; // rerender
|
|
|
|
}
|
|
|
|
|
|
|
|
const { background, border } = this.options;
|
|
|
|
const css: CSSProperties = {};
|
|
|
|
if (background) {
|
|
|
|
if (background.color) {
|
|
|
|
const color = ctx.getColor(background.color);
|
|
|
|
css.backgroundColor = color.value();
|
|
|
|
}
|
|
|
|
if (background.image) {
|
|
|
|
const image = ctx.getResource(background.image);
|
|
|
|
if (image) {
|
|
|
|
const v = image.value();
|
|
|
|
if (v) {
|
|
|
|
css.backgroundImage = `url("${v}")`;
|
|
|
|
switch (background.size ?? BackgroundImageSize.Contain) {
|
|
|
|
case BackgroundImageSize.Contain:
|
|
|
|
css.backgroundSize = 'contain';
|
|
|
|
css.backgroundRepeat = 'no-repeat';
|
|
|
|
break;
|
|
|
|
case BackgroundImageSize.Cover:
|
|
|
|
css.backgroundSize = 'cover';
|
|
|
|
css.backgroundRepeat = 'no-repeat';
|
|
|
|
break;
|
|
|
|
case BackgroundImageSize.Original:
|
|
|
|
css.backgroundRepeat = 'no-repeat';
|
|
|
|
break;
|
|
|
|
case BackgroundImageSize.Tile:
|
|
|
|
css.backgroundRepeat = 'repeat';
|
|
|
|
break;
|
|
|
|
case BackgroundImageSize.Fill:
|
|
|
|
css.backgroundSize = '100% 100%';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (border && border.color && border.width) {
|
|
|
|
const color = ctx.getColor(border.color);
|
|
|
|
css.borderWidth = border.width;
|
|
|
|
css.borderStyle = 'solid';
|
|
|
|
css.borderColor = color.value();
|
|
|
|
|
|
|
|
// Move the image to inside the border
|
|
|
|
if (css.backgroundImage) {
|
|
|
|
css.backgroundOrigin = 'padding-box';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-12 12:52:15 -05:00
|
|
|
this.dataStyle = css;
|
2021-09-02 12:01:08 -05:00
|
|
|
}
|
|
|
|
|
2021-10-12 12:52:15 -05:00
|
|
|
/** Recursively visit all nodes */
|
2021-09-02 12:01:08 -05:00
|
|
|
visit(visitor: (v: ElementState) => void) {
|
|
|
|
visitor(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
onChange(options: CanvasElementOptions) {
|
|
|
|
if (this.item.id !== options.type) {
|
|
|
|
this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem;
|
|
|
|
}
|
|
|
|
|
2021-12-06 23:04:58 -06:00
|
|
|
// rename handling
|
|
|
|
const oldName = this.options.name;
|
|
|
|
const newName = options.name;
|
|
|
|
|
2021-09-02 12:01:08 -05:00
|
|
|
this.revId++;
|
|
|
|
this.options = { ...options };
|
|
|
|
let trav = this.parent;
|
|
|
|
while (trav) {
|
2021-10-28 11:58:31 -05:00
|
|
|
if (trav.isRoot()) {
|
|
|
|
trav.scene.save();
|
|
|
|
break;
|
|
|
|
}
|
2021-09-02 12:01:08 -05:00
|
|
|
trav.revId++;
|
|
|
|
trav = trav.parent;
|
|
|
|
}
|
2021-12-06 23:04:58 -06:00
|
|
|
|
|
|
|
const scene = this.getScene();
|
|
|
|
if (oldName !== newName && scene) {
|
|
|
|
scene.byName.delete(oldName);
|
|
|
|
scene.byName.set(newName, this);
|
|
|
|
}
|
2021-09-02 12:01:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getSaveModel() {
|
|
|
|
return { ...this.options };
|
|
|
|
}
|
|
|
|
|
2021-10-12 12:52:15 -05:00
|
|
|
initElement = (target: HTMLDivElement) => {
|
|
|
|
this.div = target;
|
|
|
|
};
|
|
|
|
|
|
|
|
applyDrag = (event: OnDrag) => {
|
2022-04-20 11:59:49 -05:00
|
|
|
const { options } = this;
|
|
|
|
const { placement, constraint } = options;
|
|
|
|
const { vertical, horizontal } = constraint ?? {};
|
2021-10-12 12:52:15 -05:00
|
|
|
|
2021-10-13 15:12:16 -05:00
|
|
|
const deltaX = event.delta[0];
|
|
|
|
const deltaY = event.delta[1];
|
|
|
|
|
|
|
|
const style = event.target.style;
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
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`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
if (isConstrainedBottom) {
|
|
|
|
placement!.bottom! -= deltaY;
|
|
|
|
style.bottom = `${placement!.bottom}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
if (isConstrainedLeft) {
|
|
|
|
placement!.left! += deltaX;
|
|
|
|
style.left = `${placement!.left}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
if (isConstrainedRight) {
|
|
|
|
placement!.right! -= deltaX;
|
|
|
|
style.right = `${placement!.right}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
2022-04-20 11:59:49 -05:00
|
|
|
|
|
|
|
// TODO: Center + Scale
|
2021-10-12 12:52:15 -05:00
|
|
|
};
|
|
|
|
|
2021-10-13 15:12:16 -05:00
|
|
|
// kinda like:
|
|
|
|
// https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
|
2021-10-12 12:52:15 -05:00
|
|
|
applyResize = (event: OnResize) => {
|
2022-04-20 11:59:49 -05:00
|
|
|
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;
|
2021-10-13 15:12:16 -05:00
|
|
|
|
|
|
|
const style = event.target.style;
|
|
|
|
const deltaX = event.delta[0];
|
|
|
|
const deltaY = event.delta[1];
|
|
|
|
const dirLR = event.direction[0];
|
|
|
|
const dirTB = event.direction[1];
|
|
|
|
if (dirLR === 1) {
|
|
|
|
// RIGHT
|
2022-04-20 11:59:49 -05:00
|
|
|
if (right) {
|
|
|
|
placement!.right! -= deltaX;
|
|
|
|
style.right = `${placement!.right}px`;
|
|
|
|
if (!left) {
|
|
|
|
placement!.width = event.width;
|
|
|
|
style.width = `${placement!.width}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
} else {
|
2022-04-20 11:59:49 -05:00
|
|
|
placement!.width! = event.width;
|
|
|
|
style.width = `${placement!.width}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
} else if (dirLR === -1) {
|
|
|
|
// LEFT
|
2022-04-20 11:59:49 -05:00
|
|
|
if (left) {
|
|
|
|
placement!.left! -= deltaX;
|
|
|
|
placement!.width! = event.width;
|
|
|
|
style.left = `${placement!.left}px`;
|
|
|
|
style.width = `${placement!.width}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
} else {
|
2022-04-20 11:59:49 -05:00
|
|
|
placement!.width! += deltaX;
|
|
|
|
style.width = `${placement!.width}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dirTB === -1) {
|
|
|
|
// TOP
|
2022-04-20 11:59:49 -05:00
|
|
|
if (top) {
|
|
|
|
placement!.top! -= deltaY;
|
|
|
|
placement!.height = event.height;
|
|
|
|
style.top = `${placement!.top}px`;
|
|
|
|
style.height = `${placement!.height}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
} else {
|
2022-04-20 11:59:49 -05:00
|
|
|
placement!.height = event.height;
|
|
|
|
style.height = `${placement!.height}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
} else if (dirTB === 1) {
|
|
|
|
// BOTTOM
|
2022-04-20 11:59:49 -05:00
|
|
|
if (bottom) {
|
|
|
|
placement!.bottom! -= deltaY;
|
|
|
|
placement!.height! = event.height;
|
|
|
|
style.bottom = `${placement!.bottom}px`;
|
|
|
|
style.height = `${placement!.height}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
} else {
|
2022-04-20 11:59:49 -05:00
|
|
|
placement!.height! = event.height;
|
|
|
|
style.height = `${placement!.height}px`;
|
2021-10-13 15:12:16 -05:00
|
|
|
}
|
|
|
|
}
|
2021-10-12 12:52:15 -05:00
|
|
|
|
2022-04-20 11:59:49 -05:00
|
|
|
// TODO: Center + Scale
|
2021-10-12 12:52:15 -05:00
|
|
|
};
|
|
|
|
|
2021-09-02 12:01:08 -05:00
|
|
|
render() {
|
|
|
|
const { item } = this;
|
|
|
|
return (
|
2021-10-28 11:58:31 -05:00
|
|
|
<div key={`${this.UID}`} style={{ ...this.sizeStyle, ...this.dataStyle }} ref={this.initElement}>
|
2022-04-20 11:59:49 -05:00
|
|
|
<item.display key={`${this.UID}/${this.revId}`} config={this.options.config} data={this.data} />
|
2021-09-02 12:01:08 -05:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|