Add ability to shuffle and align items
This commit is contained in:
parent
0e087ed1c3
commit
2539ba7c14
4 changed files with 176 additions and 50 deletions
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue