Add ability to shuffle and align items

This commit is contained in:
Jeremie Pardou-Piquemal 2020-06-14 15:04:01 +02:00
parent 0e087ed1c3
commit 2539ba7c14
4 changed files with 176 additions and 50 deletions

View file

@ -85,23 +85,6 @@ const ActionPane = ({ children }) => {
}
};
React.useEffect(() => {
const unsub = c2c.subscribe(`selectedItemsMove`, ({ itemIds, move }) => {
setItemList((prevList) => {
return prevList.map((item) => {
if (itemIds.includes(item.id)) {
const x = item.x + move.x;
const y = item.y + move.y;
const newItem = { ...item, x, y };
return newItem;
}
return item;
});
});
});
return unsub;
}, [c2c, setItemList]);
return (
<div
onMouseDown={onMouseDown}

View file

@ -35,6 +35,39 @@ const Items = ({}) => {
[setItemList, c2c]
);
React.useEffect(() => {
const unsub = c2c.subscribe(`selectedItemsMove`, ({ itemIds, move }) => {
setItemList((prevList) => {
return prevList.map((item) => {
if (itemIds.includes(item.id)) {
const x = item.x + move.x;
const y = item.y + move.y;
const newItem = { ...item, x, y };
return newItem;
}
return item;
});
});
});
return unsub;
}, [c2c, setItemList]);
React.useEffect(() => {
const unsub = c2c.subscribe(`updateItemListOrder`, (itemIds) => {
setItemList((prevList) => {
const itemsMap = prevList.reduce((prev, item) => {
prev[item.id] = item;
return prev;
}, {});
const result = prevList.map((item, index) => {
return itemsMap[itemIds[index]];
});
return result;
});
});
return unsub;
}, [c2c, setItemList]);
return itemList.map((item) => (
<Item key={item.id} state={item} setState={updateItem} />
));

View file

@ -3,8 +3,11 @@ import React from 'react';
import { useRecoilState } from 'recoil';
import { ItemListAtom } from '../components/Items';
import { selectedItemsAtom } from '../components/Selector';
import { shuffleSelectedItems } from '../utils';
import { useC2C } from '../hooks/useC2C';
export const SelectedItems = ({}) => {
const [c2c] = useC2C();
const [itemList, setItemList] = useRecoilState(ItemListAtom);
const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsAtom);
@ -21,62 +24,144 @@ export const SelectedItems = ({}) => {
setItemList((prevList) => {
return prevList.map((item) => {
if (item.id === id) {
return {
const newItem = {
...callback(item),
id: item.id,
};
c2c.publish(`itemStateUpdate.${item.id}`, newItem);
return newItem;
}
return item;
});
});
},
[setItemList]
[c2c, setItemList]
);
const massUpdateItems = React.useCallback(
(ids, callbackOrItem) => {
let callback = callbackOrItem;
if (typeof callbackOrItem === 'object') {
callback = (item) => callbackOrItem;
}
setItemList((prevList) => {
return prevList.map((item) => {
if (ids.includes(item.id)) {
const newItem = {
...callback(item),
id: item.id,
};
c2c.publish(`itemStateUpdate.${item.id}`, newItem);
return newItem;
}
return item;
});
});
},
[c2c, setItemList]
);
// Shuffle selection
const shuffle = React.useCallback(() => {
setItemList((prevItemList) => {
const result = shuffleSelectedItems(prevItemList, selectedItems);
c2c.publish(
`updateItemListOrder`,
result.map(({ id }) => id)
);
return result;
});
}, [c2c, setItemList, selectedItems]);
// Align selection to center
const align = React.useCallback(() => {
const minMax = { min: {}, max: {} };
minMax.min.x = Math.min(...selectedItemList.map(({ x }) => x));
minMax.min.y = Math.min(...selectedItemList.map(({ y }) => y));
minMax.max.x = Math.max(
...selectedItemList.map(({ x, actualWidth }) => x + actualWidth)
);
minMax.max.y = Math.max(
...selectedItemList.map(({ y, actualHeight }) => y + actualHeight)
);
const [newX, newY] = [
(minMax.min.x + minMax.max.x) / 2,
(minMax.min.y + minMax.max.y) / 2,
];
massUpdateItems(selectedItems, (item) => ({
...item,
x: newX - item.actualWidth / 2,
y: newY - item.actualHeight / 2,
}));
console.log(minMax);
}, [selectedItemList, selectedItems, massUpdateItems]);
if (selectedItemList.length === 0) {
return null;
}
return (
<ul
<div
style={{
position: 'fixed',
right: '1em',
bottom: '1em',
background: '#ffffff77',
padding: '0.2em',
listStyle: 'none',
maxHeight: '50vh',
overflowY: 'scroll',
}}
>
{selectedItemList.map(({ id, ...state }, index) => (
<li key={id} style={{}}>
<h2 style={{ lineHeight: '30px' }}>{index}</h2>
<label>
Locked:
<input
type='checkbox'
checked={Boolean(state.locked)}
onChange={(e) =>
updateItem(id, (item) => ({ ...item, locked: !item.locked }))
}
/>
</label>
<label>
Rotation:
<input
type='number'
value={state.rotation || 0}
onChange={(e) =>
updateItem(id, (item) => ({
...item,
rotation: parseInt(e.target.value, 10),
}))
}
/>
</label>
</li>
))}
</ul>
{selectedItems.length > 1 && (
<div>
<h2>{selectedItems.length} items selected</h2>
<button onClick={shuffle}>Shuffle selection</button>
<button onClick={align}>Align selection</button>
</div>
)}
{selectedItems.length === 1 && (
<ul
style={{
listStyle: 'none',
}}
>
{selectedItemList.map(({ id, ...state }, index) => (
<li key={id} style={{}}>
<h2 style={{ lineHeight: '30px' }}>{index}</h2>
<label>
Locked:
<input
type='checkbox'
checked={Boolean(state.locked)}
onChange={(e) =>
updateItem(id, (item) => ({
...item,
locked: !item.locked,
}))
}
/>
</label>
<label>
Rotation:
<input
type='number'
value={state.rotation || 0}
onChange={(e) =>
updateItem(id, (item) => ({
...item,
rotation: parseInt(e.target.value, 10),
}))
}
/>
</label>
</li>
))}
</ul>
)}
</div>
);
};

View file

@ -28,3 +28,28 @@ export const isPointInsideItem = (point, item) => {
height: item.actualHeight,
});
};
/**
* Shuffles array in place.
* @param {Array} a items An array containing the items.
*/
export const shuffle = (a) => {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
export const shuffleSelectedItems = (itemList, selectedItemIds) => {
const selectedItems = shuffle(
itemList.filter(({ id }) => selectedItemIds.includes(id))
);
return itemList.map((item) => {
if (selectedItemIds.includes(item.id)) {
return selectedItems.pop();
}
return item;
});
};