actionMap.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. // import React from "react";
  2. // import { useTranslation } from "react-i18next";
  3. import i18n from "../i18n";
  4. import { nanoid } from "nanoid";
  5. import { toast } from "react-toastify";
  6. import { shuffle as shuffleArray, randInt } from "../views/utils";
  7. import deleteIcon from "../images/delete.svg";
  8. import stackToCenterIcon from "../images/stackToCenter.svg";
  9. import stackToTopLeftIcon from "../images/stackToTopLeft.svg";
  10. import alignAsLineIcon from "../images/alignAsLine.svg";
  11. import alignAsSquareIcon from "../images/alignAsSquare.svg";
  12. import duplicateIcon from "../images/duplicate.svg";
  13. import seeIcon from "../images/see.svg";
  14. import flipIcon from "../images/flip.svg";
  15. import lockIcon from "../images/lock.svg";
  16. import rotateIcon from "../images/rotate.svg";
  17. import shuffleIcon from "../images/shuffle.svg";
  18. import tapIcon from "../images/tap.svg";
  19. const stackToCenter = async (
  20. itemIds,
  21. {
  22. stackThicknessMin = 0.5,
  23. stackThicknessMax = 1,
  24. limitCardsNumber = 32,
  25. } = {},
  26. { getItem, batchUpdateItems }
  27. ) => {
  28. const items = itemIds.map(getItem);
  29. // Rule to manage thickness of the stack.
  30. let stackThickness = stackThicknessMax;
  31. if (items.length >= limitCardsNumber) {
  32. stackThickness = stackThicknessMin;
  33. }
  34. // To avoid displacement effects.
  35. let isSameGap = true;
  36. for (let i = 1; i < items.length; i++) {
  37. if (Math.abs(items[i].x - items[i - 1].x) != stackThickness) {
  38. isSameGap = false;
  39. break;
  40. }
  41. if (Math.abs(items[i].y - items[i - 1].y) != stackThickness) {
  42. isSameGap = false;
  43. break;
  44. }
  45. }
  46. if (isSameGap == true) {
  47. return;
  48. }
  49. // Compute middle position
  50. const minMax = { min: {}, max: {} };
  51. minMax.min.x = Math.min(...items.map(({ x }) => x));
  52. minMax.min.y = Math.min(...items.map(({ y }) => y));
  53. minMax.max.x = Math.max(
  54. ...items.map(({ x, id }) => x + document.getElementById(id).clientWidth)
  55. );
  56. minMax.max.y = Math.max(
  57. ...items.map(({ y, id }) => y + document.getElementById(id).clientHeight)
  58. );
  59. const { clientWidth, clientHeight } = document.getElementById(items[0].id);
  60. let newX = minMax.min.x + (minMax.max.x - minMax.min.x) / 2 - clientWidth / 2;
  61. let newY =
  62. minMax.min.y + (minMax.max.y - minMax.min.y) / 2 - clientHeight / 2;
  63. batchUpdateItems(itemIds, (item) => {
  64. const newItem = {
  65. ...item,
  66. x: newX,
  67. y: newY,
  68. };
  69. newX += stackThickness;
  70. newY -= stackThickness;
  71. return newItem;
  72. });
  73. };
  74. // Stack selection to Top Left
  75. export const stackToTopLeft = async (
  76. itemIds,
  77. {
  78. stackThicknessMin = 0.5,
  79. stackThicknessMax = 1,
  80. limitCardsNumber = 32,
  81. } = {},
  82. { getItem, batchUpdateItems }
  83. ) => {
  84. const items = await Promise.all(itemIds.map(getItem));
  85. let { x: newX, y: newY } = items[0];
  86. // Rule to manage thickness of the stack.
  87. let stackThickness = stackThicknessMax;
  88. if (items.length >= limitCardsNumber) {
  89. stackThickness = stackThicknessMin;
  90. }
  91. batchUpdateItems(itemIds, (item) => {
  92. const newItem = {
  93. ...item,
  94. x: newX,
  95. y: newY,
  96. };
  97. newX += stackThickness;
  98. newY -= stackThickness;
  99. return newItem;
  100. });
  101. };
  102. // Align selection to a line
  103. const alignAsLine = async (
  104. itemIds,
  105. { gapBetweenItems = 5 } = {},
  106. { getItem, batchUpdateItems }
  107. ) => {
  108. // Negative value is possible for 'gapBetweenItems'.
  109. let { x: newX, y: newY } = await getItem(itemIds[0]);
  110. batchUpdateItems(itemIds, (item) => {
  111. const { clientWidth } = document.getElementById(item.id);
  112. const newItem = {
  113. ...item,
  114. x: newX,
  115. y: newY,
  116. };
  117. newX += clientWidth + gapBetweenItems;
  118. return newItem;
  119. });
  120. };
  121. // Align selection to an array
  122. const alignAsSquare = async (
  123. itemIds,
  124. { gapBetweenItems = 5 } = {},
  125. { batchUpdateItems, getItem }
  126. ) => {
  127. // Negative value is possible for 'gapBetweenItems'.
  128. const items = await Promise.all(itemIds.map(getItem));
  129. // Count number of elements
  130. const numberOfElements = items.length;
  131. const numberOfColumns = Math.ceil(Math.sqrt(numberOfElements));
  132. let { x: newX, y: newY } = items[0];
  133. let currentColumn = 1;
  134. batchUpdateItems(itemIds, (item) => {
  135. const { clientWidth, clientHeight } = document.getElementById(item.id);
  136. const newItem = {
  137. ...item,
  138. x: newX,
  139. y: newY,
  140. };
  141. newX += clientWidth + gapBetweenItems;
  142. currentColumn += 1;
  143. if (currentColumn > numberOfColumns) {
  144. currentColumn = 1;
  145. newX = items[0].x;
  146. newY += clientHeight + gapBetweenItems;
  147. }
  148. return newItem;
  149. });
  150. };
  151. const shuffleItems = async (itemIds, _, { swapItems }) => {
  152. itemIds.forEach((itemId) => {
  153. const elem = document.getElementById(itemId);
  154. elem.firstChild.className = "hvr-wobble-horizontal";
  155. });
  156. const shuffledItems = shuffleArray([...itemIds]);
  157. swapItems(itemIds, shuffledItems);
  158. };
  159. const randomlyRotateSelectedItems = async (
  160. itemIds,
  161. { angle, maxRotateCount },
  162. { batchUpdateItems }
  163. ) => {
  164. batchUpdateItems(itemIds, (item) => {
  165. const rotation =
  166. ((item.rotation || 0) + angle * randInt(0, maxRotateCount)) % 360;
  167. return { ...item, rotation };
  168. });
  169. };
  170. // Tap/Untap elements
  171. const toggleTap = async (itemIds, _, { getItem, batchUpdateItems }) => {
  172. const items = await Promise.all(itemIds.map(getItem));
  173. const tappedCount = items.filter(({ rotation }) => rotation === 90).length;
  174. let untap = false;
  175. if (tappedCount > itemIds.length / 2) {
  176. untap = true;
  177. }
  178. batchUpdateItems(itemIds, (item) => ({
  179. ...item,
  180. rotation: untap ? 0 : 90,
  181. }));
  182. };
  183. // Lock / unlock elements
  184. const toggleLock = async (itemIds, _, { batchUpdateItems }) => {
  185. batchUpdateItems(itemIds, (item) => ({
  186. ...item,
  187. locked: !item.locked,
  188. }));
  189. const isFirstLock = !window.localStorage.getItem("isFirstLock");
  190. // Help user on first lock
  191. if (isFirstLock) {
  192. toast.info(
  193. i18n.t(
  194. "You've locked your first element. Long click to select it again."
  195. ),
  196. { autoClose: false }
  197. );
  198. window.localStorage.setItem("isFirstLock", "false");
  199. }
  200. };
  201. // Flip or reveal items
  202. export const setFlip = async (
  203. itemIds,
  204. { flip = true, reverseOrder = true } = {},
  205. { batchUpdateItems, reverseItemsOrder }
  206. ) => {
  207. batchUpdateItems(itemIds, (item) => ({
  208. ...item,
  209. flipped: flip,
  210. unflippedFor:
  211. !Array.isArray(item.unflippedFor) || item.unflippedFor.length > 0
  212. ? null
  213. : item.unflippedFor,
  214. }));
  215. if (reverseOrder) {
  216. reverseItemsOrder(itemIds);
  217. }
  218. };
  219. // Toggle flip state
  220. const toggleFlip = async (
  221. itemIds,
  222. { reverseOrder = true } = {},
  223. baseActions
  224. ) => {
  225. const items = await Promise.all(itemIds.map(baseActions.getItem));
  226. const flippedCount = items.filter(({ flipped }) => flipped).length;
  227. setFlip(
  228. itemIds,
  229. {
  230. flip: flippedCount < itemIds.length / 2,
  231. reverseOrder,
  232. },
  233. baseActions
  234. );
  235. };
  236. // Rotate element
  237. const rotate = async (itemIds, { angle }, { batchUpdateItems }) => {
  238. batchUpdateItems(itemIds, (item) => ({
  239. ...item,
  240. rotation: (item.rotation || 0) + angle,
  241. }));
  242. };
  243. // Reveal for player only
  244. export const setFlipSelf = async (
  245. itemIds,
  246. { flipSelf = true } = {},
  247. { batchUpdateItems }
  248. ) => {
  249. batchUpdateItems(itemIds, (item) => {
  250. let { unflippedFor = [] } = item;
  251. if (!Array.isArray(item.unflippedFor)) {
  252. unflippedFor = [];
  253. }
  254. if (flipSelf && !unflippedFor.includes(currentUser.uid)) {
  255. unflippedFor = [...unflippedFor, currentUser.uid];
  256. }
  257. if (!flipSelf && unflippedFor.includes(currentUser.uid)) {
  258. unflippedFor = unflippedFor.filter((id) => id !== currentUser.uid);
  259. }
  260. return {
  261. ...item,
  262. flipped: true,
  263. unflippedFor,
  264. };
  265. });
  266. };
  267. // Reveal for player only
  268. const toggleFlipSelf = async (itemIds, params, actions) => {
  269. const items = await Promise.all(itemIds.map(actions.getItem));
  270. const flippedSelfCount = items.filter(
  271. ({ unflippedFor }) =>
  272. Array.isArray(unflippedFor) && unflippedFor.includes(currentUser.uid)
  273. ).length;
  274. let flipSelf = true;
  275. if (flippedSelfCount > itemIds.length / 2) {
  276. flipSelf = false;
  277. }
  278. setFlipSelf(itemIds, { ...params, flipSelf }, actions);
  279. };
  280. const remove = async (itemIds, _, { removeItems }) => {
  281. removeItems(itemIds);
  282. };
  283. const cloneItem = async (itemIds, _, { getItem, insertItemBefore }) => {
  284. const items = await Promise.all(itemIds.map(getItem));
  285. items.forEach((itemToClone) => {
  286. const newItem = JSON.parse(JSON.stringify(itemToClone));
  287. newItem.id = nanoid();
  288. delete newItem.move;
  289. insertItemBefore(newItem, itemToClone.id);
  290. });
  291. };
  292. const actionMap = {
  293. flip: {
  294. action: toggleFlip,
  295. label: i18n.t("Reveal") + "/" + i18n.t("Hide"),
  296. shortcut: "f",
  297. icon: flipIcon,
  298. },
  299. reveal: {
  300. action: (itemIds, _, baseActions) =>
  301. setFlip(itemIds, { flip: false }, baseActions),
  302. label: i18n.t("Reveal"),
  303. icon: flipIcon,
  304. },
  305. hide: {
  306. action: (itemIds, _, baseActions) =>
  307. setFlip(itemIds, { flip: true }, baseActions),
  308. label: i18n.t("Hide"),
  309. icon: flipIcon,
  310. },
  311. flipSelf: {
  312. action: toggleFlipSelf,
  313. label: i18n.t("Reveal for me"),
  314. shortcut: "o",
  315. icon: seeIcon,
  316. },
  317. revealSelf: {
  318. action: (itemIds, _, baseActions) =>
  319. setFlipSelf(itemIds, { flipSelf: true }, baseActions),
  320. label: i18n.t("Reveal for me"),
  321. icon: seeIcon,
  322. },
  323. hideSelf: {
  324. action: (itemIds, _, baseActions) =>
  325. setFlipSelf(itemIds, { flipSelf: false }, baseActions),
  326. label: i18n.t("Hide for me"),
  327. icon: seeIcon,
  328. },
  329. tap: {
  330. action: toggleTap,
  331. label: i18n.t("Tap") + "/" + i18n.t("Untap"),
  332. shortcut: "t",
  333. icon: tapIcon,
  334. },
  335. stackToCenter: {
  336. action: stackToCenter,
  337. label: i18n.t("Stack To Center"),
  338. shortcut: "",
  339. multiple: true,
  340. icon: stackToCenterIcon,
  341. },
  342. stack: {
  343. action: stackToTopLeft,
  344. label: i18n.t("Stack To Top Left"),
  345. multiple: true,
  346. icon: stackToTopLeftIcon,
  347. },
  348. alignAsLine: {
  349. action: alignAsLine,
  350. label: i18n.t("Align as line"),
  351. multiple: true,
  352. icon: alignAsLineIcon,
  353. },
  354. alignAsSquare: {
  355. action: alignAsSquare,
  356. label: i18n.t("Align as square"),
  357. multiple: true,
  358. icon: alignAsSquareIcon,
  359. },
  360. shuffle: {
  361. action: shuffleItems,
  362. label: i18n.t("Shuffle"),
  363. multiple: true,
  364. icon: shuffleIcon,
  365. },
  366. randomlyRotate30: {
  367. action: (itemIds, _, baseActions) =>
  368. randomlyRotateSelectedItems(
  369. itemIds,
  370. {
  371. angle: 30,
  372. maxRotateCount: 11,
  373. },
  374. baseActions
  375. ),
  376. label: i18n.t("Rotate randomly 30"),
  377. multiple: false,
  378. icon: rotateIcon,
  379. },
  380. randomlyRotate45: {
  381. action: (itemIds, _, baseActions) =>
  382. randomlyRotateSelectedItems(
  383. itemIds,
  384. {
  385. angle: 45,
  386. maxRotateCount: 7,
  387. },
  388. baseActions
  389. ),
  390. label: i18n.t("Rotate randomly 45"),
  391. shortcut: "",
  392. multiple: false,
  393. icon: rotateIcon,
  394. },
  395. randomlyRotate60: {
  396. action: (itemIds, _, baseActions) =>
  397. randomlyRotateSelectedItems(
  398. itemIds,
  399. {
  400. angle: 60,
  401. maxRotateCount: 5,
  402. },
  403. baseActions
  404. ),
  405. label: i18n.t("Rotate randomly 60"),
  406. shortcut: "",
  407. multiple: false,
  408. icon: rotateIcon,
  409. },
  410. randomlyRotate90: {
  411. action: (itemIds, _, baseActions) =>
  412. randomlyRotateSelectedItems(
  413. itemIds,
  414. {
  415. angle: 90,
  416. maxRotateCount: 3,
  417. },
  418. baseActions
  419. ),
  420. label: i18n.t("Rotate randomly 90"),
  421. shortcut: "",
  422. multiple: false,
  423. icon: rotateIcon,
  424. },
  425. randomlyRotate180: {
  426. action: (itemIds, _, baseActions) =>
  427. randomlyRotateSelectedItems(
  428. itemIds,
  429. {
  430. angle: 180,
  431. maxRotateCount: 1,
  432. },
  433. baseActions
  434. ),
  435. label: i18n.t("Rotate randomly 180"),
  436. shortcut: "",
  437. multiple: false,
  438. icon: rotateIcon,
  439. },
  440. rotate30: {
  441. action: (itemIds, _, baseActions) =>
  442. rotate(itemIds, { angle: 30 }, baseActions),
  443. label: i18n.t("Rotate 30"),
  444. shortcut: "r",
  445. icon: rotateIcon,
  446. },
  447. rotate45: {
  448. action: (itemIds, _, baseActions) =>
  449. rotate(itemIds, { angle: 45 }, baseActions),
  450. label: i18n.t("Rotate 45"),
  451. shortcut: "r",
  452. icon: rotateIcon,
  453. },
  454. rotate60: {
  455. action: (itemIds, _, baseActions) =>
  456. rotate(itemIds, { angle: 60 }, baseActions),
  457. label: i18n.t("Rotate 60"),
  458. shortcut: "r",
  459. icon: rotateIcon,
  460. },
  461. rotate90: {
  462. action: (itemIds, _, baseActions) =>
  463. rotate(itemIds, { angle: 90 }, baseActions),
  464. label: i18n.t("Rotate 90"),
  465. shortcut: "r",
  466. icon: rotateIcon,
  467. },
  468. rotate180: {
  469. action: (itemIds, _, baseActions) =>
  470. rotate(itemIds, { angle: 180 }, baseActions),
  471. label: i18n.t("Rotate 180"),
  472. shortcut: "r",
  473. icon: rotateIcon,
  474. },
  475. clone: {
  476. action: cloneItem,
  477. label: i18n.t("Clone"),
  478. shortcut: "c",
  479. disableDblclick: true,
  480. edit: true,
  481. icon: duplicateIcon,
  482. },
  483. lock: {
  484. action: toggleLock,
  485. label: i18n.t("Unlock") + "/" + i18n.t("Lock"),
  486. disableDblclick: true,
  487. icon: lockIcon,
  488. },
  489. remove: {
  490. action: remove,
  491. label: i18n.t("Remove all"),
  492. shortcut: "Delete",
  493. edit: true,
  494. disableDblclick: true,
  495. icon: deleteIcon,
  496. },
  497. };
  498. export default actionMap;