123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- import React from "react";
- import { useC2C } from "../hooks/useC2C";
- import { useRecoilValue } from "recoil";
- import { selectedItemsAtom } from "./Selector";
- import { userAtom } from "../hooks/useUser";
- const Rect = ({ width, height, color }) => {
- return (
- <div
- style={{
- width: width,
- height: height,
- backgroundColor: color,
- }}
- />
- );
- };
- const Round = ({
- radius,
- color,
- text = "",
- textColor = "#000",
- fontSize = "16",
- }) => {
- return (
- <div
- style={{
- borderRadius: "100%",
- width: radius,
- height: radius,
- backgroundColor: color,
- textAlign: "center",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- }}
- >
- <span
- style={{
- textColor,
- fontSize: fontSize + "px",
- }}
- >
- {text}
- </span>
- </div>
- );
- };
- const Counter = ({
- value = 0,
- color = "#CCC",
- label = "",
- textColor = "#000",
- fontSize = "16",
- updateState,
- }) => {
- const setValue = (e) => {
- updateState((prevState) => ({
- ...prevState,
- value: e.target.value,
- }));
- };
- const increment = (e) => {
- updateState((prevState) => ({
- ...prevState,
- value: prevState.value + 1,
- }));
- };
- const decrement = (e) => {
- updateState((prevState) => ({
- ...prevState,
- value: prevState.value - 1,
- }));
- };
- return (
- <div
- style={{
- backgroundColor: color,
- width: "5em",
- padding: "0.5em",
- paddingBottom: "2em",
- textAlign: "center",
- fontSize: fontSize + "px",
- display: "flex",
- justifyContent: "space-between",
- flexDirection: "column",
- borderRadius: "0.5em",
- boxShadow: "10px 10px 13px 0px rgb(0, 0, 0, 0.3)",
- }}
- >
- <label style={{ userSelect: "none" }}>
- {label}
- <input
- style={{
- textColor,
- width: "100%",
- display: "block",
- textAlign: "center",
- border: "none",
- margin: "0.2em 0",
- padding: "0.2em 0",
- fontSize: fontSize + "px",
- userSelect: "none",
- }}
- value={value}
- onChange={setValue}
- />
- </label>
- <span
- style={{
- paddingTop: "1em",
- }}
- >
- <button onClick={increment} style={{ fontSize: fontSize + "px" }}>
- +
- </button>
- <button onClick={decrement} style={{ fontSize: fontSize + "px" }}>
- -
- </button>
- </span>
- </div>
- );
- };
- // See https://stackoverflow.com/questions/3680429/click-through-div-to-underlying-elements
- // https://developer.mozilla.org/fr/docs/Web/CSS/pointer-events
- const Image = ({
- width,
- height,
- content,
- backContent,
- flipped,
- updateState,
- unflippedFor,
- text,
- backText,
- overlay,
- }) => {
- const user = useRecoilValue(userAtom);
- const size = {};
- if (width) {
- size.width = width;
- }
- if (height) {
- size.height = height;
- }
- const onDblClick = React.useCallback(
- (e) => {
- if (e.ctrlKey) {
- updateState((prevItem) => {
- if (prevItem.unflippedFor !== null) {
- return { ...prevItem, unflippedFor: null };
- } else {
- return { ...prevItem, unflippedFor: user.id, flipped: false };
- }
- });
- } else {
- updateState((prevItem) => ({
- ...prevItem,
- flipped: !prevItem.flipped,
- unflippedFor: null,
- }));
- }
- },
- [updateState, user.id]
- );
- let image;
- if (backContent && (flipped || (unflippedFor && unflippedFor !== user.id))) {
- image = (
- <>
- {text && (
- <div
- className="image-text"
- style={{
- position: "absolute",
- right: 0,
- padding: "0 3px",
- backgroundColor: "black",
- color: "white",
- borderRadius: "50%",
- userSelect: "none",
- }}
- >
- {backText}
- </div>
- )}
- <img
- src={backContent}
- alt=""
- draggable={false}
- {...size}
- style={{ userSelect: "none", pointerEvents: "none" }}
- />
- </>
- );
- } else {
- image = (
- <div className="image-wrapper" style={{ position: "relative" }}>
- {unflippedFor && (
- <div
- style={{
- position: "absolute",
- top: "-18px",
- left: "4px",
- color: "#555",
- backgroundColor: "#CCCCCCA0",
- userSelect: "none",
- pointerEvents: "none",
- }}
- >
- Only you
- </div>
- )}
- {overlay && (
- <img
- src={overlay.content}
- alt=""
- style={{
- position: "absolute",
- userSelect: "none",
- pointerEvents: "none",
- }}
- />
- )}
- {text && (
- <div
- className="image-text"
- style={{
- position: "absolute",
- right: 0,
- padding: "0 3px",
- backgroundColor: "black",
- color: "white",
- borderRadius: "50%",
- userSelect: "none",
- }}
- >
- {text}
- </div>
- )}
- <img
- src={content}
- alt=""
- draggable={false}
- {...size}
- style={{ userSelect: "none", pointerEvents: "none" }}
- />
- </div>
- );
- }
- return <div onDoubleClick={onDblClick}>{image}</div>;
- };
- const getComponent = (type) => {
- switch (type) {
- case "rect":
- return Rect;
- case "round":
- return Round;
- case "image":
- return Image;
- case "counter":
- return Counter;
- default:
- return Rect;
- }
- };
- const Item = ({ setState, state }) => {
- const selectedItems = useRecoilValue(selectedItemsAtom);
- const itemRef = React.useRef(null);
- const [unlock, setUnlock] = React.useState(false);
- React.useEffect(() => {
- // Add id to element
- itemRef.current.id = state.id;
- }, [state]);
- // Allow to operate on locked item if ctrl is pressed
- React.useEffect(() => {
- const onKeyDown = (e) => {
- if (e.key === "Control") {
- setUnlock(true);
- }
- };
- const onKeyUp = (e) => {
- if (e.key === "Control") {
- setUnlock(false);
- }
- };
- document.addEventListener("keydown", onKeyDown);
- document.addEventListener("keyup", onKeyUp);
- return () => {
- document.removeEventListener("keydown", onKeyDown);
- document.removeEventListener("keyup", onKeyUp);
- };
- }, []);
- const Component = getComponent(state.type);
- const style = {};
- if (selectedItems.includes(state.id)) {
- style.border = "2px dashed #ff0000A0";
- style.padding = "2px";
- } else {
- style.padding = "4px";
- }
- const rotation = state.rotation || 0;
- const updateState = React.useCallback(
- (callbackOrItem, sync = true) => setState(state.id, callbackOrItem, sync),
- [setState, state]
- );
- // Update actual size when update
- React.useEffect(() => {
- const currentElem = itemRef.current;
- const callback = (entries) => {
- entries.forEach((entry) => {
- if (entry.contentBoxSize) {
- const { inlineSize: width, blockSize: height } = entry.contentBoxSize;
- if (state.actualWidth !== width || state.actualHeight !== height) {
- updateState(
- (prevState) => ({
- ...prevState,
- actualWidth: width,
- actualHeight: height,
- }),
- false // Don't need to sync that.
- );
- }
- }
- });
- };
- const observer = new ResizeObserver(callback);
- observer.observe(currentElem);
- return () => {
- observer.unobserve(currentElem);
- };
- }, [updateState, state]);
- const content = (
- <div
- style={{
- position: "absolute",
- left: state.x,
- top: state.y,
- display: "inline-block",
- boxSizing: "content-box",
- transform: `rotate(${rotation}deg)`,
- ...style,
- }}
- className="item"
- ref={itemRef}
- >
- <Component {...state} updateState={updateState} />
- </div>
- );
- if (!state.locked || unlock) {
- return content;
- }
- return (
- <div
- style={{
- pointerEvents: "none",
- userSelect: "none",
- }}
- >
- {content}
- </div>
- );
- };
- const SyncedItem = ({ setState, state }) => {
- const [c2c] = useC2C();
- React.useEffect(() => {
- const unsub = c2c.subscribe(
- `itemStateUpdate.${state.id}`,
- (newItemState) => {
- setState(
- state.id,
- (prevState) => ({
- ...newItemState,
- // Ignore some modifications
- actualWidth: prevState.actualWidth,
- actualHeight: prevState.actualHeight,
- }),
- false
- );
- }
- );
- return unsub;
- }, [c2c, setState, state]);
- return <Item state={state} setState={setState} />;
- };
- export default SyncedItem;
|