Item.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import React from "react";
  2. import { useC2C } from "../hooks/useC2C";
  3. import { useRecoilValue } from "recoil";
  4. import { selectedItemsAtom } from "./Selector";
  5. import { userAtom } from "../hooks/useUser";
  6. const Rect = ({ width, height, color }) => {
  7. return (
  8. <div
  9. style={{
  10. width: width,
  11. height: height,
  12. backgroundColor: color,
  13. }}
  14. />
  15. );
  16. };
  17. const Round = ({
  18. radius,
  19. color,
  20. text = "",
  21. textColor = "#000",
  22. fontSize = "16",
  23. }) => {
  24. return (
  25. <div
  26. style={{
  27. borderRadius: "100%",
  28. width: radius,
  29. height: radius,
  30. backgroundColor: color,
  31. textAlign: "center",
  32. display: "flex",
  33. alignItems: "center",
  34. justifyContent: "center",
  35. }}
  36. >
  37. <span
  38. style={{
  39. textColor,
  40. fontSize: fontSize + "px",
  41. }}
  42. >
  43. {text}
  44. </span>
  45. </div>
  46. );
  47. };
  48. const Counter = ({
  49. value = 0,
  50. color = "#CCC",
  51. label = "",
  52. textColor = "#000",
  53. fontSize = "16",
  54. updateState,
  55. }) => {
  56. const setValue = (e) => {
  57. updateState((prevState) => ({
  58. ...prevState,
  59. value: e.target.value,
  60. }));
  61. };
  62. const increment = (e) => {
  63. updateState((prevState) => ({
  64. ...prevState,
  65. value: prevState.value + 1,
  66. }));
  67. };
  68. const decrement = (e) => {
  69. updateState((prevState) => ({
  70. ...prevState,
  71. value: prevState.value - 1,
  72. }));
  73. };
  74. return (
  75. <div
  76. style={{
  77. backgroundColor: color,
  78. width: "5em",
  79. padding: "0.5em",
  80. paddingBottom: "2em",
  81. textAlign: "center",
  82. fontSize: fontSize + "px",
  83. display: "flex",
  84. justifyContent: "space-between",
  85. flexDirection: "column",
  86. borderRadius: "0.5em",
  87. boxShadow: "10px 10px 13px 0px rgb(0, 0, 0, 0.3)",
  88. }}
  89. >
  90. <label style={{ userSelect: "none" }}>
  91. {label}
  92. <input
  93. style={{
  94. textColor,
  95. width: "100%",
  96. display: "block",
  97. textAlign: "center",
  98. border: "none",
  99. margin: "0.2em 0",
  100. padding: "0.2em 0",
  101. fontSize: fontSize + "px",
  102. userSelect: "none",
  103. }}
  104. value={value}
  105. onChange={setValue}
  106. />
  107. </label>
  108. <span
  109. style={{
  110. paddingTop: "1em",
  111. }}
  112. >
  113. <button onClick={increment} style={{ fontSize: fontSize + "px" }}>
  114. +
  115. </button>
  116. <button onClick={decrement} style={{ fontSize: fontSize + "px" }}>
  117. -
  118. </button>
  119. </span>
  120. </div>
  121. );
  122. };
  123. // See https://stackoverflow.com/questions/3680429/click-through-div-to-underlying-elements
  124. // https://developer.mozilla.org/fr/docs/Web/CSS/pointer-events
  125. const Image = ({
  126. width,
  127. height,
  128. content,
  129. backContent,
  130. flipped,
  131. updateState,
  132. unflippedFor,
  133. text,
  134. backText,
  135. overlay,
  136. }) => {
  137. const user = useRecoilValue(userAtom);
  138. const size = {};
  139. if (width) {
  140. size.width = width;
  141. }
  142. if (height) {
  143. size.height = height;
  144. }
  145. const onDblClick = React.useCallback(
  146. (e) => {
  147. if (e.ctrlKey) {
  148. updateState((prevItem) => {
  149. if (prevItem.unflippedFor !== null) {
  150. return { ...prevItem, unflippedFor: null };
  151. } else {
  152. return { ...prevItem, unflippedFor: user.id, flipped: false };
  153. }
  154. });
  155. } else {
  156. updateState((prevItem) => ({
  157. ...prevItem,
  158. flipped: !prevItem.flipped,
  159. unflippedFor: null,
  160. }));
  161. }
  162. },
  163. [updateState, user.id]
  164. );
  165. let image;
  166. if (backContent && (flipped || (unflippedFor && unflippedFor !== user.id))) {
  167. image = (
  168. <>
  169. {text && (
  170. <div
  171. className="image-text"
  172. style={{
  173. position: "absolute",
  174. right: 0,
  175. padding: "0 3px",
  176. backgroundColor: "black",
  177. color: "white",
  178. borderRadius: "50%",
  179. userSelect: "none",
  180. }}
  181. >
  182. {backText}
  183. </div>
  184. )}
  185. <img
  186. src={backContent}
  187. alt=""
  188. draggable={false}
  189. {...size}
  190. style={{ userSelect: "none", pointerEvents: "none" }}
  191. />
  192. </>
  193. );
  194. } else {
  195. image = (
  196. <div className="image-wrapper" style={{ position: "relative" }}>
  197. {unflippedFor && (
  198. <div
  199. style={{
  200. position: "absolute",
  201. top: "-18px",
  202. left: "4px",
  203. color: "#555",
  204. backgroundColor: "#CCCCCCA0",
  205. userSelect: "none",
  206. pointerEvents: "none",
  207. }}
  208. >
  209. Only you
  210. </div>
  211. )}
  212. {overlay && (
  213. <img
  214. src={overlay.content}
  215. alt=""
  216. style={{
  217. position: "absolute",
  218. userSelect: "none",
  219. pointerEvents: "none",
  220. }}
  221. />
  222. )}
  223. {text && (
  224. <div
  225. className="image-text"
  226. style={{
  227. position: "absolute",
  228. right: 0,
  229. padding: "0 3px",
  230. backgroundColor: "black",
  231. color: "white",
  232. borderRadius: "50%",
  233. userSelect: "none",
  234. }}
  235. >
  236. {text}
  237. </div>
  238. )}
  239. <img
  240. src={content}
  241. alt=""
  242. draggable={false}
  243. {...size}
  244. style={{ userSelect: "none", pointerEvents: "none" }}
  245. />
  246. </div>
  247. );
  248. }
  249. return <div onDoubleClick={onDblClick}>{image}</div>;
  250. };
  251. const getComponent = (type) => {
  252. switch (type) {
  253. case "rect":
  254. return Rect;
  255. case "round":
  256. return Round;
  257. case "image":
  258. return Image;
  259. case "counter":
  260. return Counter;
  261. default:
  262. return Rect;
  263. }
  264. };
  265. const Item = ({ setState, state }) => {
  266. const selectedItems = useRecoilValue(selectedItemsAtom);
  267. const itemRef = React.useRef(null);
  268. const [unlock, setUnlock] = React.useState(false);
  269. React.useEffect(() => {
  270. // Add id to element
  271. itemRef.current.id = state.id;
  272. }, [state]);
  273. // Allow to operate on locked item if ctrl is pressed
  274. React.useEffect(() => {
  275. const onKeyDown = (e) => {
  276. if (e.key === "Control") {
  277. setUnlock(true);
  278. }
  279. };
  280. const onKeyUp = (e) => {
  281. if (e.key === "Control") {
  282. setUnlock(false);
  283. }
  284. };
  285. document.addEventListener("keydown", onKeyDown);
  286. document.addEventListener("keyup", onKeyUp);
  287. return () => {
  288. document.removeEventListener("keydown", onKeyDown);
  289. document.removeEventListener("keyup", onKeyUp);
  290. };
  291. }, []);
  292. const Component = getComponent(state.type);
  293. const style = {};
  294. if (selectedItems.includes(state.id)) {
  295. style.border = "2px dashed #ff0000A0";
  296. style.padding = "2px";
  297. } else {
  298. style.padding = "4px";
  299. }
  300. const rotation = state.rotation || 0;
  301. const updateState = React.useCallback(
  302. (callbackOrItem, sync = true) => setState(state.id, callbackOrItem, sync),
  303. [setState, state]
  304. );
  305. // Update actual size when update
  306. React.useEffect(() => {
  307. const currentElem = itemRef.current;
  308. const callback = (entries) => {
  309. entries.forEach((entry) => {
  310. if (entry.contentBoxSize) {
  311. const { inlineSize: width, blockSize: height } = entry.contentBoxSize;
  312. if (state.actualWidth !== width || state.actualHeight !== height) {
  313. updateState(
  314. (prevState) => ({
  315. ...prevState,
  316. actualWidth: width,
  317. actualHeight: height,
  318. }),
  319. false // Don't need to sync that.
  320. );
  321. }
  322. }
  323. });
  324. };
  325. const observer = new ResizeObserver(callback);
  326. observer.observe(currentElem);
  327. return () => {
  328. observer.unobserve(currentElem);
  329. };
  330. }, [updateState, state]);
  331. const content = (
  332. <div
  333. style={{
  334. position: "absolute",
  335. left: state.x,
  336. top: state.y,
  337. display: "inline-block",
  338. boxSizing: "content-box",
  339. transform: `rotate(${rotation}deg)`,
  340. ...style,
  341. }}
  342. className="item"
  343. ref={itemRef}
  344. >
  345. <Component {...state} updateState={updateState} />
  346. </div>
  347. );
  348. if (!state.locked || unlock) {
  349. return content;
  350. }
  351. return (
  352. <div
  353. style={{
  354. pointerEvents: "none",
  355. userSelect: "none",
  356. }}
  357. >
  358. {content}
  359. </div>
  360. );
  361. };
  362. const SyncedItem = ({ setState, state }) => {
  363. const [c2c] = useC2C();
  364. React.useEffect(() => {
  365. const unsub = c2c.subscribe(
  366. `itemStateUpdate.${state.id}`,
  367. (newItemState) => {
  368. setState(
  369. state.id,
  370. (prevState) => ({
  371. ...newItemState,
  372. // Ignore some modifications
  373. actualWidth: prevState.actualWidth,
  374. actualHeight: prevState.actualHeight,
  375. }),
  376. false
  377. );
  378. }
  379. );
  380. return unsub;
  381. }, [c2c, setState, state]);
  382. return <Item state={state} setState={setState} />;
  383. };
  384. export default SyncedItem;