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", "parser": "babel-eslint",
"rules": { "rules": {
"semi": "error", "semi": "error",
"react/prop-types": "off" "react/prop-types": "off",
"react-hooks/exhaustive-deps": [
"warn", {
"additionalHooks": "useRecoilCallback"
}
]
}, },
"plugins": ["react-hooks"], "plugins": ["react-hooks"],
"extends": [ "extends": [

View file

@ -1,74 +1,89 @@
import React from "react"; import React from "react";
import { useRecoilValue } from "recoil";
import { PanZoomRotateAtom } from "./PanZoomRotate"; import { PanZoomRotateAtom } from "./PanZoomRotate";
import { selectedItemsAtom } from "./Selector"; import { selectedItemsAtom } from "./Selector";
import { useItems } from "./Items"; import { useItems } from "./Items";
import { useRecoilState } from "recoil"; import { useSetRecoilState, useRecoilCallback } from "recoil";
import { insideClass, hasClass } from "../../utils"; import { insideClass, hasClass } from "../../utils";
const ActionPane = ({ children }) => { const ActionPane = ({ children }) => {
const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
const { putItemsOnTop, moveItems } = useItems(); const { putItemsOnTop, moveItems } = useItems();
const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsAtom); const setSelectedItems = useSetRecoilState(selectedItemsAtom);
const wrapperRef = React.useRef(null); const wrapperRef = React.useRef(null);
const actionRef = React.useRef({}); const actionRef = React.useRef({});
const onMouseDown = (e) => { const onMouseDown = useRecoilCallback(
if (e.button === 0 && !e.altKey) { async (snapshot, e) => {
// Allow text selection instead of moving if (e.button === 0 && !e.altKey) {
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return; // Allow text selection instead of moving
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
const { top, left } = e.currentTarget.getBoundingClientRect(); const { top, left } = e.currentTarget.getBoundingClientRect();
const point = { const { clientX, clientY, ctrlKey, metaKey } = e;
x: (e.clientX - left) / panZoomRotate.scale,
y: (e.clientY - top) / panZoomRotate.scale,
};
const foundElement = insideClass(e.target, "item"); const foundElement = insideClass(e.target, "item");
if (foundElement && !hasClass(foundElement, "locked")) { const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
let selectedItemsToMove = selectedItems; const selectedItems = await snapshot.getPromise(selectedItemsAtom);
if (!selectedItems.includes(foundElement.id)) { const point = {
if (e.ctrlKey || e.metaKey) { x: (clientX - left) / panZoomRotate.scale,
setSelectedItems((prev) => [...prev, foundElement.id]); y: (clientY - top) / panZoomRotate.scale,
selectedItemsToMove = [...selectedItems, foundElement.id]; };
} else {
setSelectedItems([foundElement.id]); if (foundElement && !hasClass(foundElement, "locked")) {
selectedItemsToMove = [foundElement.id]; 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) => { const onMouseMouve = useRecoilCallback(
if (actionRef.current.moving) { async (snapshot, e) => {
const { top, left } = e.currentTarget.getBoundingClientRect(); if (actionRef.current.moving) {
const currentX = (e.clientX - left) / panZoomRotate.scale; const { top, left } = e.currentTarget.getBoundingClientRect();
const currentY = (e.clientY - top) / panZoomRotate.scale; const { clientX, clientY } = e;
moveItems(selectedItems, {
x: currentX - actionRef.current.prevX, const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
y: currentY - actionRef.current.prevY, const selectedItems = await snapshot.getPromise(selectedItemsAtom);
});
actionRef.current.prevX = currentX; const currentX = (clientX - left) / panZoomRotate.scale;
actionRef.current.prevY = currentY; const currentY = (clientY - top) / panZoomRotate.scale;
e.preventDefault();
} 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(() => { const onMouseUp = React.useCallback(() => {
if (actionRef.current.moving) { 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; display: inline-block;
transition: transform 200ms; transition: transform 200ms;
user-select: none; user-select: none;
@ -70,8 +67,7 @@ const ItemWrapper = styled.div.attrs(({ rotation, loaded, locked }) => {
`} `}
`; `;
const Item = ({ setState, state }) => { const Item = ({ setState, state, isSelected }) => {
const selectedItems = useRecoilValue(selectedItemsAtom);
const itemRef = React.useRef(null); const itemRef = React.useRef(null);
const sizeRef = React.useRef({}); const sizeRef = React.useRef({});
const [unlock, setUnlock] = React.useState(false); const [unlock, setUnlock] = React.useState(false);
@ -160,12 +156,15 @@ const Item = ({ setState, state }) => {
style={{ style={{
transform: `translate(${state.x}px, ${state.y}px)`, transform: `translate(${state.x}px, ${state.y}px)`,
display: "inline-block", display: "inline-block",
position: "absolute",
top: 0,
left: 0,
}} }}
> >
<ItemWrapper <ItemWrapper
rotation={rotation} rotation={rotation}
locked={state.locked && !unlock} locked={state.locked && !unlock}
selected={selectedItems.includes(state.id)} selected={isSelected}
ref={itemRef} ref={itemRef}
layer={state.layer} layer={state.layer}
loaded={loaded} loaded={loaded}
@ -177,15 +176,29 @@ const Item = ({ setState, state }) => {
); );
}; };
export default memo( const MemoizedItem = memo(
Item, Item,
( (
{ state: prevState, setState: prevSetState }, { state: prevState, setState: prevSetState, isSelected: prevIsSelected },
{ state: nextState, setState: nextSetState } { state: nextState, setState: nextSetState, isSelected: nextIsSelected }
) => { ) => {
return ( return (
JSON.stringify(prevState) === JSON.stringify(nextState) && 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 { import {
atom, atom,
useRecoilValue, useRecoilValue,
useRecoilState, useSetRecoilState,
useRecoilCallback, useRecoilCallback,
} from "recoil"; } from "recoil";
import styled from "styled-components"; import styled from "styled-components";
@ -31,21 +31,19 @@ const findSelected = (items, rect) => {
const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({ const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({
style: { style: {
top: `${top}px`, transform: `translate(${left}px, ${top}px)`,
left: `${left}px`,
height: `${height}px`, height: `${height}px`,
width: `${width}px`, width: `${width}px`,
}, },
}))` }))`
z-index: 100; z-index: 100;
position: absolute; position: absolute;
background-color: #ff000050; background-color: hsla(0, 40%, 50%, 10%);
border: 2px solid hsl(0, 55%, 40%);
`; `;
const Selector = ({ children }) => { const Selector = ({ children }) => {
const panZoomRotate = useRecoilValue(PanZoomRotateAtom); const setSelected = useSetRecoilState(selectedItemsAtom);
const [selected, setSelected] = useRecoilState(selectedItemsAtom);
const [selector, setSelector] = React.useState({}); const [selector, setSelector] = React.useState({});
const wrapperRef = React.useRef(null); const wrapperRef = React.useRef(null);
@ -60,15 +58,18 @@ const Selector = ({ children }) => {
setSelected([]); setSelected([]);
}, [config, setSelected]); }, [config, setSelected]);
const onMouseDown = (e) => { const onMouseDown = useRecoilCallback(async (snapshot, e) => {
if ( if (
e.button === 0 && e.button === 0 &&
!e.altKey && !e.altKey &&
(!insideClass(e.target, "item") || insideClass(e.target, "locked")) (!insideClass(e.target, "item") || insideClass(e.target, "locked"))
) { ) {
const { top, left } = e.currentTarget.getBoundingClientRect(); const { top, left } = e.currentTarget.getBoundingClientRect();
const displayX = (e.clientX - left) / panZoomRotate.scale; const { clientX, clientY } = e;
const displayY = (e.clientY - top) / panZoomRotate.scale;
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
const displayX = (clientX - left) / panZoomRotate.scale;
const displayY = (clientY - top) / panZoomRotate.scale;
stateRef.current.moving = true; stateRef.current.moving = true;
stateRef.current.startX = displayX; stateRef.current.startX = displayX;
@ -77,15 +78,40 @@ const Selector = ({ children }) => {
setSelector({ ...stateRef.current }); setSelector({ ...stateRef.current });
wrapperRef.current.style.cursor = "crosshair"; 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 (stateRef.current.moving) {
if (selected.length) setSelected([]);
const { top, left } = e.currentTarget.getBoundingClientRect(); const { top, left } = e.currentTarget.getBoundingClientRect();
const currentX = (e.clientX - left) / panZoomRotate.scale; const { clientX, clientY } = e;
const currentY = (e.clientY - top) / panZoomRotate.scale; 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) { if (currentX > stateRef.current.startX) {
stateRef.current.left = stateRef.current.startX; stateRef.current.left = stateRef.current.startX;
@ -103,9 +129,8 @@ const Selector = ({ children }) => {
} }
setSelector({ ...stateRef.current }); setSelector({ ...stateRef.current });
e.preventDefault();
} }
}; }, []);
const onMouseUp = useRecoilCallback( const onMouseUp = useRecoilCallback(
async (snapshot) => { async (snapshot) => {
@ -133,9 +158,9 @@ const Selector = ({ children }) => {
return ( return (
<div <div
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onMouseMove={onMouseMouve} onMouseMove={onMouseMove}
onMouseEnter={onMouseMouve} onMouseEnter={onMouseMove}
onMouseOut={onMouseMouve} onMouseOut={onMouseMove}
ref={wrapperRef} ref={wrapperRef}
> >
{selector.moving && ( {selector.moving && (

View file

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