packages/x-flow/src/components/NodesMenu/index.tsx (136 lines of code) (raw):
import React, { forwardRef, Ref, useContext, useMemo } from 'react';
import { Popover, Input } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { ConfigContext } from '../../models/context';
import { useSet } from '../../utils/hooks';
import createIconFont from '../../utils/createIconFont';
import { TNodeMenu } from '../../types';
import './index.less';
// 检索节点
const searchNodeList = (query: string, list: any[]) => {
if (!query) {
return list;
}
const searchTerm = query.toLowerCase();
function searchList(nodes: any, preResult = []) {
if (nodes.length === 0) {
return preResult;
}
const [currentNode, ...restNodes] = nodes;
let result: any = [...preResult];
if (currentNode.title.toLowerCase().includes(searchTerm)) {
result.push(currentNode);
} else if (currentNode?.type === '_group' && currentNode.items) {
const matchingItems = searchList(currentNode.items);
if (matchingItems.length > 0) {
result.push({ ...currentNode, items: matchingItems });
}
}
return searchList(restNodes, result);
}
return searchList(list);
};
// 悬浮菜单项详细描述
export const MenuTooltip = ({ icon, title, description, iconFontUrl, iconSvg, SVGWidget, nodeSetting }: any) => {
const IconBox = useMemo(() => createIconFont(iconFontUrl), [iconFontUrl]);
return (
<div className='xflow-node-menu-tooltip'>
<div className='icon-box-max' style={{ background: icon?.bgColor || '#F79009', marginRight: '8px' }}>
{iconSvg ? <SVGWidget setting={nodeSetting} /> :<IconBox type={icon?.type} style={{ color: '#fff', fontSize: 13, ...icon?.style }} />}
</div>
<div className='title'>
{title}
</div>
<div className='description'>
{description}
</div>
</div>
)
};
// 节点菜单项
const MenuItem = (props: any) => {
const { title, type, icon, onClick, iconFontUrl, iconSvg } = props;
const IconBox = useMemo(() => createIconFont(iconFontUrl), [iconFontUrl]);
const { widgets, settingMap } = useContext(ConfigContext);
const nodeSetting = settingMap[type] || {};
const SVGWidget = widgets[nodeSetting?.iconSvg]
return (
<Popover
key={type}
content={<MenuTooltip {...props} SVGWidget={SVGWidget} nodeSetting={nodeSetting} />}
placement='right'
arrow={false}
getPopupContainer={() => document.getElementById('xflow-container') as HTMLElement}
>
<div
className='menu-item'
onClick={onClick(type)}
>
<span className='icon-box' style={{ background: icon?.bgColor || '#F79009', marginRight: '8px' }}>
{iconSvg ? <SVGWidget setting={nodeSetting} /> :
<IconBox
type={icon?.type}
style={{ color: '#fff', fontSize: 13 }}
/>}
</span>
<span>{title}</span>
</div>
</Popover>
);
};
// 过滤 hidden 节点
const filterHiddenMenu = (list: any) => {
return (list || []).filter((item: any) => !item.hidden)
}
/**
*
* 节点菜单List
*
*/
const NodesMenu = (props: TNodeMenu, ref: Ref<HTMLDivElement>) => {
const { items, showSearch, onClick } = props;
const { iconFontUrl } = useContext(ConfigContext);
const [state, setState] = useSet({
menuList: [...items]
});
const { menuList } = state;
const handleItemClick = (type: string) => (ev: React.MouseEvent<HTMLDivElement>) => {
ev.stopPropagation();
onClick({ type });
}
const handleSearch = (ev: any) => {
setState({ menuList: searchNodeList(ev.target.value, items) })
};
return (
<div className='xflow-node-menu' ref={ref}>
{!!showSearch && (
<div style={{ margin: '5px 9px 9px' }}>
<Input
placeholder='搜索节点'
onChange={handleSearch}
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,.45)' }} />}
style={{ width: '100%' }}
/>
</div>
)}
<div>
{filterHiddenMenu(menuList).map((item: any, index: number) => item.type === '_group' ? (
<div key={`${item.type}-${index}`}>
<div className='menu-group-title'>{item.title}</div>
{filterHiddenMenu(item.items).map((data: any, index: number) => (
<MenuItem
iconFontUrl={iconFontUrl}
{...data}
onClick={handleItemClick}
key={index}
/>
))}
</div>
) : (
<div key={`${item.type}-${index}`}>
<MenuItem
iconFontUrl={iconFontUrl}
{...item}
onClick={handleItemClick}
/>
</div>
))}
</div>
</div>
);
};
export default forwardRef(NodesMenu);