SelectedItems.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import React from 'react';
  2. import { useRecoilState, useRecoilValue } from 'recoil';
  3. import { ItemListAtom } from '../components/Items';
  4. import { selectedItemsAtom } from '../components/Selector';
  5. import { shuffle as shuffleArray } from '../utils';
  6. import { useC2C } from '../hooks/useC2C';
  7. import Slider from 'rc-slider';
  8. import 'rc-slider/assets/index.css';
  9. export const SelectedItems = () => {
  10. const [c2c] = useC2C();
  11. const [itemList, setItemList] = useRecoilState(ItemListAtom);
  12. const selectedItems = useRecoilValue(selectedItemsAtom);
  13. const selectedItemList = React.useMemo(() => {
  14. return itemList.filter(({ id }) => selectedItems.includes(id));
  15. }, [itemList, selectedItems]);
  16. const updateItem = React.useCallback(
  17. (id, callbackOrItem) => {
  18. let callback = callbackOrItem;
  19. if (typeof callbackOrItem === 'object') {
  20. callback = (item) => callbackOrItem;
  21. }
  22. setItemList((prevList) => {
  23. return prevList.map((item) => {
  24. if (item.id === id) {
  25. const newItem = {
  26. ...callback(item),
  27. id: item.id,
  28. };
  29. c2c.publish(`itemStateUpdate.${item.id}`, newItem);
  30. return newItem;
  31. }
  32. return item;
  33. });
  34. });
  35. },
  36. [c2c, setItemList]
  37. );
  38. const massUpdateItems = React.useCallback(
  39. (ids, callbackOrItem) => {
  40. let callback = callbackOrItem;
  41. if (typeof callbackOrItem === 'object') {
  42. callback = (item) => callbackOrItem;
  43. }
  44. setItemList((prevList) => {
  45. return prevList.map((item) => {
  46. if (ids.includes(item.id)) {
  47. const newItem = {
  48. ...callback(item),
  49. id: item.id,
  50. };
  51. c2c.publish(`itemStateUpdate.${item.id}`, newItem);
  52. return newItem;
  53. }
  54. return item;
  55. });
  56. });
  57. },
  58. [c2c, setItemList]
  59. );
  60. // Shuffle selection
  61. const shuffle = React.useCallback(() => {
  62. setItemList((prevItemList) => {
  63. const shuffledSelectedItems = shuffleArray(
  64. prevItemList.filter(({ id }) => selectedItems.includes(id))
  65. );
  66. const result = prevItemList.map((item) => {
  67. if (selectedItems.includes(item.id)) {
  68. const newItem = {
  69. ...shuffledSelectedItems.pop(),
  70. x: item.x,
  71. y: item.y,
  72. };
  73. c2c.publish(`itemStateUpdate.${newItem.id}`, newItem);
  74. return newItem;
  75. }
  76. return item;
  77. });
  78. c2c.publish(
  79. `updateItemListOrder`,
  80. result.map(({ id }) => id)
  81. );
  82. return result;
  83. });
  84. }, [c2c, setItemList, selectedItems]);
  85. // Align selection to center
  86. const align = React.useCallback(() => {
  87. // Compute
  88. const minMax = { min: {}, max: {} };
  89. minMax.min.x = Math.min(...selectedItemList.map(({ x }) => x));
  90. minMax.min.y = Math.min(...selectedItemList.map(({ y }) => y));
  91. minMax.max.x = Math.max(
  92. ...selectedItemList.map(({ x, actualWidth }) => x + actualWidth)
  93. );
  94. minMax.max.y = Math.max(
  95. ...selectedItemList.map(({ y, actualHeight }) => y + actualHeight)
  96. );
  97. const [newX, newY] = [
  98. (minMax.min.x + minMax.max.x) / 2,
  99. (minMax.min.y + minMax.max.y) / 2,
  100. ];
  101. let index = -1;
  102. massUpdateItems(selectedItems, (item) => {
  103. index += 1;
  104. return {
  105. ...item,
  106. x: newX - item.actualWidth / 2 + index,
  107. y: newY - item.actualHeight / 2 - index,
  108. };
  109. });
  110. }, [selectedItemList, selectedItems, massUpdateItems]);
  111. const flip = React.useCallback(() => {
  112. massUpdateItems(selectedItems, (item) => ({
  113. ...item,
  114. flipped: true,
  115. }));
  116. }, [selectedItems, massUpdateItems]);
  117. const unflip = React.useCallback(() => {
  118. massUpdateItems(selectedItems, (item) => ({
  119. ...item,
  120. flipped: false,
  121. }));
  122. }, [selectedItems, massUpdateItems]);
  123. if (selectedItemList.length === 0) {
  124. return null;
  125. }
  126. return (
  127. <div
  128. style={{
  129. position: 'fixed',
  130. right: '1em',
  131. bottom: '1em',
  132. background: '#ffffff77',
  133. padding: '0.2em',
  134. maxHeight: '50vh',
  135. overflowY: 'scroll',
  136. }}
  137. >
  138. {selectedItems.length > 1 && (
  139. <div>
  140. <h2>{selectedItems.length} items selected</h2>
  141. <button onClick={shuffle}>Shuffle</button>
  142. <button onClick={align}>Stack</button>
  143. <button onClick={flip}>Flip</button>
  144. <button onClick={unflip}>UnFlip</button>
  145. </div>
  146. )}
  147. {selectedItems.length === 1 && (
  148. <ul
  149. style={{
  150. listStyle: 'none',
  151. }}
  152. >
  153. {selectedItemList.map(({ id, ...state }, index) => (
  154. <li
  155. key={id}
  156. style={{
  157. display: 'flex',
  158. flexDirection: 'column',
  159. width: '25em',
  160. }}
  161. >
  162. <h2 style={{ lineHeight: '30px' }}>{index}</h2>
  163. <label>
  164. Locked:
  165. <input
  166. type="checkbox"
  167. checked={Boolean(state.locked)}
  168. onChange={(e) =>
  169. updateItem(id, (item) => ({
  170. ...item,
  171. locked: !item.locked,
  172. }))
  173. }
  174. />
  175. </label>
  176. <label>
  177. Rotation:
  178. <Slider
  179. defaultValue={0}
  180. value={state.rotation}
  181. min={-180}
  182. max={180}
  183. step={5}
  184. included={false}
  185. marks={{
  186. '-45': -45,
  187. '-30': -30,
  188. 0: 0,
  189. 30: 30,
  190. 45: 45,
  191. 90: 90,
  192. }}
  193. onChange={(value) =>
  194. updateItem(id, (item) => ({
  195. ...item,
  196. rotation: parseInt(value, 10),
  197. }))
  198. }
  199. />
  200. </label>
  201. </li>
  202. ))}
  203. </ul>
  204. )}
  205. </div>
  206. );
  207. };
  208. export default SelectedItems;