Show selected items while selecting

This commit is contained in:
Jeremie Pardou-Piquemal 2020-07-28 19:54:16 +02:00 committed by Jérémie Pardou-Piquemal
parent 7eda777b73
commit 2653008a67
5 changed files with 153 additions and 84 deletions

View file

@ -21,7 +21,12 @@
"parser": "babel-eslint",
"rules": {
"semi": "error",
"react/prop-types": "off"
"react/prop-types": "off",
"react-hooks/exhaustive-deps": [
"warn", {
"additionalHooks": "useRecoilCallback"
}
]
},
"plugins": ["react-hooks"],
"extends": [

View file

@ -1,74 +1,89 @@
import React from "react";
import { useRecoilValue } from "recoil";
import { PanZoomRotateAtom } from "./PanZoomRotate";
import { selectedItemsAtom } from "./Selector";
import { useItems } from "./Items";
import { useRecoilState } from "recoil";
import { useSetRecoilState, useRecoilCallback } from "recoil";
import { insideClass, hasClass } from "../../utils";
const ActionPane = ({ children }) => {
const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
const { putItemsOnTop, moveItems } = useItems();
const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsAtom);
const setSelectedItems = useSetRecoilState(selectedItemsAtom);
const wrapperRef = React.useRef(null);
const actionRef = React.useRef({});
const onMouseDown = (e) => {
if (e.button === 0 && !e.altKey) {
// Allow text selection instead of moving
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
const onMouseDown = useRecoilCallback(
async (snapshot, e) => {
if (e.button === 0 && !e.altKey) {
// Allow text selection instead of moving
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
const { top, left } = e.currentTarget.getBoundingClientRect();
const point = {
x: (e.clientX - left) / panZoomRotate.scale,
y: (e.clientY - top) / panZoomRotate.scale,
};
const { top, left } = e.currentTarget.getBoundingClientRect();
const { clientX, clientY, ctrlKey, metaKey } = e;
const foundElement = insideClass(e.target, "item");
const foundElement = insideClass(e.target, "item");
if (foundElement && !hasClass(foundElement, "locked")) {
let selectedItemsToMove = selectedItems;
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
const selectedItems = await snapshot.getPromise(selectedItemsAtom);
if (!selectedItems.includes(foundElement.id)) {
if (e.ctrlKey || e.metaKey) {
setSelectedItems((prev) => [...prev, foundElement.id]);
selectedItemsToMove = [...selectedItems, foundElement.id];
} else {
setSelectedItems([foundElement.id]);
selectedItemsToMove = [foundElement.id];
const point = {
x: (clientX - left) / panZoomRotate.scale,
y: (clientY - top) / panZoomRotate.scale,
};
if (foundElement && !hasClass(foundElement, "locked")) {
let selectedItemsToMove = selectedItems;
if (!selectedItems.includes(foundElement.id)) {
if (ctrlKey || metaKey) {
setSelectedItems((prev) => [...prev, foundElement.id]);
selectedItemsToMove = [...selectedItems, foundElement.id];
} else {
setSelectedItems([foundElement.id]);
selectedItemsToMove = [foundElement.id];
}
}
putItemsOnTop(selectedItemsToMove);
actionRef.current.moving = true;
actionRef.current.startX = point.x;
actionRef.current.startY = point.y;
actionRef.current.prevX = point.x;
actionRef.current.prevY = point.y;
actionRef.current.moving = true;
actionRef.current.itemId = foundElement.id;
wrapperRef.current.style.cursor = "move";
}
putItemsOnTop(selectedItemsToMove);
actionRef.current.moving = true;
actionRef.current.startX = point.x;
actionRef.current.startY = point.y;
actionRef.current.prevX = point.x;
actionRef.current.prevY = point.y;
actionRef.current.moving = true;
actionRef.current.itemId = foundElement.id;
wrapperRef.current.style.cursor = "move";
e.stopPropagation();
}
}
};
},
[putItemsOnTop, setSelectedItems]
);
const onMouseMouve = (e) => {
if (actionRef.current.moving) {
const { top, left } = e.currentTarget.getBoundingClientRect();
const currentX = (e.clientX - left) / panZoomRotate.scale;
const currentY = (e.clientY - top) / panZoomRotate.scale;
moveItems(selectedItems, {
x: currentX - actionRef.current.prevX,
y: currentY - actionRef.current.prevY,
});
actionRef.current.prevX = currentX;
actionRef.current.prevY = currentY;
e.preventDefault();
}
};
const onMouseMouve = useRecoilCallback(
async (snapshot, e) => {
if (actionRef.current.moving) {
const { top, left } = e.currentTarget.getBoundingClientRect();
const { clientX, clientY } = e;
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
const selectedItems = await snapshot.getPromise(selectedItemsAtom);
const currentX = (clientX - left) / panZoomRotate.scale;
const currentY = (clientY - top) / panZoomRotate.scale;
moveItems(selectedItems, {
x: currentX - actionRef.current.prevX,
y: currentY - actionRef.current.prevY,
});
actionRef.current.prevX = currentX;
actionRef.current.prevY = currentY;
}
},
[moveItems]
);
const onMouseUp = React.useCallback(() => {
if (actionRef.current.moving) {

View file

@ -24,9 +24,6 @@ const ItemWrapper = styled.div.attrs(({ rotation, loaded, locked }) => {
},
};
})`
position: absolute;
top: 0;
left: 0;
display: inline-block;
transition: transform 200ms;
user-select: none;
@ -70,8 +67,7 @@ const ItemWrapper = styled.div.attrs(({ rotation, loaded, locked }) => {
`}
`;
const Item = ({ setState, state }) => {
const selectedItems = useRecoilValue(selectedItemsAtom);
const Item = ({ setState, state, isSelected }) => {
const itemRef = React.useRef(null);
const sizeRef = React.useRef({});
const [unlock, setUnlock] = React.useState(false);
@ -160,12 +156,15 @@ const Item = ({ setState, state }) => {
style={{
transform: `translate(${state.x}px, ${state.y}px)`,
display: "inline-block",
position: "absolute",
top: 0,
left: 0,
}}
>
<ItemWrapper
rotation={rotation}
locked={state.locked && !unlock}
selected={selectedItems.includes(state.id)}
selected={isSelected}
ref={itemRef}
layer={state.layer}
loaded={loaded}
@ -177,15 +176,29 @@ const Item = ({ setState, state }) => {
);
};
export default memo(
const MemoizedItem = memo(
Item,
(
{ state: prevState, setState: prevSetState },
{ state: nextState, setState: nextSetState }
{ state: prevState, setState: prevSetState, isSelected: prevIsSelected },
{ state: nextState, setState: nextSetState, isSelected: nextIsSelected }
) => {
return (
JSON.stringify(prevState) === JSON.stringify(nextState) &&
prevSetState === nextSetState
prevSetState === nextSetState &&
prevIsSelected === nextIsSelected
);
}
);
const BaseItem = ({ setState, state }) => {
const selectedItems = useRecoilValue(selectedItemsAtom);
return (
<MemoizedItem
state={state}
setState={setState}
isSelected={selectedItems.includes(state.id)}
/>
);
};
export default BaseItem;

View file

@ -3,7 +3,7 @@ import React from "react";
import {
atom,
useRecoilValue,
useRecoilState,
useSetRecoilState,
useRecoilCallback,
} from "recoil";
import styled from "styled-components";
@ -31,21 +31,19 @@ const findSelected = (items, rect) => {
const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({
style: {
top: `${top}px`,
left: `${left}px`,
transform: `translate(${left}px, ${top}px)`,
height: `${height}px`,
width: `${width}px`,
},
}))`
z-index: 100;
position: absolute;
background-color: #ff000050;
background-color: hsla(0, 40%, 50%, 10%);
border: 2px solid hsl(0, 55%, 40%);
`;
const Selector = ({ children }) => {
const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
const [selected, setSelected] = useRecoilState(selectedItemsAtom);
const setSelected = useSetRecoilState(selectedItemsAtom);
const [selector, setSelector] = React.useState({});
const wrapperRef = React.useRef(null);
@ -60,15 +58,18 @@ const Selector = ({ children }) => {
setSelected([]);
}, [config, setSelected]);
const onMouseDown = (e) => {
const onMouseDown = useRecoilCallback(async (snapshot, e) => {
if (
e.button === 0 &&
!e.altKey &&
(!insideClass(e.target, "item") || insideClass(e.target, "locked"))
) {
const { top, left } = e.currentTarget.getBoundingClientRect();
const displayX = (e.clientX - left) / panZoomRotate.scale;
const displayY = (e.clientY - top) / panZoomRotate.scale;
const { clientX, clientY } = e;
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
const displayX = (clientX - left) / panZoomRotate.scale;
const displayY = (clientY - top) / panZoomRotate.scale;
stateRef.current.moving = true;
stateRef.current.startX = displayX;
@ -77,15 +78,40 @@ const Selector = ({ children }) => {
setSelector({ ...stateRef.current });
wrapperRef.current.style.cursor = "crosshair";
}
};
}, []);
const onMouseMouve = (e) => {
const throttledSetSelected = useRecoilCallback(
async (snapshot, selector) => {
if (stateRef.current.moving) {
const itemList = await snapshot.getPromise(ItemListAtom);
const selected = findSelected(itemList, selector).map(({ id }) => id);
setSelected((prevSelected) => {
if (JSON.stringify(prevSelected) !== JSON.stringify(selected)) {
return selected;
}
return prevSelected;
});
}
},
[setSelected]
);
// Reset selection on game loading
React.useEffect(() => {
throttledSetSelected(selector);
}, [selector, throttledSetSelected]);
const onMouseMove = useRecoilCallback(async (snapshot, e) => {
if (stateRef.current.moving) {
if (selected.length) setSelected([]);
const { top, left } = e.currentTarget.getBoundingClientRect();
const currentX = (e.clientX - left) / panZoomRotate.scale;
const currentY = (e.clientY - top) / panZoomRotate.scale;
const { clientX, clientY } = e;
e.preventDefault();
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
const currentX = (clientX - left) / panZoomRotate.scale;
const currentY = (clientY - top) / panZoomRotate.scale;
if (currentX > stateRef.current.startX) {
stateRef.current.left = stateRef.current.startX;
@ -103,9 +129,8 @@ const Selector = ({ children }) => {
}
setSelector({ ...stateRef.current });
e.preventDefault();
}
};
}, []);
const onMouseUp = useRecoilCallback(
async (snapshot) => {
@ -133,9 +158,9 @@ const Selector = ({ children }) => {
return (
<div
onMouseDown={onMouseDown}
onMouseMove={onMouseMouve}
onMouseEnter={onMouseMouve}
onMouseOut={onMouseMouve}
onMouseMove={onMouseMove}
onMouseEnter={onMouseMove}
onMouseOut={onMouseMove}
ref={wrapperRef}
>
{selector.moving && (

View file

@ -40,9 +40,19 @@ const genGame = () => {
x: 420,
y: 400,
});
items.push({
type: "rect",
color: "#00D022",
width: 100,
height: 100,
x: 0,
y: 0,
});
items.push({
type: "rect",
color: "#22D022",
width: 80,
height: 80,
x: 10,
@ -87,6 +97,7 @@ const genGame = () => {
layer: -1,
width: 500,
height: 300,
locked: true,
x: 200,
y: 600,
});