@happyhyep/tree-component
Version:
React Tree Component with Search functionality
244 lines (228 loc) • 12.6 kB
JavaScript
;
var jsxRuntime = require('react/jsx-runtime');
var React = require('react');
var DefaultContext = {
color: undefined,
size: undefined,
className: undefined,
style: undefined,
attr: undefined
};
var IconContext = React.createContext && React.createContext(DefaultContext);
var __assign = undefined && undefined.__assign || function () {
__assign = Object.assign || function (t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __rest$1 = undefined && undefined.__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
function Tree2Element(tree) {
return tree && tree.map(function (node, i) {
return React.createElement(node.tag, __assign({
key: i
}, node.attr), Tree2Element(node.child));
});
}
function GenIcon(data) {
// eslint-disable-next-line react/display-name
return function (props) {
return React.createElement(IconBase, __assign({
attr: __assign({}, data.attr)
}, props), Tree2Element(data.child));
};
}
function IconBase(props) {
var elem = function (conf) {
var attr = props.attr,
size = props.size,
title = props.title,
svgProps = __rest$1(props, ["attr", "size", "title"]);
var computedSize = size || conf.size || "1em";
var className;
if (conf.className) className = conf.className;
if (props.className) className = (className ? className + " " : "") + props.className;
return React.createElement("svg", __assign({
stroke: "currentColor",
fill: "currentColor",
strokeWidth: "0"
}, conf.attr, attr, svgProps, {
className: className,
style: __assign(__assign({
color: props.color || conf.color
}, conf.style), props.style),
height: computedSize,
width: computedSize,
xmlns: "http://www.w3.org/2000/svg"
}), title && React.createElement("title", null, title), props.children);
};
return IconContext !== undefined ? React.createElement(IconContext.Consumer, null, function (conf) {
return elem(conf);
}) : elem(DefaultContext);
}
// THIS FILE IS AUTO GENERATED
function MdKeyboardArrowDown (props) {
return GenIcon({"attr":{"viewBox":"0 0 24 24"},"child":[{"tag":"path","attr":{"fill":"none","d":"M0 0h24v24H0V0z"}},{"tag":"path","attr":{"d":"M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"}}]})(props);
}function MdKeyboardArrowRight (props) {
return GenIcon({"attr":{"viewBox":"0 0 24 24"},"child":[{"tag":"path","attr":{"fill":"none","d":"M0 0h24v24H0V0z"}},{"tag":"path","attr":{"d":"M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"}}]})(props);
}
const TreeNode = ({ item, level, isOpen, toggle, renderLabel, renderLeaf, openFolders, onItemClick, selectedId, className = '', }) => {
var _a, _b;
const hasChildren = (((_a = item.children) === null || _a === void 0 ? void 0 : _a.length) || 0) > 0;
const isSelected = selectedId === item.id;
const paddingLeft = 0.5 + level * 1.5;
return (jsxRuntime.jsxs("li", { className: `list-none ${className}`, style: { listStyle: 'none' }, children: [jsxRuntime.jsxs("div", { className: `flex items-center gap-1 cursor-pointer hover:bg-gray-50 py-1 px-2 rounded-md transition-colors duration-150 ${isSelected ? 'bg-blue-50 text-blue-700 font-medium' : ''}`, style: { paddingLeft: `${paddingLeft}rem` }, children: [item.canOpen ? (jsxRuntime.jsx("button", { onClick: () => {
toggle(item.id);
onItemClick === null || onItemClick === void 0 ? void 0 : onItemClick(item);
}, className: "flex items-center justify-center w-4 h-4 text-gray-500 hover:text-gray-700 transition-colors duration-150 bg-transparent border-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-blue-300 rounded", style: { background: 'none', border: 'none', padding: '0' }, "aria-label": isOpen ? 'Collapse' : 'Expand', children: isOpen ? jsxRuntime.jsx(MdKeyboardArrowDown, { size: 14 }) : jsxRuntime.jsx(MdKeyboardArrowRight, { size: 14 }) })) : (jsxRuntime.jsx("span", { className: "w-4 h-4" })), jsxRuntime.jsx("div", { className: "w-full", onClick: () => {
onItemClick === null || onItemClick === void 0 ? void 0 : onItemClick(item);
}, children: renderLabel(item.data, item) })] }), isOpen &&
hasChildren &&
((_b = item.children) === null || _b === void 0 ? void 0 : _b.map((child) => (jsxRuntime.jsx(TreeNode, { item: child, level: level + 1, isOpen: openFolders.has(child.id), toggle: toggle, renderLabel: renderLabel, renderLeaf: renderLeaf, openFolders: openFolders, onItemClick: onItemClick, selectedId: selectedId }, child.id)))), isOpen && item.hasLeaf && (renderLeaf === null || renderLeaf === void 0 ? void 0 : renderLeaf(item.data))] }));
};
const buildTree = (flatItems, parentId = null) => {
return flatItems
.filter((item) => item.parentId === parentId)
.map((item) => (Object.assign(Object.assign({}, item), { children: buildTree(flatItems, item.id) })));
};
const getChildren = (items, id) => {
return items.filter((item) => item.parentId === id);
};
const filterTreeWithDescendants = (items, keyword, matchFn) => {
return items
.map((item) => {
const children = filterTreeWithDescendants(item.children || [], keyword, matchFn);
const isMatch = matchFn(item);
if (isMatch) {
return Object.assign(Object.assign({}, item), { children: item.children || [] });
}
if (children.length > 0) {
return Object.assign(Object.assign({}, item), { children });
}
return null;
})
.filter(Boolean);
};
const Tree = ({ items, renderLabel, renderLeaf, defaultExpandAll = false, onItemClick, selectedId, className = '', }) => {
const [openFolders, setOpenFolders] = React.useState(() => new Set(defaultExpandAll ? items.map((el) => el.id) : []));
const treeData = React.useMemo(() => buildTree(items), [items]);
const toggle = (id) => {
setOpenFolders((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
}
else {
next.add(id);
}
return next;
});
};
return (jsxRuntime.jsx("ul", { className: `${className}`, style: { listStyle: 'none', margin: 0, padding: 0 }, children: treeData.map((item) => (jsxRuntime.jsx(TreeNode, { item: item, level: 0, isOpen: openFolders.has(item.id), toggle: toggle, renderLabel: renderLabel, renderLeaf: renderLeaf, openFolders: openFolders, onItemClick: onItemClick, selectedId: selectedId }, item.id))) }));
};
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
const TreeSearchContext = React.createContext(null);
const useTreeSearch = () => {
const ctx = React.useContext(TreeSearchContext);
if (!ctx)
throw new Error('useTreeSearch must be used within <TreeWithSearch>');
return ctx;
};
const TreeSearchInput = (_a) => {
var { placeholder = '검색...', className = '' } = _a, rest = __rest(_a, ["placeholder", "className"]);
const { search, setSearch } = useTreeSearch();
return (jsxRuntime.jsx("input", Object.assign({ type: "text", value: search, onChange: (e) => setSearch(e.target.value), placeholder: placeholder, className: `w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-150 ${className}`, style: { fontSize: '14px' } }, rest)));
};
const TreeWithSearch = ({ items, renderLabel, renderLeaf, searchFn, defaultExpandAll = false, children, onItemClick, selectedId, className = '', }) => {
const [search, setSearch] = React.useState('');
const [manualOpenFolders, setManualOpenFolders] = React.useState(() => new Set(defaultExpandAll ? items.map((el) => el.id) : []));
const [searchOpenFolders, setSearchOpenFolders] = React.useState(new Set());
React.useEffect(() => {
if (defaultExpandAll) {
setManualOpenFolders(new Set(items.map((el) => el.id)));
}
}, [items, defaultExpandAll]);
const isSearching = search.trim().length > 0;
const activeOpenFolders = isSearching ? searchOpenFolders : manualOpenFolders;
const treeData = React.useMemo(() => buildTree(items), [items]);
const filteredData = React.useMemo(() => {
if (!isSearching || !searchFn)
return treeData;
return filterTreeWithDescendants(treeData, search, (item) => searchFn(item.data, search));
}, [search, treeData, searchFn, isSearching]);
React.useEffect(() => {
if (!isSearching || !searchFn)
return;
const matched = filterTreeWithDescendants(treeData, search, (item) => searchFn(item.data, search));
const idsToOpen = new Set();
const collectAllIds = (items) => {
for (const item of items) {
idsToOpen.add(item.id);
if (item.children)
collectAllIds(item.children);
}
};
collectAllIds(matched);
setSearchOpenFolders(idsToOpen);
}, [search, searchFn, treeData, isSearching]);
const toggle = (id) => {
const setState = isSearching ? setSearchOpenFolders : setManualOpenFolders;
setState((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
}
else {
next.add(id);
}
return next;
});
};
return (jsxRuntime.jsx(TreeSearchContext.Provider, { value: { search, setSearch }, children: jsxRuntime.jsxs("div", { className: className, children: [children, jsxRuntime.jsx("ul", { style: { listStyle: 'none', margin: 0, padding: 0 }, children: filteredData.map((item) => (jsxRuntime.jsx(TreeNode, { item: item, level: 0, isOpen: activeOpenFolders.has(item.id), toggle: toggle, renderLabel: (data) => renderLabel(data, isSearching ? search : undefined), renderLeaf: renderLeaf, openFolders: activeOpenFolders, onItemClick: onItemClick, selectedId: selectedId }, item.id))) })] }) }));
};
TreeWithSearch.Input = TreeSearchInput;
exports.Tree = Tree;
exports.TreeNode = TreeNode;
exports.TreeWithSearch = TreeWithSearch;
exports.buildTree = buildTree;
exports.filterTreeWithDescendants = filterTreeWithDescendants;
exports.getChildren = getChildren;
exports.useTreeSearch = useTreeSearch;
//# sourceMappingURL=index.js.map