smallstonesk
4 years ago
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