element.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import React, { CSSProperties } from 'react';
  2. import { OnDrag, OnResize } from 'react-moveable/declaration/types';
  3. import { LayerElement } from 'app/core/components/Layers/types';
  4. import {
  5. BackgroundImageSize,
  6. CanvasElementItem,
  7. CanvasElementOptions,
  8. canvasElementRegistry,
  9. } from 'app/features/canvas';
  10. import { notFoundItem } from 'app/features/canvas/elements/notFound';
  11. import { DimensionContext } from 'app/features/dimensions';
  12. import { Constraint, HorizontalConstraint, Placement, VerticalConstraint } from '../types';
  13. import { FrameState } from './frame';
  14. import { RootElement } from './root';
  15. import { Scene } from './scene';
  16. let counter = 0;
  17. export class ElementState implements LayerElement {
  18. // UID necessary for moveable to work (for now)
  19. readonly UID = counter++;
  20. revId = 0;
  21. sizeStyle: CSSProperties = {};
  22. dataStyle: CSSProperties = {};
  23. // Determine whether or not element is in motion or not (via moveable)
  24. isMoving = false;
  25. // Temp stored constraint for visualization purposes (switch to top / left constraint to simplify some functionality)
  26. tempConstraint: Constraint | undefined;
  27. // Filled in by ref
  28. div?: HTMLDivElement;
  29. // Calculated
  30. data?: any; // depends on the type
  31. constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: FrameState) {
  32. const fallbackName = `Element ${Date.now()}`;
  33. if (!options) {
  34. this.options = { type: item.id, name: fallbackName };
  35. }
  36. options.constraint = options.constraint ?? {
  37. vertical: VerticalConstraint.Top,
  38. horizontal: HorizontalConstraint.Left,
  39. };
  40. options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0 };
  41. const scene = this.getScene();
  42. if (!options.name) {
  43. const newName = scene?.getNextElementName();
  44. options.name = newName ?? fallbackName;
  45. }
  46. scene?.byName.set(options.name, this);
  47. }
  48. private getScene(): Scene | undefined {
  49. let trav = this.parent;
  50. while (trav) {
  51. if (trav.isRoot()) {
  52. return trav.scene;
  53. }
  54. trav = trav.parent;
  55. }
  56. return undefined;
  57. }
  58. getName() {
  59. return this.options.name;
  60. }
  61. /** Use the configured options to update CSS style properties directly on the wrapper div **/
  62. applyLayoutStylesToDiv() {
  63. if (this.isRoot()) {
  64. // Root supersedes layout engine and is always 100% width + height of panel
  65. return;
  66. }
  67. const { constraint } = this.options;
  68. const { vertical, horizontal } = constraint ?? {};
  69. const placement = this.options.placement ?? ({} as Placement);
  70. const style: React.CSSProperties = {
  71. position: 'absolute',
  72. // Minimum element size is 10x10
  73. minWidth: '10px',
  74. minHeight: '10px',
  75. };
  76. const translate = ['0px', '0px'];
  77. switch (vertical) {
  78. case VerticalConstraint.Top:
  79. placement.top = placement.top ?? 0;
  80. placement.height = placement.height ?? 100;
  81. style.top = `${placement.top}px`;
  82. style.height = `${placement.height}px`;
  83. delete placement.bottom;
  84. break;
  85. case VerticalConstraint.Bottom:
  86. placement.bottom = placement.bottom ?? 0;
  87. placement.height = placement.height ?? 100;
  88. style.bottom = `${placement.bottom}px`;
  89. style.height = `${placement.height}px`;
  90. delete placement.top;
  91. break;
  92. case VerticalConstraint.TopBottom:
  93. placement.top = placement.top ?? 0;
  94. placement.bottom = placement.bottom ?? 0;
  95. style.top = `${placement.top}px`;
  96. style.bottom = `${placement.bottom}px`;
  97. delete placement.height;
  98. style.height = '';
  99. break;
  100. case VerticalConstraint.Center:
  101. placement.top = placement.top ?? 0;
  102. placement.height = placement.height ?? 100;
  103. translate[1] = '-50%';
  104. style.top = `calc(50% - ${placement.top}px)`;
  105. style.height = `${placement.height}px`;
  106. delete placement.bottom;
  107. break;
  108. case VerticalConstraint.Scale:
  109. placement.top = placement.top ?? 0;
  110. placement.bottom = placement.bottom ?? 0;
  111. style.top = `${placement.top}%`;
  112. style.bottom = `${placement.bottom}%`;
  113. delete placement.height;
  114. style.height = '';
  115. break;
  116. }
  117. switch (horizontal) {
  118. case HorizontalConstraint.Left:
  119. placement.left = placement.left ?? 0;
  120. placement.width = placement.width ?? 100;
  121. style.left = `${placement.left}px`;
  122. style.width = `${placement.width}px`;
  123. delete placement.right;
  124. break;
  125. case HorizontalConstraint.Right:
  126. placement.right = placement.right ?? 0;
  127. placement.width = placement.width ?? 100;
  128. style.right = `${placement.right}px`;
  129. style.width = `${placement.width}px`;
  130. delete placement.left;
  131. break;
  132. case HorizontalConstraint.LeftRight:
  133. placement.left = placement.left ?? 0;
  134. placement.right = placement.right ?? 0;
  135. style.left = `${placement.left}px`;
  136. style.right = `${placement.right}px`;
  137. delete placement.width;
  138. style.width = '';
  139. break;
  140. case HorizontalConstraint.Center:
  141. placement.left = placement.left ?? 0;
  142. placement.width = placement.width ?? 100;
  143. translate[0] = '-50%';
  144. style.left = `calc(50% - ${placement.left}px)`;
  145. style.width = `${placement.width}px`;
  146. delete placement.right;
  147. break;
  148. case HorizontalConstraint.Scale:
  149. placement.left = placement.left ?? 0;
  150. placement.right = placement.right ?? 0;
  151. style.left = `${placement.left}%`;
  152. style.right = `${placement.right}%`;
  153. delete placement.width;
  154. style.width = '';
  155. break;
  156. }
  157. style.transform = `translate(${translate[0]}, ${translate[1]})`;
  158. this.options.placement = placement;
  159. this.sizeStyle = style;
  160. if (this.div) {
  161. for (const key in this.sizeStyle) {
  162. this.div.style[key as any] = (this.sizeStyle as any)[key];
  163. }
  164. for (const key in this.dataStyle) {
  165. this.div.style[key as any] = (this.dataStyle as any)[key];
  166. }
  167. }
  168. }
  169. setPlacementFromConstraint(elementContainer?: DOMRect, parentContainer?: DOMRect) {
  170. const { constraint } = this.options;
  171. const { vertical, horizontal } = constraint ?? {};
  172. if (!elementContainer) {
  173. elementContainer = this.div && this.div.getBoundingClientRect();
  174. }
  175. if (!parentContainer) {
  176. parentContainer = this.div && this.div.parentElement?.getBoundingClientRect();
  177. }
  178. const relativeTop =
  179. elementContainer && parentContainer ? Math.round(elementContainer.top - parentContainer.top) : 0;
  180. const relativeBottom =
  181. elementContainer && parentContainer ? Math.round(parentContainer.bottom - elementContainer.bottom) : 0;
  182. const relativeLeft =
  183. elementContainer && parentContainer ? Math.round(elementContainer.left - parentContainer.left) : 0;
  184. const relativeRight =
  185. elementContainer && parentContainer ? Math.round(parentContainer.right - elementContainer.right) : 0;
  186. const placement = {} as Placement;
  187. const width = elementContainer?.width ?? 100;
  188. const height = elementContainer?.height ?? 100;
  189. switch (vertical) {
  190. case VerticalConstraint.Top:
  191. placement.top = relativeTop;
  192. placement.height = height;
  193. break;
  194. case VerticalConstraint.Bottom:
  195. placement.bottom = relativeBottom;
  196. placement.height = height;
  197. break;
  198. case VerticalConstraint.TopBottom:
  199. placement.top = relativeTop;
  200. placement.bottom = relativeBottom;
  201. break;
  202. case VerticalConstraint.Center:
  203. const elementCenter = elementContainer ? relativeTop + height / 2 : 0;
  204. const parentCenter = parentContainer ? parentContainer.height / 2 : 0;
  205. const distanceFromCenter = parentCenter - elementCenter;
  206. placement.top = distanceFromCenter;
  207. placement.height = height;
  208. break;
  209. case VerticalConstraint.Scale:
  210. placement.top = (relativeTop / (parentContainer?.height ?? height)) * 100;
  211. placement.bottom = (relativeBottom / (parentContainer?.height ?? height)) * 100;
  212. break;
  213. }
  214. switch (horizontal) {
  215. case HorizontalConstraint.Left:
  216. placement.left = relativeLeft;
  217. placement.width = width;
  218. break;
  219. case HorizontalConstraint.Right:
  220. placement.right = relativeRight;
  221. placement.width = width;
  222. break;
  223. case HorizontalConstraint.LeftRight:
  224. placement.left = relativeLeft;
  225. placement.right = relativeRight;
  226. break;
  227. case HorizontalConstraint.Center:
  228. const elementCenter = elementContainer ? relativeLeft + width / 2 : 0;
  229. const parentCenter = parentContainer ? parentContainer.width / 2 : 0;
  230. const distanceFromCenter = parentCenter - elementCenter;
  231. placement.left = distanceFromCenter;
  232. placement.width = width;
  233. break;
  234. case HorizontalConstraint.Scale:
  235. placement.left = (relativeLeft / (parentContainer?.width ?? width)) * 100;
  236. placement.right = (relativeRight / (parentContainer?.width ?? width)) * 100;
  237. break;
  238. }
  239. this.options.placement = placement;
  240. this.applyLayoutStylesToDiv();
  241. this.revId++;
  242. }
  243. updateData(ctx: DimensionContext) {
  244. if (this.item.prepareData) {
  245. this.data = this.item.prepareData(ctx, this.options.config);
  246. this.revId++; // rerender
  247. }
  248. const { background, border } = this.options;
  249. const css: CSSProperties = {};
  250. if (background) {
  251. if (background.color) {
  252. const color = ctx.getColor(background.color);
  253. css.backgroundColor = color.value();
  254. }
  255. if (background.image) {
  256. const image = ctx.getResource(background.image);
  257. if (image) {
  258. const v = image.value();
  259. if (v) {
  260. css.backgroundImage = `url("${v}")`;
  261. switch (background.size ?? BackgroundImageSize.Contain) {
  262. case BackgroundImageSize.Contain:
  263. css.backgroundSize = 'contain';
  264. css.backgroundRepeat = 'no-repeat';
  265. break;
  266. case BackgroundImageSize.Cover:
  267. css.backgroundSize = 'cover';
  268. css.backgroundRepeat = 'no-repeat';
  269. break;
  270. case BackgroundImageSize.Original:
  271. css.backgroundRepeat = 'no-repeat';
  272. break;
  273. case BackgroundImageSize.Tile:
  274. css.backgroundRepeat = 'repeat';
  275. break;
  276. case BackgroundImageSize.Fill:
  277. css.backgroundSize = '100% 100%';
  278. break;
  279. }
  280. }
  281. }
  282. }
  283. }
  284. if (border && border.color && border.width) {
  285. const color = ctx.getColor(border.color);
  286. css.borderWidth = border.width;
  287. css.borderStyle = 'solid';
  288. css.borderColor = color.value();
  289. // Move the image to inside the border
  290. if (css.backgroundImage) {
  291. css.backgroundOrigin = 'padding-box';
  292. }
  293. }
  294. this.dataStyle = css;
  295. this.applyLayoutStylesToDiv();
  296. }
  297. isRoot(): this is RootElement {
  298. return false;
  299. }
  300. /** Recursively visit all nodes */
  301. visit(visitor: (v: ElementState) => void) {
  302. visitor(this);
  303. }
  304. onChange(options: CanvasElementOptions) {
  305. if (this.item.id !== options.type) {
  306. this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem;
  307. }
  308. // rename handling
  309. const oldName = this.options.name;
  310. const newName = options.name;
  311. this.revId++;
  312. this.options = { ...options };
  313. let trav = this.parent;
  314. while (trav) {
  315. if (trav.isRoot()) {
  316. trav.scene.save();
  317. break;
  318. }
  319. trav.revId++;
  320. trav = trav.parent;
  321. }
  322. const scene = this.getScene();
  323. if (oldName !== newName && scene) {
  324. scene.byName.delete(oldName);
  325. scene.byName.set(newName, this);
  326. }
  327. }
  328. getSaveModel() {
  329. return { ...this.options };
  330. }
  331. initElement = (target: HTMLDivElement) => {
  332. this.div = target;
  333. this.applyLayoutStylesToDiv();
  334. };
  335. applyDrag = (event: OnDrag) => {
  336. event.target.style.transform = event.transform;
  337. };
  338. // kinda like:
  339. // https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
  340. applyResize = (event: OnResize) => {
  341. const placement = this.options.placement!;
  342. const style = event.target.style;
  343. const deltaX = event.delta[0];
  344. const deltaY = event.delta[1];
  345. const dirLR = event.direction[0];
  346. const dirTB = event.direction[1];
  347. if (dirLR === 1) {
  348. placement.width = event.width;
  349. style.width = `${placement.width}px`;
  350. } else if (dirLR === -1) {
  351. placement.left! -= deltaX;
  352. placement.width = event.width;
  353. style.left = `${placement.left}px`;
  354. style.width = `${placement.width}px`;
  355. }
  356. if (dirTB === -1) {
  357. placement.top! -= deltaY;
  358. placement.height = event.height;
  359. style.top = `${placement.top}px`;
  360. style.height = `${placement.height}px`;
  361. } else if (dirTB === 1) {
  362. placement.height = event.height;
  363. style.height = `${placement.height}px`;
  364. }
  365. };
  366. render() {
  367. const { item } = this;
  368. return (
  369. <div key={this.UID} ref={this.initElement}>
  370. <item.display key={`${this.UID}/${this.revId}`} config={this.options.config} data={this.data} />
  371. </div>
  372. );
  373. }
  374. }