Add shortcut to actions

This commit is contained in:
Jeremie Pardou-Piquemal 2020-06-30 21:47:55 +02:00 committed by Jérémie Pardou-Piquemal
parent cf4e68ca34
commit 03dab656aa
3 changed files with 113 additions and 85 deletions

View file

@ -29,7 +29,7 @@ const Wrapper = styled.div`
position: relative; position: relative;
`; `;
const StyledImage = styled.img` const FrontImage = styled.img`
transition: transform 200ms; transition: transform 200ms;
transform: rotateY(${({ visible }) => (visible ? 0 : 180)}deg); transform: rotateY(${({ visible }) => (visible ? 0 : 180)}deg);
backface-visibility: hidden; backface-visibility: hidden;
@ -37,7 +37,7 @@ const StyledImage = styled.img`
z-index: -1; z-index: -1;
`; `;
const BackImage = styled(StyledImage)` const BackImage = styled(FrontImage)`
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -86,7 +86,7 @@ const Image = ({
return { return {
...prevItem, ...prevItem,
unflippedFor: currentUser.id, unflippedFor: currentUser.id,
flipped: false, flipped: true,
}; };
} }
}); });
@ -102,8 +102,7 @@ const Image = ({
); );
const flippedForMe = const flippedForMe =
backContent && backContent && flipped && unflippedFor !== currentUser.id;
(flipped || (unflippedFor && unflippedFor !== currentUser.id));
return ( return (
<Wrapper onDoubleClick={onDblClick}> <Wrapper onDoubleClick={onDblClick}>
@ -118,7 +117,7 @@ const Image = ({
)} )}
{flippedForMe && backText && <Label>{backText}</Label>} {flippedForMe && backText && <Label>{backText}</Label>}
{(!flippedForMe || !backText) && text && <Label>{text}</Label>} {(!flippedForMe || !backText) && text && <Label>{text}</Label>}
<StyledImage <FrontImage
visible={!flippedForMe} visible={!flippedForMe}
src={content} src={content}
alt="" alt=""

View file

@ -4,6 +4,8 @@ import { useRecoilValue } from "recoil";
import { selectedItemsAtom } from "../../Selector"; import { selectedItemsAtom } from "../../Selector";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import styled, { css } from "styled-components";
import Rect from "./Rect"; import Rect from "./Rect";
import Round from "./Round"; import Round from "./Round";
import Image from "./Image"; import Image from "./Image";
@ -30,21 +32,47 @@ const getComponent = (type) => {
} }
}; };
const ItemWrapper = styled.div.attrs(({ x, y, rotation }) => ({
className: "item",
style: { left: `${x}px`, top: `${y}px`, transform: `rotate(${rotation}deg)` },
}))`
position: absolute;
display: inline-block;
transition: transform 200ms;
z-index: ${({ layer }) => (layer || 0) + 3};
${({ selected }) =>
selected
? css`
border: 2px dashed #ff0000a0;
padding: 2px;
cursor: pointer;
`
: css`
padding: 4px;
`}
${({ locked }) =>
locked &&
css`
pointer-events: none;
user-select: none;
`}
`;
const Item = ({ setState, state }) => { const Item = ({ setState, state }) => {
const selectedItems = useRecoilValue(selectedItemsAtom); 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);
// Allow to operate on locked item if ctrl is pressed // Allow to operate on locked item if key is pressed
React.useEffect(() => { React.useEffect(() => {
const onKeyDown = (e) => { const onKeyDown = (e) => {
if (e.key === "Control") { if (e.key === "u" || e.key === "l") {
setUnlock(true); setUnlock(true);
} }
}; };
const onKeyUp = (e) => { const onKeyUp = (e) => {
if (e.key === "Control") { if (e.key === "u" || e.key === "l") {
setUnlock(false); setUnlock(false);
} }
}; };
@ -65,6 +93,7 @@ const Item = ({ setState, state }) => {
[setState, state.id] [setState, state.id]
); );
// Update actual dimension. Usefull when image with own dimensions.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const actualSizeCallback = React.useCallback( const actualSizeCallback = React.useCallback(
debounce((entries) => { debounce((entries) => {
@ -102,43 +131,18 @@ const Item = ({ setState, state }) => {
}; };
}, [actualSizeCallback]); }, [actualSizeCallback]);
const extraStyle = selectedItems.includes(state.id) return (
? { border: "2px dashed #ff0000a0", padding: "2px" } <ItemWrapper
: { padding: "4px" }; x={state.x}
y={state.y}
const content = ( rotation={rotation}
<div locked={state.locked && !unlock}
style={{ selected={selectedItems.includes(state.id)}
left: state.x + "px",
top: state.y + "px",
position: "absolute",
display: "inline-block",
transform: `rotate(${rotation}deg)`,
transition: "transform 200ms",
zIndex: (state.layer || 0) + 3,
...extraStyle,
}}
className="item"
ref={itemRef} ref={itemRef}
id={state.id} id={state.id}
> >
<Component {...state} x={0} y={0} setState={updateState} /> <Component {...state} x={0} y={0} setState={updateState} />
</div> </ItemWrapper>
);
if (!state.locked || unlock) {
return content;
}
return (
<div
style={{
pointerEvents: "none",
userSelect: "none",
}}
>
{content}
</div>
); );
}; };

View file

@ -5,11 +5,15 @@ import { useRecoilValue } from "recoil";
import { useItems } from "./Board/Items"; import { useItems } from "./Board/Items";
import { selectedItemsAtom } from "../components/Board/Selector"; import { selectedItemsAtom } from "../components/Board/Selector";
import { insideClass } from "../utils";
import ItemFormFactory from "./Board/Items/Item/forms/ItemFormFactory"; import ItemFormFactory from "./Board/Items/Item/forms/ItemFormFactory";
import { confirmAlert } from "react-confirm-alert"; import { confirmAlert } from "react-confirm-alert";
import "react-confirm-alert/src/react-confirm-alert.css"; import "react-confirm-alert/src/react-confirm-alert.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useUsers } from "./users";
const SelectedPane = styled.div.attrs(() => ({ className: "casrd" }))` const SelectedPane = styled.div.attrs(() => ({ className: "casrd" }))`
position: absolute; position: absolute;
@ -32,6 +36,8 @@ export const SelectedItems = ({ edit }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentUser } = useUsers();
const selectedItems = useRecoilValue(selectedItemsAtom); const selectedItems = useRecoilValue(selectedItemsAtom);
const selectedItemList = React.useMemo(() => { const selectedItemList = React.useMemo(() => {
@ -66,33 +72,21 @@ export const SelectedItems = ({ edit }) => {
}); });
}, [selectedItemList, selectedItems, batchUpdateItems]); }, [selectedItemList, selectedItems, batchUpdateItems]);
const flip = React.useCallback(() => {
batchUpdateItems(selectedItems, (item) => ({
...item,
flipped: true,
}));
}, [selectedItems, batchUpdateItems]);
const tap = React.useCallback(() => {
batchUpdateItems(selectedItems, (item) => ({
...item,
rotation: 90,
}));
}, [selectedItems, batchUpdateItems]);
const untap = React.useCallback(() => {
batchUpdateItems(selectedItems, (item) => ({
...item,
rotation: 0,
}));
}, [selectedItems, batchUpdateItems]);
const toggleTap = React.useCallback(() => { const toggleTap = React.useCallback(() => {
const tappedCount = selectedItemList.filter(
({ rotation }) => rotation === 90
).length;
let untap = false;
if (tappedCount > selectedItems.length / 2) {
untap = true;
}
batchUpdateItems(selectedItems, (item) => ({ batchUpdateItems(selectedItems, (item) => ({
...item, ...item,
rotation: item.rotation === 90 ? 0 : 90, rotation: untap ? 0 : 90,
})); }));
}, [selectedItems, batchUpdateItems]); }, [selectedItems, batchUpdateItems, selectedItemList]);
const toggleLock = React.useCallback(() => { const toggleLock = React.useCallback(() => {
batchUpdateItems(selectedItems, (item) => ({ batchUpdateItems(selectedItems, (item) => ({
@ -102,29 +96,62 @@ export const SelectedItems = ({ edit }) => {
}, [selectedItems, batchUpdateItems]); }, [selectedItems, batchUpdateItems]);
const toggleFlip = React.useCallback(() => { const toggleFlip = React.useCallback(() => {
batchUpdateItems(selectedItems, (item) => ({ const flippedCount = selectedItemList.filter(({ flipped }) => flipped)
...item, .length;
flipped: !item.flipped,
}));
}, [selectedItems, batchUpdateItems]);
const unflip = React.useCallback(() => { let flip = true;
batchUpdateItems(selectedItems, (item) => ({ if (flippedCount > selectedItems.length / 2) {
...item, flip = false;
flipped: false,
}));
}, [selectedItems, batchUpdateItems]);
if (selectedItemList.length === 0) {
return null;
} }
batchUpdateItems(selectedItems, (item) => ({
...item,
flipped: flip,
unflippedFor: undefined,
}));
}, [selectedItemList, selectedItems, batchUpdateItems]);
const onSubmitHandler = (formValues) => { const revealForMe = React.useCallback(() => {
batchUpdateItems(selectedItems, (item) => ({
...item,
flipped: true,
unflippedFor: currentUser.id,
}));
}, [batchUpdateItems, selectedItems, currentUser.id]);
React.useEffect(() => {
const onKeyUp = (e) => {
if (e.key === "f") {
if (insideClass(e.target, "item")) return;
toggleFlip();
}
if (e.key === "t") {
if (insideClass(e.target, "item")) return;
toggleTap();
}
if (e.key === "o") {
if (insideClass(e.target, "item")) return;
revealForMe();
}
};
document.addEventListener("keyup", onKeyUp);
return () => {
document.removeEventListener("keyup", onKeyUp);
};
}, [revealForMe, toggleFlip, toggleTap]);
const onSubmitHandler = React.useCallback(
(formValues) => {
updateItem(formValues.id, (item) => ({ updateItem(formValues.id, (item) => ({
...item, ...item,
...formValues, ...formValues,
})); }));
}; },
[updateItem]
);
if (selectedItemList.length === 0) {
return null;
}
const onRemove = () => { const onRemove = () => {
confirmAlert({ confirmAlert({
@ -195,12 +222,10 @@ export const SelectedItems = ({ edit }) => {
<h3>{t("items selected", { count: selectedItems.length })}</h3> <h3>{t("items selected", { count: selectedItems.length })}</h3>
</header> </header>
<section className="content"> <section className="content">
<button onClick={shuffleSelectedItems}>{t("Shuffle")}</button> <button onClick={toggleFlip}>{t("Reveal") + "/" + t("Hide")}</button>
<button onClick={toggleTap}>{t("Tap") + "/" + t("Untap")}</button>
<button onClick={align}>{t("Stack")}</button> <button onClick={align}>{t("Stack")}</button>
<button onClick={flip}>{t("Hide")}</button> <button onClick={shuffleSelectedItems}>{t("Shuffle")}</button>
<button onClick={unflip}>{t("Reveal")}</button>
<button onClick={tap}>{t("Tap")}</button>
<button onClick={untap}>{t("Untap")}</button>
{edit && <button onClick={onRemove}>{t("Remove all")}</button>} {edit && <button onClick={onRemove}>{t("Remove all")}</button>}
</section> </section>
</div> </div>