ItemLibrary.jsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import React, { memo } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import styled from "styled-components";
  4. import debounce from "lodash.debounce";
  5. import { useItemActions } from "react-sync-board";
  6. import { search, uid } from "../../utils";
  7. import Chevron from "../../ui/Chevron";
  8. const StyledItemList = styled.ul`
  9. display: flex;
  10. flex-flow: row wrap;
  11. list-style: none;
  12. margin: 0;
  13. padding: 0;
  14. & li.group {
  15. background-color: rgba(0, 0, 0, 0.1);
  16. padding: 0 0.5em;
  17. flex-basis: 100%;
  18. }
  19. overflow: visible;
  20. `;
  21. const StyledItem = styled.li`
  22. display: block;
  23. padding: 0.5em;
  24. margin: 0.2em;
  25. cursor: pointer;
  26. opacity: 0.7;
  27. &:hover {
  28. opacity: 1;
  29. }
  30. & > div {
  31. display: flex;
  32. flex-direction: column;
  33. align-items: center;
  34. pointer-events: none;
  35. max-width: 80px;
  36. & > span {
  37. margin-top: 0.2em;
  38. text-align: center;
  39. max-width: 80px;
  40. white-space: nowrap;
  41. overflow: hidden;
  42. text-overflow: ellipsis;
  43. padding: 0.2em 0.5em;
  44. }
  45. }
  46. &:hover > div > span {
  47. z-index: 2;
  48. max-width: none;
  49. overflow: visible;
  50. background-color: #222;
  51. box-shadow: 0px 3px 6px #00000029;
  52. }
  53. `;
  54. const size = 60;
  55. const NewItem = memo(({ type, template, component: Component, name }) => {
  56. const { pushItem } = useItemActions();
  57. const addItem = React.useCallback(async () => {
  58. pushItem({
  59. ...(typeof template === "function" ? template() : template),
  60. id: uid(),
  61. type,
  62. });
  63. }, [pushItem, template, type]);
  64. return (
  65. <StyledItem onClick={addItem}>
  66. <div className="item-library__component">
  67. <Component
  68. {...(typeof template === "function" ? template() : template)}
  69. width={size}
  70. height={size}
  71. size={size}
  72. />
  73. <span>{name}</span>
  74. </div>
  75. </StyledItem>
  76. );
  77. });
  78. NewItem.displayName = "NewItem";
  79. const SubItemList = ({ name, items }) => {
  80. const { t } = useTranslation();
  81. const [open, setOpen] = React.useState(false);
  82. const { pushItems } = useItemActions();
  83. const addItems = React.useCallback(
  84. async (itemsToAdd) => {
  85. pushItems(
  86. itemsToAdd.map(({ template }) => ({
  87. ...(typeof template === "function" ? template() : template),
  88. id: uid(),
  89. }))
  90. );
  91. },
  92. [pushItems]
  93. );
  94. return (
  95. <>
  96. <h3
  97. onClick={() => setOpen((prev) => !prev)}
  98. style={{ cursor: "pointer" }}
  99. >
  100. {open ? (
  101. <Chevron orientation="bottom" color="#8c8c8c" />
  102. ) : (
  103. <Chevron color="#8c8c8c" />
  104. )}{" "}
  105. {name}{" "}
  106. <span
  107. style={{ fontSize: "0.6em" }}
  108. onClick={(e) => {
  109. e.preventDefault();
  110. e.stopPropagation();
  111. addItems(items);
  112. }}
  113. >
  114. [{t("Add all")}]
  115. </span>
  116. </h3>
  117. {open && <ItemList items={items} />}
  118. </>
  119. );
  120. };
  121. const ItemList = ({ items }) => (
  122. <StyledItemList>
  123. {items.map((node) => {
  124. if (node.type) {
  125. return <NewItem {...node} key={node.uid} />;
  126. }
  127. // it's a group
  128. return (
  129. <li key={`group_${node.name}`} className="group">
  130. <SubItemList {...node} />
  131. </li>
  132. );
  133. })}
  134. </StyledItemList>
  135. );
  136. const MemoizedItemList = memo(ItemList);
  137. const filterItems = (filter, nodes) =>
  138. nodes.reduce((acc, node) => {
  139. if (node.type) {
  140. if (search(filter, node.name)) {
  141. acc.push(node);
  142. }
  143. return acc;
  144. }
  145. const filteredItems = filterItems(filter, node.items);
  146. if (filteredItems.length) {
  147. acc.push({ ...node, items: filteredItems });
  148. }
  149. return acc;
  150. }, []);
  151. const ItemLibrary = ({ items }) => {
  152. const { t } = useTranslation();
  153. const [filter, setFilter] = React.useState("");
  154. const [filteredItems, setFilteredItems] = React.useState(items);
  155. // eslint-disable-next-line react-hooks/exhaustive-deps
  156. const debouncedFilterItems = React.useCallback(
  157. debounce((filterToApply, itemsToFilter) => {
  158. setFilteredItems(filterItems(filterToApply, itemsToFilter));
  159. }, 500),
  160. []
  161. );
  162. React.useEffect(() => {
  163. debouncedFilterItems(filter, items);
  164. }, [debouncedFilterItems, filter, items]);
  165. return (
  166. <>
  167. <input
  168. onChange={(e) => setFilter(e.target.value)}
  169. style={{ marginBottom: "1em" }}
  170. placeholder={t("Search...")}
  171. />
  172. <MemoizedItemList items={filteredItems} />
  173. </>
  174. );
  175. };
  176. export default memo(ItemLibrary);