2021-08-05 23:09:57 +02:00
|
|
|
import React, { memo } from "react";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import styled from "styled-components";
|
|
|
|
import { useRecoilCallback } from "recoil";
|
|
|
|
import debounce from "lodash.debounce";
|
|
|
|
|
2021-08-10 00:04:37 +02:00
|
|
|
import { useItemActions } from "react-sync-board";
|
2021-08-05 23:09:57 +02:00
|
|
|
|
2022-03-26 15:05:08 +01:00
|
|
|
import { search, uid } from "../../utils";
|
2021-08-05 23:09:57 +02:00
|
|
|
|
2022-03-26 15:05:08 +01:00
|
|
|
import Chevron from "../../ui/Chevron";
|
2021-08-05 23:09:57 +02:00
|
|
|
|
|
|
|
const StyledItemList = styled.ul`
|
|
|
|
display: flex;
|
|
|
|
flex-flow: row wrap;
|
|
|
|
list-style: none;
|
|
|
|
margin: 0;
|
|
|
|
padding: 0;
|
|
|
|
& li.group {
|
|
|
|
background-color: rgba(0, 0, 0, 0.1);
|
|
|
|
padding: 0 0.5em;
|
|
|
|
flex-basis: 100%;
|
|
|
|
}
|
|
|
|
overflow: visible;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const StyledItem = styled.li`
|
|
|
|
display: block;
|
|
|
|
padding: 0.5em;
|
|
|
|
margin: 0.2em;
|
|
|
|
cursor: pointer;
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
& > div {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: center;
|
|
|
|
pointer-events: none;
|
|
|
|
max-width: 80px;
|
|
|
|
& > span {
|
|
|
|
margin-top: 0.2em;
|
|
|
|
text-align: center;
|
|
|
|
max-width: 80px;
|
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
padding: 0.2em 0.5em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
&:hover > div > span {
|
|
|
|
z-index: 2;
|
|
|
|
max-width: none;
|
|
|
|
overflow: visible;
|
|
|
|
background-color: #222;
|
|
|
|
box-shadow: 0px 3px 6px #00000029;
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const size = 60;
|
|
|
|
|
|
|
|
const NewItem = memo(({ type, template, component: Component, name }) => {
|
2021-08-10 00:04:37 +02:00
|
|
|
const { pushItem } = useItemActions();
|
2021-08-05 23:09:57 +02:00
|
|
|
|
|
|
|
const addItem = React.useCallback(async () => {
|
|
|
|
pushItem({
|
2021-08-30 21:40:58 +02:00
|
|
|
...(typeof template === "function" ? template() : template),
|
2021-09-26 20:51:09 +02:00
|
|
|
id: uid(),
|
2021-08-05 23:09:57 +02:00
|
|
|
type,
|
|
|
|
});
|
|
|
|
}, [pushItem, template, type]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<StyledItem onClick={addItem}>
|
2022-03-26 15:05:08 +01:00
|
|
|
<div className="item-library__component">
|
2021-08-30 21:40:58 +02:00
|
|
|
<Component
|
|
|
|
{...(typeof template === "function" ? template() : template)}
|
|
|
|
width={size}
|
|
|
|
height={size}
|
|
|
|
size={size}
|
|
|
|
/>
|
2021-08-05 23:09:57 +02:00
|
|
|
<span>{name}</span>
|
|
|
|
</div>
|
|
|
|
</StyledItem>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
NewItem.displayName = "NewItem";
|
|
|
|
|
|
|
|
const SubItemList = ({ name, items }) => {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const [open, setOpen] = React.useState(false);
|
2021-08-10 00:04:37 +02:00
|
|
|
const { pushItems } = useItemActions();
|
2021-08-05 23:09:57 +02:00
|
|
|
|
|
|
|
const addItems = useRecoilCallback(
|
|
|
|
async (itemsToAdd) => {
|
|
|
|
pushItems(
|
2021-08-30 21:40:58 +02:00
|
|
|
itemsToAdd.map(({ template }) => ({
|
|
|
|
...(typeof template === "function" ? template() : template),
|
2021-09-26 20:51:09 +02:00
|
|
|
id: uid(),
|
2021-08-30 21:40:58 +02:00
|
|
|
}))
|
2021-08-05 23:09:57 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
[pushItems]
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<h3
|
|
|
|
onClick={() => setOpen((prev) => !prev)}
|
|
|
|
style={{ cursor: "pointer" }}
|
|
|
|
>
|
|
|
|
{open ? (
|
|
|
|
<Chevron orientation="bottom" color="#8c8c8c" />
|
|
|
|
) : (
|
|
|
|
<Chevron color="#8c8c8c" />
|
|
|
|
)}{" "}
|
|
|
|
{name}{" "}
|
|
|
|
<span
|
|
|
|
style={{ fontSize: "0.6em" }}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
addItems(items);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
[{t("Add all")}]
|
|
|
|
</span>
|
|
|
|
</h3>
|
|
|
|
{open && <ItemList items={items} />}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const ItemList = ({ items }) => (
|
|
|
|
<StyledItemList>
|
|
|
|
{items.map((node) => {
|
|
|
|
if (node.type) {
|
|
|
|
return <NewItem {...node} key={node.uid} />;
|
|
|
|
}
|
|
|
|
// it's a group
|
|
|
|
return (
|
|
|
|
<li key={`group_${node.name}`} className="group">
|
|
|
|
<SubItemList {...node} />
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</StyledItemList>
|
|
|
|
);
|
|
|
|
|
|
|
|
const MemoizedItemList = memo(ItemList);
|
|
|
|
|
|
|
|
const filterItems = (filter, nodes) =>
|
|
|
|
nodes.reduce((acc, node) => {
|
|
|
|
if (node.type) {
|
|
|
|
if (search(filter, node.name)) {
|
|
|
|
acc.push(node);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}
|
|
|
|
const filteredItems = filterItems(filter, node.items);
|
|
|
|
if (filteredItems.length) {
|
|
|
|
acc.push({ ...node, items: filteredItems });
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const ItemLibrary = ({ items }) => {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const [filter, setFilter] = React.useState("");
|
|
|
|
const [filteredItems, setFilteredItems] = React.useState(items);
|
|
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
const debouncedFilterItems = React.useCallback(
|
|
|
|
debounce((filterToApply, itemsToFilter) => {
|
|
|
|
setFilteredItems(filterItems(filterToApply, itemsToFilter));
|
|
|
|
}, 500),
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
debouncedFilterItems(filter, items);
|
|
|
|
}, [debouncedFilterItems, filter, items]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<input
|
|
|
|
onChange={(e) => setFilter(e.target.value)}
|
|
|
|
style={{ marginBottom: "1em" }}
|
|
|
|
placeholder={t("Search...")}
|
|
|
|
/>
|
|
|
|
<MemoizedItemList items={filteredItems} />
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default memo(ItemLibrary);
|