Show selected items while selecting
This commit is contained in:
parent
7eda777b73
commit
2653008a67
5 changed files with 153 additions and 84 deletions
|
@ -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": [
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue