123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import React from "react";
- import {
- atom,
- useRecoilValue,
- useSetRecoilState,
- useRecoilCallback,
- } from "recoil";
- import styled from "styled-components";
- import {
- PanZoomRotateAtom,
- BoardConfigAtom,
- ItemMapAtom,
- BoardStateAtom,
- } from "./";
- import { insideClass, isPointInsideRect } from "../../utils";
- import throttle from "lodash.throttle";
- import Gesture from "./Gesture";
- export const selectedItemsAtom = atom({
- key: "selectedItems",
- default: [],
- });
- const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({
- className: "selector",
- style: {
- transform: `translate(${left}px, ${top}px)`,
- height: `${height}px`,
- width: `${width}px`,
- },
- }))`
- z-index: 210;
- position: absolute;
- background-color: hsla(0, 40%, 50%, 10%);
- border: 2px solid hsl(0, 55%, 40%);
- `;
- const findSelected = (itemMap) => {
- const selector = document.body.querySelector(".selector");
- if (!selector) {
- return [];
- }
- const rect = selector.getBoundingClientRect();
- return Array.from(document.getElementsByClassName("item"))
- .filter((elem) => {
- const { id } = elem;
- const item = itemMap[id];
- if (!item) {
- // Avoid to find item that are not yet removed from DOM
- console.error(`Missing item ${id}`);
- return false;
- }
- if (item.locked) {
- return false;
- }
- const fourElem = Array.from(elem.querySelectorAll(".corner"));
- return fourElem.every((corner) => {
- const { top: y, left: x } = corner.getBoundingClientRect();
- return isPointInsideRect({ x, y }, rect);
- });
- })
- .map((elem) => elem.id);
- };
- const Selector = ({ children, moveFirst }) => {
- const setSelected = useSetRecoilState(selectedItemsAtom);
- const setBoardState = useSetRecoilState(BoardStateAtom);
- const [selector, setSelector] = React.useState({});
- const [emptySelection] = React.useState([]);
- const wrapperRef = React.useRef(null);
- const stateRef = React.useRef({
- moving: false,
- });
- const config = useRecoilValue(BoardConfigAtom);
- // Reset selection on game loading
- React.useEffect(() => {
- setSelected(emptySelection);
- }, [config, emptySelection, setSelected]);
- const throttledSetSelected = useRecoilCallback(
- ({ snapshot }) =>
- throttle(async () => {
- if (stateRef.current.moving) {
- const itemMap = await snapshot.getPromise(ItemMapAtom);
- const selected = findSelected(itemMap);
- setSelected((prevSelected) => {
- if (JSON.stringify(prevSelected) !== JSON.stringify(selected)) {
- return selected;
- }
- return prevSelected;
- });
- }
- }, 300),
- [setSelected]
- );
- React.useEffect(() => {
- throttledSetSelected();
- }, [selector, throttledSetSelected]);
- // Reset selected on unmount
- React.useEffect(() => {
- return () => {
- setSelected(emptySelection);
- };
- }, [setSelected, emptySelection]);
- const onDragStart = ({ button, altKey, ctrlKey, metaKey, target }) => {
- const outsideItem =
- !insideClass(target, "item") || insideClass(target, "locked");
- const metaKeyPressed = altKey || ctrlKey || metaKey;
- const goodButton = moveFirst
- ? button === 1 || (button === 0 && metaKeyPressed)
- : button === 0 && !metaKeyPressed;
- if (goodButton && (outsideItem || moveFirst)) {
- stateRef.current.moving = true;
- setBoardState((prev) => ({ ...prev, selecting: true }));
- wrapperRef.current.style.cursor = "crosshair";
- }
- };
- const onDrag = useRecoilCallback(
- ({ snapshot }) => async ({ distanceY, distanceX, startX, startY }) => {
- if (stateRef.current.moving) {
- const { top, left } = wrapperRef.current.getBoundingClientRect();
- const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
- const displayX = (startX - left) / panZoomRotate.scale;
- const displayY = (startY - top) / panZoomRotate.scale;
- const displayDistanceX = distanceX / panZoomRotate.scale;
- const displayDistanceY = distanceY / panZoomRotate.scale;
- if (displayDistanceX > 0) {
- stateRef.current.left = displayX;
- stateRef.current.width = displayDistanceX;
- } else {
- stateRef.current.left = displayX + displayDistanceX;
- stateRef.current.width = -displayDistanceX;
- }
- if (displayDistanceY > 0) {
- stateRef.current.top = displayY;
- stateRef.current.height = displayDistanceY;
- } else {
- stateRef.current.top = displayY + displayDistanceY;
- stateRef.current.height = -displayDistanceY;
- }
- setSelector({ ...stateRef.current, moving: true });
- }
- },
- []
- );
- const onDragEnd = () => {
- if (stateRef.current.moving) {
- setBoardState((prev) => ({ ...prev, selecting: false }));
- stateRef.current.moving = false;
- setSelector({ moving: false });
- wrapperRef.current.style.cursor = "auto";
- }
- };
- const onLongTap = React.useCallback(
- ({ target }) => {
- const foundElement = insideClass(target, "item");
- if (foundElement) {
- setSelected([foundElement.id]);
- }
- },
- [setSelected]
- );
- const onTap = useRecoilCallback(
- ({ snapshot }) => async ({ target, ctrlKey, metaKey }) => {
- const foundItem = insideClass(target, "item");
- if (
- (!foundItem || insideClass(foundItem, "locked")) &&
- insideClass(target, "board")
- ) {
- setSelected(emptySelection);
- } else {
- const selectedItems = await snapshot.getPromise(selectedItemsAtom);
- if (foundItem && !selectedItems.includes(foundItem.id)) {
- if (ctrlKey || metaKey) {
- setSelected((prev) => [...prev, foundItem.id]);
- } else {
- setSelected([foundItem.id]);
- }
- }
- }
- },
- [emptySelection, setSelected]
- );
- return (
- <Gesture
- onDragStart={onDragStart}
- onDrag={onDrag}
- onDragEnd={onDragEnd}
- onTap={onTap}
- onLongTap={onLongTap}
- >
- <div ref={wrapperRef}>
- {selector.moving && <SelectorZone {...selector} />}
- {children}
- </div>
- </Gesture>
- );
- };
- export default Selector;
|