g6/site/pages/canvas-menu.tsx

502 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
EyeOutlined,
EyeInvisibleOutlined,
NodeIndexOutlined,
DisconnectOutlined,
TagOutlined,
SearchOutlined,
HighlightOutlined
} from '@ant-design/icons';
2020-11-21 13:38:09 +08:00
const isBrowser = typeof window !== 'undefined';
const G6 = isBrowser ? require('../../dist/g6.min.js') : null;
const insertCss = isBrowser ? require('insert-css') : null;
const modifyCSS = isBrowser ? require('@antv/dom-util/lib/modify-css').default : null;
2020-11-21 13:38:09 +08:00
if (isBrowser) {
insertCss(`
#canvas-menu {
position: absolute;
z-index: 2;
left: 16px;
top: 80px;
width: fit-content;
padding: 8px 16px;
background-color: rgba(54, 59, 64, 0);
border-radius: 24px;
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0);
font-family: PingFangSC-Semibold;
transition: all 0.2s linear;
}
#canvas-menu:hover {
background-color: rgba(54, 59, 64, 1);
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0.6);
}
.icon-span {
padding-left: 8px;
padding-right: 8px;
cursor: pointer;
}
#search-node-input {
background-color: rgba(60, 60, 60, 0.95);
border-radius: 21px;
width: 100px;
border-color: rgba(80, 80, 80, 0.95);
border-style: solid;
color: rgba(255, 255, 255, 0.85);
}
#submit-button {
background-color: rgba(82, 115, 224, 0.2);
border-radius: 21px;
border-color: rgb(82, 115, 224);
border-style: solid;
color: rgba(152, 165, 254, 1);
margin-left: 4px;
}
.menu-tip {
position: absolute;
right: calc(30% + 32px);
width: fit-content;
height: 40px;
line-height: 40px;
top: 80px;
padding-left: 16px;
padding-right: 16px;
background-color: rgba(54, 59, 64, 0.5);
color: rgba(255, 255, 255, 0.65);
border-radius: 8px;
transition: all 0.2s linear;
font-family: PingFangSC-Semibold;
}
#g6-canavs-menu-item-tip {
position: absolute;
background-color: rgba(0,0,0, 0.65);
padding: 10px;
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 10px;
width: fit-content;
color: #fff;
border-radius: 8px;
font-size: 12px;
height: fit-content;
font-family: PingFangSC-Semibold;
transition: all 0.2s linear;
}
`);
2020-11-21 13:38:09 +08:00
}
let fishEye = null;
const CanvasMenu: React.FC<{
graph: any;
clickFisheyeIcon: (onlyDisable?: boolean) => void;
clickLassoIcon: (onlyDisable?: boolean) => void;
fisheyeEnabled: boolean;
lassoEnabled: boolean;
edgeLabelVisible: boolean;
setEdgeLabelVisible: (vis: boolean) => void;
searchNode: (id: string) => boolean;
handleFindPath: () => void;
stopLayout: () => void;
}> = ({
graph,
clickFisheyeIcon,
clickLassoIcon,
fisheyeEnabled,
lassoEnabled,
stopLayout,
edgeLabelVisible,
setEdgeLabelVisible,
searchNode,
handleFindPath
}) => {
const { t } = useTranslation();
// menu tip, 例如 “按下 ESC 退出鱼眼”
const [menuTip, setMenuTip] = useState({
text: '',
display: 'none',
opacity: 0
});
// menu item tip
const [menuItemTip, setMenuItemTip] = useState({
text: '',
display: 'none',
opacity: 0
});
const [enableSearch, setEnableSearch] = useState(false);
const [enableSelectPathEnd, setEnableSelectPathEnd] = useState(false);
const clickEdgeLabelController = () => {
setEdgeLabelVisible(!edgeLabelVisible);
}
const handleEnableSearch = () => {
// 关闭 lasso 框选
if (lassoEnabled) clickLassoIcon(true)
// 关闭选择路径端点
if (enableSelectPathEnd) setEnableSelectPathEnd(false);
// 关闭搜索节点框
if (enableSearch) setEnableSearch(false);
// 关闭 fisheye
if (fisheyeEnabled && fishEye) {
graph.removePlugin(fishEye);
clickFisheyeIcon(true);
}
setEnableSearch(old => {
if (old) {
// 设置 menuTip
setMenuTip({
text: '',
display: 'none',
opacity: 0
});
return false;
}
// 设置 menuTip
setMenuTip({
text: t('输入需要搜索的节点 ID并点击 Submit 按钮'),
display: 'block',
opacity: 1
});
return true;
});
}
// 放大
const handleZoomOut = () => {
if (!graph || graph.destroyed) return;
const current = graph.getZoom();
const canvas = graph.get('canvas');
const point = canvas.getPointByClient(canvas.get('width') / 2, canvas.get('height') / 2);
const pixelRatio = canvas.get('pixelRatio') || 1;
const ratio = 1 + 0.05 * 5;
if (ratio * current > 5) {
return;
}
graph.zoom(ratio, { x: point.x / pixelRatio, y: point.y / pixelRatio });
};
// 缩小
const handleZoomIn = () => {
if (!graph || graph.destroyed) return;
const current = graph.getZoom();
const canvas = graph.get('canvas');
const point = canvas.getPointByClient(canvas.get('width') / 2, canvas.get('height') / 2);
const pixelRatio = canvas.get('pixelRatio') || 1;
const ratio = 1 - 0.05 * 5;
if (ratio * current < 0.3) {
return;
}
graph.zoom(ratio, { x: point.x / pixelRatio, y: point.y / pixelRatio });
};
const handleFitViw = () => {
if (!graph || graph.destroyed) return;
graph.fitView(16);
}
const handleSearchNode = () => {
const value = (document.getElementById('search-node-input') as HTMLInputElement).value;
const found = searchNode(value);
if (!found) setMenuTip({
text: t('没有找到该节点'),
display: 'block',
opacity: 1
});
}
const showItemTip = (e, text) => {
const { clientX: x, clientY: y } = e;
setMenuItemTip({
text,
display: 'block',
opacity: 1
});
const tipDom = document.getElementById('g6-canavs-menu-item-tip')
modifyCSS(tipDom, {
top: `${124}px`,
left: `${x - 20}px`,
zIndex: 100,
});
};
const hideItemTip = () => {
setMenuItemTip({
text: '',
display: 'none',
opacity: 0
});
const tipDom = document.getElementById('g6-canavs-menu-item-tip')
modifyCSS(tipDom, {
zIndex: -100,
});
}
/**
*
*/
const toggleFishEye = () => {
if (!graph || graph.destroyed) return;
// 设置鼠标样式为默认
graph.get('canvas').setCursor('default');
// 关闭 FishEye
if (fisheyeEnabled && fishEye) {
graph.removePlugin(fishEye);
graph.setMode('default')
// 设置 menuTip
setMenuTip({
text: t('按下 Esc 键退出鱼眼放大镜'),
display: 'none',
opacity: 0
});
} else {
// 停止布局
stopLayout();
// 关闭 lasso 框选
if (lassoEnabled) clickLassoIcon(true)
// 关闭选择路径端点
if (enableSelectPathEnd) setEnableSelectPathEnd(false);
// 关闭搜索节点框
if (enableSearch) setEnableSearch(false);
// 设置 menuTip
setMenuTip({
text: t('按下 Esc 键退出鱼眼放大镜'),
display: 'block',
opacity: 1
});
// 将交互模式切换到鱼眼只读模式,即不能缩放画布、框选节点、拉索选择节点等可能和鱼眼有冲突的操作
graph.setMode('fisheyeMode');
// 开启 FishEye
fishEye = new G6.Fisheye({
r: 249,
scaleRByWheel: true,
minR: 100,
maxR: 500
// showLabel: true,
});
graph.addPlugin(fishEye);
}
clickFisheyeIcon();
};
// 开启 lasso select 功能
const enabledLassoSelect = () => {
if (!graph || graph.destroyed) return;
clickLassoIcon();
if (!lassoEnabled) {
graph.setMode('lassoSelect');
// 设置鼠标样式为十字形
graph.get('canvas').setCursor('crosshair');
// 关闭 fisheye
if (fisheyeEnabled && fishEye) {
graph.removePlugin(fishEye);
clickFisheyeIcon(true);
}
// 关闭选择路径端点
if (enableSelectPathEnd) setEnableSelectPathEnd(false);
// 关闭搜索节点框
if (enableSearch) setEnableSearch(false);
// 设置 menuTip
setMenuTip({
text: t('按下 Esc 键退出拉索选择模式'),
display: 'block',
opacity: 1
});
} else {
graph.setMode('default');
// 设置鼠标样式为默认
graph.get('canvas').setCursor('default');
// 设置 menuTip
setMenuTip({
text: t('按下 Esc 键退出拉索选择模式'),
display: 'none',
opacity: 0
});
}
}
// 开启选择两个节点显示最短路径
const handleEnableSelectPathEnd = () => {
setEnableSelectPathEnd(old => {
if (!old) {
graph.setMode('default');
// 关闭 fisheye
if (fishEye) {
graph.removePlugin(fishEye);
clickFisheyeIcon(true);
}
// 关闭 lasso 框选
if (lassoEnabled) clickLassoIcon(true)
// 关闭搜索节点框
if (enableSearch) setEnableSearch(false);
// 设置 menuTip
setMenuTip({
text: t('按住 SHIFT 键并点选两个节点作为路径起终点'),
display: 'block',
opacity: 1
});
return true;
}
// 设置 menuTip
setMenuTip({
text: '',
display: 'none',
opacity: 0
});
return false;
})
}
const escListener = e => {
if (!graph || graph.destroyed) return;
if (e.key !== 'Escape') return;
if (fishEye) {
graph.removePlugin(fishEye);
clickFisheyeIcon(true);
}
// 关闭 lasso 框选
graph.setMode('default');
// 关闭搜索节点框
setEnableSearch(false);
// 关闭选择路径端点
setEnableSelectPathEnd(false);
// 设置鼠标样式为默认
graph.get('canvas').setCursor('default');
clickLassoIcon(true);
// 设置 menuTip
setMenuTip({
text: t('按下 Esc 键退出当前模式'),
display: 'none',
opacity: 0
});
}
useEffect(() => {
2020-11-21 11:56:53 +08:00
if (typeof window !== 'undefined') {
2020-11-21 01:08:19 +08:00
window.addEventListener("keydown", escListener.bind(this));
return window.removeEventListener("keydown", escListener.bind(this));
}
}, []);
const iconStyle = {
disable: { color: 'rgba(255, 255, 255, 0.85)' },
enable: { color: 'rgba(82, 115, 224, 1)' },
};
return (
<>
<div id='canvas-menu'>
<div className='icon-container'>
<span className='icon-span'
style={{ backgroundColor: 'rgba(0, 0, 0, 0)' }}
onClick={clickEdgeLabelController}
onMouseEnter={e => showItemTip(e, edgeLabelVisible ? t('隐藏边标签') : t('显示边标签'))}
onMouseLeave={hideItemTip}
>
{edgeLabelVisible ? <DisconnectOutlined style={iconStyle.enable} /> : <TagOutlined style={iconStyle.disable} />}
</span>
<span className='icon-span'
onClick={toggleFishEye}
onMouseEnter={e => showItemTip(e, fisheyeEnabled ? t('关闭鱼眼放大镜') : t('打开鱼眼放大镜'))}
onMouseLeave={hideItemTip}
>
{fisheyeEnabled ? <EyeInvisibleOutlined style={iconStyle.enable} /> : <EyeOutlined style={iconStyle.disable} />}
</span>
<span className='icon-span'
onClick={enabledLassoSelect}
onMouseEnter={e => showItemTip(e, lassoEnabled ? t('关闭拉索选择模式') : t('打开拉索选择模式'))}
onMouseLeave={hideItemTip}
>
<HighlightOutlined style={lassoEnabled ? iconStyle.enable : iconStyle.disable} />
</span>
<span className='icon-span'
onClick={handleEnableSelectPathEnd}
onMouseEnter={e => showItemTip(e, t('搜索最短路径'))}
onMouseLeave={hideItemTip}
>
<NodeIndexOutlined style={enableSelectPathEnd ? iconStyle.enable : iconStyle.disable} />
</span>
<span className='icon-span' style={{ width: 'fit-content', ...iconStyle.disable }} >
<span className='zoom-icon'
onClick={handleZoomIn}
onMouseEnter={e => showItemTip(e, t('缩小'))}
onMouseLeave={hideItemTip}
>
-
</span>
<span className='zoom-icon'
onClick={handleFitViw}
onMouseEnter={e => showItemTip(e, t('图内容适配容器快捷键ctrl + 1'))}
onMouseLeave={hideItemTip}
style={{ paddingLeft: '8px', paddingRight: '8px' }}
>
FIT
</span>
<span className='zoom-icon'
onClick={handleZoomOut}
onMouseEnter={e => showItemTip(e, t('放大'))}
onMouseLeave={hideItemTip}
>
+
</span>
</span>
<span className='icon-span'
onClick={handleEnableSearch}
onMouseEnter={e => showItemTip(e, t('输入 ID 搜索节点'))}
onMouseLeave={hideItemTip}
>
<SearchOutlined style={enableSearch ? iconStyle.enable : iconStyle.disable} />
</span>
{enableSearch &&
<span
onMouseEnter={e => showItemTip(e, t('输入需要搜索的节点 ID并点击 Submit 按钮'))}
onMouseLeave={hideItemTip}
>
<input type="text" id="search-node-input" />
<button id="submit-button" onClick={handleSearchNode}>Submit</button>
</span>
}
{enableSelectPathEnd &&
<span
onMouseEnter={e => showItemTip(e, t('选择有且仅有两个节点作为端点,并点击 Find Path 按钮'))}
onMouseLeave={hideItemTip}
>
<button id="submit-button" onClick={handleFindPath}>Find Path</button>
</span>}
</div>
</div>
<div className='menu-tip' style={{ opacity: menuTip.opacity }}>
{menuTip.text}
</div>
<div id='g6-canavs-menu-item-tip' style={{ opacity: menuItemTip.opacity }}>
{menuItemTip.text}
</div>
</>
);
};
export default CanvasMenu;