Generator.jsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import React, { memo } from "react";
  2. import styled, { css } from "styled-components";
  3. import { useItemActions, useItemInteraction, useWire } from "react-sync-board";
  4. import { uid } from "../utils";
  5. import itemTemplates from "./itemTemplates";
  6. import { useTranslation } from "react-i18next";
  7. import debounce from "lodash.debounce";
  8. const StyledShape = styled.div`
  9. ${({ color }) => css`
  10. box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px,
  11. rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
  12. border: 3px dashed black;
  13. border-color: ${color};
  14. border-radius: 3px;
  15. background-color: #cccccc22;
  16. & .wrapper {
  17. opacity: 0.3;
  18. position: relative;
  19. }
  20. & .item-wrapper {
  21. position: absolute;
  22. top: ${({ center: { top } }) => `${top}px`};
  23. left: ${({ center: { left } }) => `${left}px`};
  24. }
  25. & .handle {
  26. position: absolute;
  27. top: -15px;
  28. left: -15px;
  29. user-select: none;
  30. & img {
  31. pointer-events: none;
  32. }
  33. }
  34. `}
  35. `;
  36. const Generator = ({ color = "#ccc", item, id, currentItemId, setState }) => {
  37. const { t } = useTranslation();
  38. const { isMaster } = useWire("board");
  39. const itemRef = React.useRef(null);
  40. const [dimension, setDimension] = React.useState({
  41. width: 50,
  42. height: 50,
  43. });
  44. const [center, setCenter] = React.useState({ top: 0, left: 0 });
  45. const { register } = useItemInteraction("place");
  46. const {
  47. pushItem,
  48. getItems,
  49. batchUpdateItems,
  50. removeItems,
  51. } = useItemActions();
  52. const centerRef = React.useRef(center);
  53. Object.assign(centerRef.current, center);
  54. const currentItemRef = React.useRef(currentItemId);
  55. currentItemRef.current = currentItemId;
  56. const addItem = React.useCallback(async () => {
  57. /**
  58. * Add new generated item
  59. */
  60. const [thisItem] = await getItems([id]);
  61. const { item } = thisItem || {}; // Inside item library, thisItem is not defined
  62. if (item?.type) {
  63. const newItemId = uid();
  64. await pushItem({
  65. ...item,
  66. x: thisItem.x + centerRef.current.left + 3,
  67. y: thisItem.y + centerRef.current.top + 3,
  68. layer: thisItem.layer + 1,
  69. editable: false,
  70. id: newItemId,
  71. });
  72. currentItemRef.current = newItemId;
  73. setState((prev) => ({ ...prev, currentItemId: newItemId }));
  74. }
  75. }, [getItems, id, pushItem, setState]);
  76. const centerItem = React.useCallback(async () => {
  77. /**
  78. * Center generated item
  79. */
  80. const [thisItem, other] = await getItems([id, currentItemRef.current]);
  81. if (!other) {
  82. // Item has been deleted, need a new one.
  83. currentItemRef.current = undefined;
  84. setState((prev) => ({ ...prev, currentItemId: undefined }));
  85. } else {
  86. batchUpdateItems([currentItemRef.current], (item) => {
  87. const newX = thisItem.x + centerRef.current.left + 3;
  88. const newY = thisItem.y + centerRef.current.top + 3;
  89. /* Prevent modification if item doesn't need update */
  90. if (
  91. newX !== item.x ||
  92. newY !== item.y ||
  93. item.layer !== thisItem.layer + 1
  94. ) {
  95. return {
  96. ...item,
  97. x: newX,
  98. y: newY,
  99. layer: thisItem.layer + 1,
  100. };
  101. }
  102. return item;
  103. });
  104. }
  105. }, [batchUpdateItems, getItems, id, setState]);
  106. const onPlaceItem = React.useCallback(
  107. async (itemIds) => {
  108. /**
  109. * Callback if generated item or generator is placed
  110. */
  111. const placeSelf = itemIds.includes(id);
  112. if (itemIds.includes(currentItemRef.current) && !placeSelf) {
  113. // We have removed generated item so we create a new one.
  114. const [thisItem] = await getItems([id]);
  115. batchUpdateItems([currentItemRef.current], (item) => {
  116. const result = {
  117. ...item,
  118. layer: thisItem.layer,
  119. };
  120. delete result.editable;
  121. return result;
  122. });
  123. await addItem();
  124. }
  125. if (placeSelf) {
  126. if (!currentItemRef.current) {
  127. // Missing item for any reason
  128. await addItem();
  129. } else {
  130. // We are moving generator so we must
  131. // update generated item position
  132. await centerItem();
  133. }
  134. }
  135. },
  136. [addItem, batchUpdateItems, centerItem, getItems, id]
  137. );
  138. /**
  139. * Set generator dimension according to Item content.
  140. */
  141. // eslint-disable-next-line react-hooks/exhaustive-deps
  142. const resize = React.useCallback(
  143. debounce((rotation) => {
  144. let targetWidth, targetHeight;
  145. const { clientWidth, clientHeight } = itemRef.current;
  146. targetWidth = clientWidth;
  147. targetHeight = clientHeight;
  148. if (currentItemRef.current) {
  149. // Get size from current item if any
  150. const currentDomItem = document.getElementById(currentItemRef.current);
  151. if (currentDomItem) {
  152. targetWidth = currentDomItem.clientWidth;
  153. targetHeight = currentDomItem.clientHeight;
  154. }
  155. }
  156. /* Compute size relative to rotation */
  157. const rad = (rotation || 0) * (Math.PI / 180);
  158. const cos = Math.abs(Math.cos(rad));
  159. const sin = Math.abs(Math.sin(rad));
  160. const width = targetWidth * cos + targetHeight * sin;
  161. const height = targetWidth * sin + targetHeight * cos;
  162. const top = -targetHeight / 2 + height / 2 + 3;
  163. const left = -targetWidth / 2 + width / 2 + 3;
  164. setCenter({
  165. top,
  166. left,
  167. });
  168. centerRef.current = {
  169. top,
  170. left,
  171. };
  172. setDimension((prev) => ({ ...prev, width, height }));
  173. }, 100),
  174. []
  175. );
  176. React.useEffect(() => {
  177. /**
  178. * update item on modifications only if master
  179. */
  180. if (item?.type && isMaster) {
  181. batchUpdateItems([currentItemRef.current], (prev) => ({
  182. ...prev,
  183. ...item,
  184. }));
  185. }
  186. }, [batchUpdateItems, isMaster, item]);
  187. React.useEffect(() => {
  188. /**
  189. * Add item if missing
  190. */
  191. if (isMaster && !currentItemId && item?.type) {
  192. addItem();
  193. }
  194. }, [addItem, currentItemId, isMaster, item?.type]);
  195. React.useEffect(() => {
  196. /**
  197. * Check if type is defined
  198. */
  199. const checkType = async () => {
  200. if (currentItemRef.current) {
  201. const [currentItem] = await getItems([currentItemRef.current]);
  202. if (currentItem?.type !== item.type) {
  203. if (currentItem) {
  204. // Remove old if exists
  205. await removeItems([currentItemRef.current]);
  206. }
  207. // Add new item on new type
  208. await addItem();
  209. }
  210. }
  211. };
  212. if (item?.type && isMaster) {
  213. checkType();
  214. }
  215. }, [addItem, getItems, item?.type, removeItems, isMaster]);
  216. React.useEffect(() => {
  217. /**
  218. * Register onPlaceItem callback
  219. */
  220. const unregisterList = [];
  221. if (currentItemId) {
  222. unregisterList.push(register(onPlaceItem));
  223. }
  224. return () => {
  225. unregisterList.forEach((callback) => callback());
  226. };
  227. }, [register, onPlaceItem, currentItemId]);
  228. React.useEffect(() => {
  229. /**
  230. * Update center and generator width height
  231. */
  232. resize(item?.rotation);
  233. }, [item, resize, dimension.height, dimension.width]);
  234. React.useEffect(() => {
  235. if (currentItemRef.current && isMaster) {
  236. centerItem();
  237. }
  238. }, [item, centerItem, isMaster, center]);
  239. // Define item component if type is defined
  240. let Item = () => (
  241. <div
  242. style={{
  243. display: "block",
  244. width: "60px",
  245. height: "60px",
  246. fontSize: "0.65em",
  247. textAlign: "center",
  248. }}
  249. >
  250. {t("No item type defined")}
  251. </div>
  252. );
  253. if (item) {
  254. const itemTemplate = itemTemplates[item.type];
  255. Item = itemTemplate.component;
  256. }
  257. return (
  258. <StyledShape color={color} center={center}>
  259. <div className="handle">
  260. <img src="https://icongr.am/clarity/cursor-move.svg?size=20&color=ffffff" />
  261. </div>
  262. <div className="wrapper" style={dimension}>
  263. <div
  264. style={{
  265. transform: `rotate(${item?.rotation || 0}deg)`,
  266. }}
  267. ref={itemRef}
  268. className="item-wrapper"
  269. >
  270. <Item {...item} />
  271. </div>
  272. </div>
  273. </StyledShape>
  274. );
  275. };
  276. export default memo(Generator);