Item.jsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import React, { memo } from "react";
  2. import styled from "styled-components";
  3. import lockIcon from "../../../images/lock.svg";
  4. const ItemWrapper = styled.div.attrs(({ rotation, locked, selected }) => {
  5. let className = "item";
  6. if (locked) {
  7. className += " locked";
  8. }
  9. if (selected) {
  10. className += " selected";
  11. }
  12. return {
  13. className,
  14. style: {
  15. transform: `rotate(${rotation}deg)`,
  16. },
  17. };
  18. })`
  19. display: inline-block;
  20. transition: transform 150ms;
  21. user-select: none;
  22. & .corner {
  23. position: absolute;
  24. width: 0px;
  25. height: 0px;
  26. }
  27. & .top-left {
  28. top: 0;
  29. left: 0;
  30. }
  31. & .top-right {
  32. top: 0;
  33. right: 0;
  34. }
  35. & .bottom-left {
  36. bottom: 0;
  37. left: 0;
  38. }
  39. & .bottom-right {
  40. bottom: 0;
  41. right: 0;
  42. }
  43. padding: 4px;
  44. &.selected {
  45. border: 2px dashed var(--color-primary);
  46. padding: 2px;
  47. cursor: pointer;
  48. }
  49. &.locked::after {
  50. content: "";
  51. position: absolute;
  52. width: 24px;
  53. height: 30px;
  54. top: 4px;
  55. right: 4px;
  56. opacity: 0.1;
  57. background-image: url(${lockIcon});
  58. background-size: cover;
  59. user-select: none;
  60. }
  61. &.locked:hover::after {
  62. opacity: 0.3;
  63. }
  64. `;
  65. const Item = ({
  66. setState,
  67. state: { type, rotation = 0, id, locked, layer, ...rest } = {},
  68. animate = "hvr-pop",
  69. isSelected,
  70. itemMap,
  71. unlocked,
  72. }) => {
  73. const itemRef = React.useRef(null);
  74. const isMountedRef = React.useRef(false);
  75. const animateRef = React.useRef(null);
  76. const Component = itemMap[type].component || null;
  77. const updateState = React.useCallback(
  78. (callbackOrItem, sync = true) => setState(id, callbackOrItem, sync),
  79. [setState, id]
  80. );
  81. // Update actual size when update
  82. React.useEffect(() => {
  83. isMountedRef.current = true;
  84. return () => {
  85. isMountedRef.current = false;
  86. };
  87. }, []);
  88. React.useEffect(() => {
  89. animateRef.current.className = animate;
  90. }, [animate]);
  91. const removeClass = (e) => {
  92. e.target.className = "";
  93. };
  94. return (
  95. <ItemWrapper
  96. rotation={rotation}
  97. locked={locked && !unlocked}
  98. selected={isSelected}
  99. ref={itemRef}
  100. layer={layer}
  101. id={id}
  102. >
  103. <div
  104. ref={animateRef}
  105. onAnimationEnd={removeClass}
  106. onKeyDown={(e) => e.stopPropagation()}
  107. onKeyUp={(e) => e.stopPropagation()}
  108. >
  109. <Component {...rest} setState={updateState} />
  110. <div className="corner top-left"></div>
  111. <div className="corner top-right"></div>
  112. <div className="corner bottom-left"></div>
  113. <div className="corner bottom-right"></div>
  114. </div>
  115. </ItemWrapper>
  116. );
  117. };
  118. const MemoizedItem = memo(
  119. Item,
  120. (
  121. {
  122. state: prevState,
  123. setState: prevSetState,
  124. isSelected: prevIsSelected,
  125. unlocked: prevUnlocked,
  126. },
  127. {
  128. state: nextState,
  129. setState: nextSetState,
  130. isSelected: nextIsSelected,
  131. unlocked: nextUnlocked,
  132. }
  133. ) => {
  134. return (
  135. prevIsSelected === nextIsSelected &&
  136. prevUnlocked === nextUnlocked &&
  137. prevSetState === nextSetState &&
  138. JSON.stringify(prevState) === JSON.stringify(nextState)
  139. );
  140. }
  141. );
  142. // Exclude positionning from memoization
  143. const PositionedItem = ({
  144. state: { x, y, layer, moving, ...stateRest } = {},
  145. ...rest
  146. }) => {
  147. return (
  148. <div
  149. style={{
  150. transform: `translate(${x}px, ${y}px)`,
  151. display: "inline-block",
  152. zIndex: ((layer || 0) + 4) * 10 + 100 + (moving ? 5 : 0), // Items z-index between 100 and 200
  153. position: "absolute",
  154. top: 0,
  155. left: 0,
  156. }}
  157. >
  158. <MemoizedItem {...rest} state={stateRest} />
  159. </div>
  160. );
  161. };
  162. const MemoizedPositionedItem = memo(PositionedItem);
  163. export default MemoizedPositionedItem;