123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- import React, { CSSProperties } from 'react';
- import { OnDrag, OnResize } from 'react-moveable/declaration/types';
- import { LayerElement } from 'app/core/components/Layers/types';
- import {
- BackgroundImageSize,
- CanvasElementItem,
- CanvasElementOptions,
- canvasElementRegistry,
- } from 'app/features/canvas';
- import { notFoundItem } from 'app/features/canvas/elements/notFound';
- import { DimensionContext } from 'app/features/dimensions';
- import { Constraint, HorizontalConstraint, Placement, VerticalConstraint } from '../types';
- import { FrameState } from './frame';
- import { RootElement } from './root';
- import { Scene } from './scene';
- let counter = 0;
- export class ElementState implements LayerElement {
- // UID necessary for moveable to work (for now)
- readonly UID = counter++;
- revId = 0;
- sizeStyle: CSSProperties = {};
- dataStyle: CSSProperties = {};
- // Determine whether or not element is in motion or not (via moveable)
- isMoving = false;
- // Temp stored constraint for visualization purposes (switch to top / left constraint to simplify some functionality)
- tempConstraint: Constraint | undefined;
- // Filled in by ref
- div?: HTMLDivElement;
- // Calculated
- data?: any; // depends on the type
- constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: FrameState) {
- const fallbackName = `Element ${Date.now()}`;
- if (!options) {
- this.options = { type: item.id, name: fallbackName };
- }
- options.constraint = options.constraint ?? {
- vertical: VerticalConstraint.Top,
- horizontal: HorizontalConstraint.Left,
- };
- options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0 };
- const scene = this.getScene();
- if (!options.name) {
- const newName = scene?.getNextElementName();
- options.name = newName ?? fallbackName;
- }
- scene?.byName.set(options.name, this);
- }
- private getScene(): Scene | undefined {
- let trav = this.parent;
- while (trav) {
- if (trav.isRoot()) {
- return trav.scene;
- }
- trav = trav.parent;
- }
- return undefined;
- }
- getName() {
- return this.options.name;
- }
- /** 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);
- const style: React.CSSProperties = {
- position: 'absolute',
- // Minimum element size is 10x10
- minWidth: '10px',
- minHeight: '10px',
- };
- const translate = ['0px', '0px'];
- switch (vertical) {
- case VerticalConstraint.Top:
- placement.top = placement.top ?? 0;
- placement.height = placement.height ?? 100;
- style.top = `${placement.top}px`;
- style.height = `${placement.height}px`;
- delete placement.bottom;
- break;
- case VerticalConstraint.Bottom:
- placement.bottom = placement.bottom ?? 0;
- placement.height = placement.height ?? 100;
- style.bottom = `${placement.bottom}px`;
- style.height = `${placement.height}px`;
- delete placement.top;
- break;
- case VerticalConstraint.TopBottom:
- placement.top = placement.top ?? 0;
- placement.bottom = placement.bottom ?? 0;
- style.top = `${placement.top}px`;
- style.bottom = `${placement.bottom}px`;
- delete placement.height;
- style.height = '';
- break;
- case VerticalConstraint.Center:
- placement.top = placement.top ?? 0;
- placement.height = placement.height ?? 100;
- translate[1] = '-50%';
- style.top = `calc(50% - ${placement.top}px)`;
- style.height = `${placement.height}px`;
- delete placement.bottom;
- break;
- case VerticalConstraint.Scale:
- placement.top = placement.top ?? 0;
- placement.bottom = placement.bottom ?? 0;
- style.top = `${placement.top}%`;
- style.bottom = `${placement.bottom}%`;
- delete placement.height;
- style.height = '';
- break;
- }
- switch (horizontal) {
- case HorizontalConstraint.Left:
- placement.left = placement.left ?? 0;
- placement.width = placement.width ?? 100;
- style.left = `${placement.left}px`;
- style.width = `${placement.width}px`;
- delete placement.right;
- break;
- case HorizontalConstraint.Right:
- placement.right = placement.right ?? 0;
- placement.width = placement.width ?? 100;
- style.right = `${placement.right}px`;
- style.width = `${placement.width}px`;
- delete placement.left;
- break;
- case HorizontalConstraint.LeftRight:
- placement.left = placement.left ?? 0;
- placement.right = placement.right ?? 0;
- style.left = `${placement.left}px`;
- style.right = `${placement.right}px`;
- delete placement.width;
- style.width = '';
- break;
- case HorizontalConstraint.Center:
- placement.left = placement.left ?? 0;
- placement.width = placement.width ?? 100;
- translate[0] = '-50%';
- style.left = `calc(50% - ${placement.left}px)`;
- style.width = `${placement.width}px`;
- delete placement.right;
- break;
- case HorizontalConstraint.Scale:
- placement.left = placement.left ?? 0;
- placement.right = placement.right ?? 0;
- style.left = `${placement.left}%`;
- style.right = `${placement.right}%`;
- delete placement.width;
- style.width = '';
- break;
- }
- style.transform = `translate(${translate[0]}, ${translate[1]})`;
- this.options.placement = placement;
- this.sizeStyle = style;
- if (this.div) {
- for (const key in this.sizeStyle) {
- this.div.style[key as any] = (this.sizeStyle as any)[key];
- }
- for (const key in this.dataStyle) {
- this.div.style[key as any] = (this.dataStyle as any)[key];
- }
- }
- }
- setPlacementFromConstraint(elementContainer?: DOMRect, parentContainer?: DOMRect) {
- const { constraint } = this.options;
- const { vertical, horizontal } = constraint ?? {};
- if (!elementContainer) {
- elementContainer = this.div && this.div.getBoundingClientRect();
- }
- if (!parentContainer) {
- parentContainer = this.div && this.div.parentElement?.getBoundingClientRect();
- }
- const relativeTop =
- elementContainer && parentContainer ? Math.round(elementContainer.top - parentContainer.top) : 0;
- const relativeBottom =
- elementContainer && parentContainer ? Math.round(parentContainer.bottom - elementContainer.bottom) : 0;
- const relativeLeft =
- elementContainer && parentContainer ? Math.round(elementContainer.left - parentContainer.left) : 0;
- const relativeRight =
- elementContainer && parentContainer ? Math.round(parentContainer.right - elementContainer.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;
- case VerticalConstraint.Center:
- const elementCenter = elementContainer ? relativeTop + height / 2 : 0;
- const parentCenter = parentContainer ? parentContainer.height / 2 : 0;
- const distanceFromCenter = parentCenter - elementCenter;
- placement.top = distanceFromCenter;
- placement.height = height;
- break;
- case VerticalConstraint.Scale:
- placement.top = (relativeTop / (parentContainer?.height ?? height)) * 100;
- placement.bottom = (relativeBottom / (parentContainer?.height ?? height)) * 100;
- 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;
- case HorizontalConstraint.Center:
- const elementCenter = elementContainer ? relativeLeft + width / 2 : 0;
- const parentCenter = parentContainer ? parentContainer.width / 2 : 0;
- const distanceFromCenter = parentCenter - elementCenter;
- placement.left = distanceFromCenter;
- placement.width = width;
- break;
- case HorizontalConstraint.Scale:
- placement.left = (relativeLeft / (parentContainer?.width ?? width)) * 100;
- placement.right = (relativeRight / (parentContainer?.width ?? width)) * 100;
- break;
- }
- this.options.placement = placement;
- this.applyLayoutStylesToDiv();
- this.revId++;
- }
- 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';
- }
- }
- this.dataStyle = css;
- this.applyLayoutStylesToDiv();
- }
- isRoot(): this is RootElement {
- return false;
- }
- /** Recursively visit all nodes */
- visit(visitor: (v: ElementState) => void) {
- visitor(this);
- }
- onChange(options: CanvasElementOptions) {
- if (this.item.id !== options.type) {
- this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem;
- }
- // rename handling
- const oldName = this.options.name;
- const newName = options.name;
- this.revId++;
- this.options = { ...options };
- let trav = this.parent;
- while (trav) {
- if (trav.isRoot()) {
- trav.scene.save();
- break;
- }
- trav.revId++;
- trav = trav.parent;
- }
- const scene = this.getScene();
- if (oldName !== newName && scene) {
- scene.byName.delete(oldName);
- scene.byName.set(newName, this);
- }
- }
- getSaveModel() {
- return { ...this.options };
- }
- initElement = (target: HTMLDivElement) => {
- this.div = target;
- this.applyLayoutStylesToDiv();
- };
- applyDrag = (event: OnDrag) => {
- event.target.style.transform = event.transform;
- };
- // kinda like:
- // https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
- applyResize = (event: OnResize) => {
- const placement = this.options.placement!;
- 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) {
- placement.width = event.width;
- style.width = `${placement.width}px`;
- } else if (dirLR === -1) {
- placement.left! -= deltaX;
- placement.width = event.width;
- style.left = `${placement.left}px`;
- style.width = `${placement.width}px`;
- }
- if (dirTB === -1) {
- placement.top! -= deltaY;
- placement.height = event.height;
- style.top = `${placement.top}px`;
- style.height = `${placement.height}px`;
- } else if (dirTB === 1) {
- placement.height = event.height;
- style.height = `${placement.height}px`;
- }
- };
- render() {
- const { item } = this;
- return (
- <div key={this.UID} ref={this.initElement}>
- <item.display key={`${this.UID}/${this.revId}`} config={this.options.config} data={this.data} />
- </div>
- );
- }
- }
|