123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- 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 = () => (
- <div
- style={{
- display: "block",
- width: "60px",
- height: "60px",
- fontSize: "0.65em",
- textAlign: "center",
- }}
- >
- {t("No item type defined")}
- </div>
- );
- if (item) {
- const itemTemplate = itemTemplates[item.type];
- Item = itemTemplate.component;
- }
- return (
- <StyledShape color={color} center={center}>
- <div className="handle">
- <img src="https://icongr.am/clarity/cursor-move.svg?size=20&color=ffffff" />
- </div>
- <div className="wrapper" style={dimension}>
- <div
- style={{
- transform: `rotate(${item?.rotation || 0}deg)`,
- }}
- ref={itemRef}
- className="item-wrapper"
- >
- <Item {...item} />
- </div>
- </div>
- </StyledShape>
- );
- };
- export default memo(Generator);
|