13 changed files with 374 additions and 16 deletions
@ -0,0 +1,65 @@ |
|||||
|
import { |
||||
|
useRef, |
||||
|
useEffect, |
||||
|
MutableRefObject |
||||
|
} from 'react'; |
||||
|
|
||||
|
type EventType = MouseEvent | TouchEvent; |
||||
|
type TargetElement = HTMLElement | Element | Document | Window; |
||||
|
type BasicTarget<T = HTMLElement> = |
||||
|
| (() => T | null) |
||||
|
| T |
||||
|
| null |
||||
|
| MutableRefObject<T | null | undefined>; |
||||
|
|
||||
|
const defaultEvent = 'click'; |
||||
|
|
||||
|
const getTargetElement = ( |
||||
|
target: BasicTarget<TargetElement>, |
||||
|
defaultElement?: TargetElement, |
||||
|
): TargetElement | undefined | null => { |
||||
|
if (!target) { |
||||
|
return defaultElement; |
||||
|
} |
||||
|
|
||||
|
let targetElement: TargetElement | undefined | null; |
||||
|
if (typeof target === 'function') { |
||||
|
targetElement = target(); |
||||
|
} else if ('current' in target) { |
||||
|
targetElement = target.current; |
||||
|
} else { |
||||
|
targetElement = target; |
||||
|
} |
||||
|
|
||||
|
return targetElement; |
||||
|
}; |
||||
|
|
||||
|
const useClickAway = ( |
||||
|
onClickAway: (event: EventType) => void, |
||||
|
target: BasicTarget | BasicTarget[], |
||||
|
eventName: string = defaultEvent, |
||||
|
): void => { |
||||
|
const onClickAwayRef = useRef(onClickAway); |
||||
|
onClickAwayRef.current = onClickAway; |
||||
|
useEffect(() => { |
||||
|
const handler = (event: any) => { |
||||
|
const targets = Array.isArray(target) ? target : [target]; |
||||
|
if ( |
||||
|
targets.some((targetItem) => { |
||||
|
const targetElement = getTargetElement(targetItem) as HTMLElement; |
||||
|
return !targetElement || targetElement?.contains(event.target); |
||||
|
}) |
||||
|
) { |
||||
|
return; |
||||
|
} |
||||
|
onClickAwayRef.current(event); |
||||
|
}; |
||||
|
document.addEventListener(eventName, handler); |
||||
|
|
||||
|
return () => { |
||||
|
document.removeEventListener(eventName, handler); |
||||
|
}; |
||||
|
}, [target, eventName]); |
||||
|
}; |
||||
|
|
||||
|
export default useClickAway; |
@ -0,0 +1,118 @@ |
|||||
|
import React, { |
||||
|
useRef, |
||||
|
useCallback |
||||
|
} from 'react'; |
||||
|
|
||||
|
import styles from '../index.module.less'; |
||||
|
|
||||
|
import { Menu } from 'antd'; |
||||
|
import { Graph } from '@antv/x6'; |
||||
|
import useClickAway from '../../../hooks/useClickAway'; |
||||
|
import { nodeMenuConfig, blankMenuConfig } from './menuConfig'; |
||||
|
|
||||
|
interface IProps { |
||||
|
x: number; |
||||
|
y: number; |
||||
|
scene: string; |
||||
|
visible: boolean; |
||||
|
flowChart: Graph; |
||||
|
} |
||||
|
|
||||
|
interface IMenuConfig { |
||||
|
key: string; |
||||
|
title: string; |
||||
|
icon?: React.ReactElement; |
||||
|
children?: IMenuConfig[]; |
||||
|
showDividerBehind?: boolean; |
||||
|
disabled?: boolean | ((flowChart: Graph) => boolean); |
||||
|
handler: (flowChart: Graph) => void; |
||||
|
} |
||||
|
|
||||
|
const menuConfigMap: { [scene: string]: IMenuConfig[] } = { |
||||
|
node: nodeMenuConfig, |
||||
|
blank: blankMenuConfig |
||||
|
}; |
||||
|
|
||||
|
const FlowChartContextMenu: React.FC<IProps> = props => { |
||||
|
const menuRef = useRef(null); |
||||
|
const { x, y, scene, visible, flowChart } = props; |
||||
|
const menuConfig = menuConfigMap[scene]; |
||||
|
|
||||
|
useClickAway(() => onClickAway(), menuRef); |
||||
|
|
||||
|
const onClickAway = useCallback(() => flowChart.trigger('graph:hideContextMenu'), [flowChart]); |
||||
|
const onClickMenu = useCallback(({ key }) => { |
||||
|
const handlerMap = Helper.makeMenuHandlerMap(menuConfig); |
||||
|
const handler = handlerMap[key]; |
||||
|
if (handler) { |
||||
|
onClickAway(); |
||||
|
handler(flowChart); |
||||
|
} |
||||
|
}, [flowChart, menuConfig]); |
||||
|
|
||||
|
return !visible ? null : ( |
||||
|
<div |
||||
|
ref={menuRef} |
||||
|
className={styles.contextMenu} |
||||
|
style={{ left: x, top: y }} |
||||
|
> |
||||
|
<Menu |
||||
|
mode={'vertical'} |
||||
|
selectable={false} |
||||
|
onClick={onClickMenu} |
||||
|
> |
||||
|
{Helper.makeMenuContent(flowChart, menuConfig)} |
||||
|
</Menu> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const Helper = { |
||||
|
makeMenuHandlerMap(config: IMenuConfig[]) { |
||||
|
const queue = config.slice(0); |
||||
|
const handlerMap: { [key: string]: (flowChart: Graph) => void } = {}; |
||||
|
while (queue.length > 0) { |
||||
|
const { key, handler, children } = queue.pop() as IMenuConfig; |
||||
|
if (children && children.length > 0) { |
||||
|
queue.push(...children); |
||||
|
} else { |
||||
|
handlerMap[key] = handler; |
||||
|
} |
||||
|
} |
||||
|
return handlerMap; |
||||
|
}, |
||||
|
makeMenuContent(flowChart: Graph, menuConfig: IMenuConfig[]) { |
||||
|
const loop = (config: IMenuConfig[]) => { |
||||
|
return config.map(item => { |
||||
|
let content = null; |
||||
|
let { key, title, icon, children, disabled = false, showDividerBehind } = item; |
||||
|
if (key === 'paste') { |
||||
|
debugger |
||||
|
} |
||||
|
if (typeof disabled === 'function') { |
||||
|
disabled = disabled(flowChart); |
||||
|
} |
||||
|
if (children && children.length > 0) { |
||||
|
content = ( |
||||
|
<Menu.SubMenu key={key} icon={icon} title={title} disabled={disabled}> |
||||
|
{loop(children)} |
||||
|
</Menu.SubMenu> |
||||
|
); |
||||
|
} else { |
||||
|
content = ( |
||||
|
<Menu.Item key={key} icon={icon} disabled={disabled}> |
||||
|
{title} |
||||
|
</Menu.Item> |
||||
|
); |
||||
|
} |
||||
|
return [ |
||||
|
content, |
||||
|
showDividerBehind && <Menu.Divider /> |
||||
|
]; |
||||
|
}); |
||||
|
}; |
||||
|
return loop(menuConfig); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default FlowChartContextMenu; |
@ -0,0 +1,24 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Graph } from '@antv/x6'; |
||||
|
import XIcon from '../../../../components/xIcon'; |
||||
|
import shortcuts from '../../../../common/shortcuts'; |
||||
|
import { SnippetsOutlined } from '@ant-design/icons'; |
||||
|
|
||||
|
const blankMenuConfig = [ |
||||
|
{ |
||||
|
key: 'selectAll', |
||||
|
title: '全选', |
||||
|
icon: <XIcon type={'icon-select-all'} />, |
||||
|
handler: shortcuts.selectAll.handler |
||||
|
}, |
||||
|
{ |
||||
|
key: 'paste', |
||||
|
title: '粘贴', |
||||
|
icon: <SnippetsOutlined />, |
||||
|
disabled: (flowChart: Graph) => flowChart.isClipboardEmpty(), |
||||
|
handler: shortcuts.paste.handler |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
export default blankMenuConfig; |
@ -0,0 +1,7 @@ |
|||||
|
import nodeMenuConfig from './node'; |
||||
|
import blankMenuConfig from './blank'; |
||||
|
|
||||
|
export { |
||||
|
nodeMenuConfig, |
||||
|
blankMenuConfig |
||||
|
}; |
@ -0,0 +1,66 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { |
||||
|
CopyOutlined, |
||||
|
EditOutlined, |
||||
|
CodeOutlined, |
||||
|
FormOutlined, |
||||
|
DeleteOutlined, |
||||
|
} from '@ant-design/icons'; |
||||
|
import XIcon from '../../../../components/xIcon'; |
||||
|
import shortcuts from '../../../../common/shortcuts'; |
||||
|
|
||||
|
const nodeMenuConfig = [ |
||||
|
{ |
||||
|
key: 'copy', |
||||
|
title: '复制', |
||||
|
icon: <CopyOutlined />, |
||||
|
handler: shortcuts.copy.handler |
||||
|
}, |
||||
|
{ |
||||
|
key: 'delete', |
||||
|
title: '删除', |
||||
|
icon: <DeleteOutlined />, |
||||
|
handler: shortcuts.delete.handler |
||||
|
}, |
||||
|
{ |
||||
|
key: 'rename', |
||||
|
title: '编辑文本', |
||||
|
icon: <EditOutlined />, |
||||
|
showDividerBehind: true, |
||||
|
handler() { |
||||
|
// TODO
|
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
key: 'bringToTop', |
||||
|
title: '置于顶层', |
||||
|
icon: <XIcon type={'icon-bringtotop'} />, |
||||
|
handler:shortcuts.bringToTop.handler |
||||
|
}, |
||||
|
{ |
||||
|
key: 'bringToBack', |
||||
|
title: '置于底层', |
||||
|
icon: <XIcon type={'icon-bringtobottom'} />, |
||||
|
showDividerBehind: true, |
||||
|
handler:shortcuts.bringToBack.handler |
||||
|
}, |
||||
|
{ |
||||
|
key: 'editCode', |
||||
|
title: '编辑代码', |
||||
|
icon: <FormOutlined />, |
||||
|
handler() { |
||||
|
// TODO
|
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
key: 'executeCode', |
||||
|
title: '执行代码', |
||||
|
icon: <CodeOutlined />, |
||||
|
handler() { |
||||
|
// TODO
|
||||
|
} |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
export default nodeMenuConfig; |
Loading…
Reference in new issue