import React, { memo } from "react"; import styled, { css } from "styled-components"; import { useItemActions, useItemInteraction, useWire } from "react-sync-board"; import { uid } from "../utils"; import itemTemplates from "./itemTemplates"; import { useTranslation } from "react-i18next"; import debounce from "lodash.debounce"; const StyledShape = styled.div` ${({ color }) => css` box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px; border: 3px dashed black; border-color: ${color}; border-radius: 3px; background-color: #cccccc22; & .wrapper { opacity: 0.3; position: relative; } & .item-wrapper { position: absolute; top: ${({ center: { top } }) => `${top}px`}; left: ${({ center: { left } }) => `${left}px`}; } & .handle { position: absolute; top: -15px; left: -15px; user-select: none; & img { pointer-events: none; } } `} `; const Generator = ({ color = "#ccc", item, id, currentItemId, setState }) => { const { t } = useTranslation(); const { isMaster } = useWire("board"); const itemRef = React.useRef(null); const [dimension, setDimension] = React.useState({ width: 50, height: 50, }); const [center, setCenter] = React.useState({ top: 0, left: 0 }); const { register } = useItemInteraction("place"); const { pushItem, getItems, batchUpdateItems, removeItems, } = useItemActions(); const centerRef = React.useRef(center); Object.assign(centerRef.current, center); const currentItemRef = React.useRef(currentItemId); currentItemRef.current = currentItemId; const addItem = React.useCallback(async () => { /** * Add new generated item */ const [thisItem] = await getItems([id]); const { item } = thisItem || {}; // Inside item library, thisItem is not defined if (item?.type) { const newItemId = uid(); await pushItem({ ...item, x: thisItem.x + centerRef.current.left + 3, y: thisItem.y + centerRef.current.top + 3, layer: thisItem.layer + 1, editable: false, id: newItemId, }); currentItemRef.current = newItemId; setState((prev) => ({ ...prev, currentItemId: newItemId })); } }, [getItems, id, pushItem, setState]); const centerItem = React.useCallback(async () => { /** * Center generated item */ const [thisItem, other] = await getItems([id, currentItemRef.current]); if (!other) { // Item has been deleted, need a new one. currentItemRef.current = undefined; setState((prev) => ({ ...prev, currentItemId: undefined })); } else { batchUpdateItems([currentItemRef.current], (item) => { const newX = thisItem.x + centerRef.current.left + 3; const newY = thisItem.y + centerRef.current.top + 3; /* Prevent modification if item doesn't need update */ if ( newX !== item.x || newY !== item.y || item.layer !== thisItem.layer + 1 ) { return { ...item, x: newX, y: newY, layer: thisItem.layer + 1, }; } return item; }); } }, [batchUpdateItems, getItems, id, setState]); const onPlaceItem = React.useCallback( async (itemIds) => { /** * Callback if generated item or generator is placed */ const placeSelf = itemIds.includes(id); if (itemIds.includes(currentItemRef.current) && !placeSelf) { // We have removed generated item so we create a new one. const [thisItem] = await getItems([id]); batchUpdateItems([currentItemRef.current], (item) => { const result = { ...item, layer: thisItem.layer, }; delete result.editable; return result; }); await addItem(); } if (placeSelf) { if (!currentItemRef.current) { // Missing item for any reason await addItem(); } else { // We are moving generator so we must // update generated item position await centerItem(); } } }, [addItem, batchUpdateItems, centerItem, getItems, id] ); /** * Set generator dimension according to Item content. */ // eslint-disable-next-line react-hooks/exhaustive-deps const resize = React.useCallback( debounce((rotation) => { let targetWidth, targetHeight; const { clientWidth, clientHeight } = itemRef.current; targetWidth = clientWidth; targetHeight = clientHeight; if (currentItemRef.current) { // Get size from current item if any const currentDomItem = document.getElementById(currentItemRef.current); if (currentDomItem) { targetWidth = currentDomItem.clientWidth; targetHeight = currentDomItem.clientHeight; } } /* Compute size relative to rotation */ const rad = (rotation || 0) * (Math.PI / 180); const cos = Math.abs(Math.cos(rad)); const sin = Math.abs(Math.sin(rad)); const width = targetWidth * cos + targetHeight * sin; const height = targetWidth * sin + targetHeight * cos; const top = -targetHeight / 2 + height / 2 + 3; const left = -targetWidth / 2 + width / 2 + 3; setCenter({ top, left, }); centerRef.current = { top, left, }; setDimension((prev) => ({ ...prev, width, height })); }, 100), [] ); React.useEffect(() => { /** * update item on modifications only if master */ if (item?.type && isMaster) { batchUpdateItems([currentItemRef.current], (prev) => ({ ...prev, ...item, })); } }, [batchUpdateItems, isMaster, item]); React.useEffect(() => { /** * Add item if missing */ if (isMaster && !currentItemId && item?.type) { addItem(); } }, [addItem, currentItemId, isMaster, item?.type]); React.useEffect(() => { /** * Check if type is defined */ const checkType = async () => { if (currentItemRef.current) { const [currentItem] = await getItems([currentItemRef.current]); if (currentItem?.type !== item.type) { if (currentItem) { // Remove old if exists await removeItems([currentItemRef.current]); } // Add new item on new type await addItem(); } } }; if (item?.type && isMaster) { checkType(); } }, [addItem, getItems, item?.type, removeItems, isMaster]); React.useEffect(() => { /** * Register onPlaceItem callback */ const unregisterList = []; if (currentItemId) { unregisterList.push(register(onPlaceItem)); } return () => { unregisterList.forEach((callback) => callback()); }; }, [register, onPlaceItem, currentItemId]); React.useEffect(() => { /** * Update center and generator width height */ resize(item?.rotation); }, [item, resize, dimension.height, dimension.width]); React.useEffect(() => { if (currentItemRef.current && isMaster) { centerItem(); } }, [item, centerItem, isMaster, center]); // Define item component if type is defined let Item = () => (
{t("No item type defined")}
); if (item) { const itemTemplate = itemTemplates[item.type]; Item = itemTemplate.component; } return (
); }; export default memo(Generator);