37 changed files with 391 additions and 442 deletions
@ -1,3 +1,3 @@ |
|||
export { createContextMenu, destroyContextMenu } from './src/createContextMenu'; |
|||
|
|||
export * from './src/types'; |
|||
export * from './src/typing'; |
|||
|
|||
@ -1,117 +0,0 @@ |
|||
import './index.less'; |
|||
|
|||
import type { ContextMenuItem, ItemContentProps } from './types'; |
|||
import type { FunctionalComponent, CSSProperties } from 'vue'; |
|||
|
|||
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue'; |
|||
|
|||
import Icon from '/@/components/Icon'; |
|||
import { Menu, Divider } from 'ant-design-vue'; |
|||
|
|||
import { contextMenuProps } from './props'; |
|||
|
|||
const prefixCls = 'context-menu'; |
|||
|
|||
const ItemContent: FunctionalComponent<ItemContentProps> = (props) => { |
|||
const { item } = props; |
|||
return ( |
|||
<span |
|||
style="display: inline-block; width: 100%; " |
|||
class="px-4" |
|||
onClick={props.handler.bind(null, item)} |
|||
> |
|||
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />} |
|||
<span>{item.label}</span> |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ContextMenu', |
|||
props: contextMenuProps, |
|||
setup(props) { |
|||
const wrapRef = ref<ElRef>(null); |
|||
const showRef = ref(false); |
|||
|
|||
const getStyle = computed((): CSSProperties => { |
|||
const { axis, items, styles, width } = props; |
|||
const { x, y } = axis || { x: 0, y: 0 }; |
|||
const menuHeight = (items || []).length * 40; |
|||
const menuWidth = width; |
|||
const body = document.body; |
|||
|
|||
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x; |
|||
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y; |
|||
return { |
|||
...styles, |
|||
width: `${width}px`, |
|||
left: `${left + 1}px`, |
|||
top: `${top + 1}px`, |
|||
}; |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
nextTick(() => (showRef.value = true)); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
const el = unref(wrapRef); |
|||
el && document.body.removeChild(el); |
|||
}); |
|||
|
|||
function handleAction(item: ContextMenuItem, e: MouseEvent) { |
|||
const { handler, disabled } = item; |
|||
if (disabled) return; |
|||
showRef.value = false; |
|||
|
|||
e?.stopPropagation(); |
|||
e?.preventDefault(); |
|||
handler?.(); |
|||
} |
|||
|
|||
function renderMenuItem(items: ContextMenuItem[]) { |
|||
return items.map((item) => { |
|||
const { disabled, label, children, divider = false } = item; |
|||
|
|||
const DividerComp = divider ? <Divider key={`d-${label}`} /> : null; |
|||
if (!children || children.length === 0) { |
|||
return ( |
|||
<> |
|||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}> |
|||
<ItemContent showIcon={props.showIcon} item={item} handler={handleAction} /> |
|||
</Menu.Item> |
|||
{DividerComp} |
|||
</> |
|||
); |
|||
} |
|||
if (!unref(showRef)) return null; |
|||
|
|||
return ( |
|||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}> |
|||
{{ |
|||
title: () => ( |
|||
<ItemContent showIcon={props.showIcon} item={item} handler={handleAction} /> |
|||
), |
|||
default: () => renderMenuItem(children), |
|||
}} |
|||
</Menu.SubMenu> |
|||
); |
|||
}); |
|||
} |
|||
return () => { |
|||
const { items } = props; |
|||
if (!unref(showRef)) return null; |
|||
return ( |
|||
<Menu |
|||
inlineIndent={12} |
|||
mode="vertical" |
|||
class={prefixCls} |
|||
ref={wrapRef} |
|||
style={unref(getStyle)} |
|||
> |
|||
{renderMenuItem(items)} |
|||
</Menu> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -0,0 +1,204 @@ |
|||
<script lang="tsx"> |
|||
import type { ContextMenuItem, ItemContentProps, Axis } from './typing'; |
|||
import type { FunctionalComponent, CSSProperties } from 'vue'; |
|||
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue'; |
|||
import Icon from '/@/components/Icon'; |
|||
import { Menu, Divider } from 'ant-design-vue'; |
|||
|
|||
const prefixCls = 'context-menu'; |
|||
|
|||
const props = { |
|||
width: { type: Number, default: 156 }, |
|||
customEvent: { type: Object as PropType<Event>, default: null }, |
|||
styles: { type: Object as PropType<CSSProperties> }, |
|||
showIcon: { type: Boolean, default: true }, |
|||
axis: { |
|||
// The position of the right mouse button click |
|||
type: Object as PropType<Axis>, |
|||
default() { |
|||
return { x: 0, y: 0 }; |
|||
}, |
|||
}, |
|||
items: { |
|||
// The most important list, if not, will not be displayed |
|||
type: Array as PropType<ContextMenuItem[]>, |
|||
default() { |
|||
return []; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
const ItemContent: FunctionalComponent<ItemContentProps> = (props) => { |
|||
const { item } = props; |
|||
return ( |
|||
<span |
|||
style="display: inline-block; width: 100%; " |
|||
class="px-4" |
|||
onClick={props.handler.bind(null, item)} |
|||
> |
|||
{props.showIcon && item.icon && <Icon class="mr-2" icon={item.icon} />} |
|||
<span>{item.label}</span> |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'ContextMenu', |
|||
props, |
|||
setup(props) { |
|||
const wrapRef = ref<ElRef>(null); |
|||
const showRef = ref(false); |
|||
|
|||
const getStyle = computed((): CSSProperties => { |
|||
const { axis, items, styles, width } = props; |
|||
const { x, y } = axis || { x: 0, y: 0 }; |
|||
const menuHeight = (items || []).length * 40; |
|||
const menuWidth = width; |
|||
const body = document.body; |
|||
|
|||
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x; |
|||
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y; |
|||
return { |
|||
...styles, |
|||
width: `${width}px`, |
|||
left: `${left + 1}px`, |
|||
top: `${top + 1}px`, |
|||
}; |
|||
}); |
|||
|
|||
onMounted(() => { |
|||
nextTick(() => (showRef.value = true)); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
unref(wrapRef) && document.body.removeChild(el); |
|||
}); |
|||
|
|||
function handleAction(item: ContextMenuItem, e: MouseEvent) { |
|||
const { handler, disabled } = item; |
|||
if (disabled) { |
|||
return; |
|||
} |
|||
showRef.value = false; |
|||
e?.stopPropagation(); |
|||
e?.preventDefault(); |
|||
handler?.(); |
|||
} |
|||
|
|||
function renderMenuItem(items: ContextMenuItem[]) { |
|||
return items.map((item) => { |
|||
const { disabled, label, children, divider = false } = item; |
|||
|
|||
const contentProps = { |
|||
item, |
|||
handler: handleAction, |
|||
showIcon: props.showIcon, |
|||
}; |
|||
|
|||
if (!children || children.length === 0) { |
|||
return ( |
|||
<> |
|||
<Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}> |
|||
<ItemContent {...contentProps} /> |
|||
</Menu.Item> |
|||
{divider ? <Divider key={`d-${label}`} /> : null} |
|||
</> |
|||
); |
|||
} |
|||
if (!unref(showRef)) return null; |
|||
|
|||
return ( |
|||
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}> |
|||
{{ |
|||
title: () => <ItemContent {...contentProps} />, |
|||
default: () => renderMenuItem(children), |
|||
}} |
|||
</Menu.SubMenu> |
|||
); |
|||
}); |
|||
} |
|||
return () => { |
|||
const { items } = props; |
|||
if (!unref(showRef)) return null; |
|||
return ( |
|||
<Menu |
|||
inlineIndent={12} |
|||
mode="vertical" |
|||
class={prefixCls} |
|||
ref={wrapRef} |
|||
style={unref(getStyle)} |
|||
> |
|||
{renderMenuItem(items)} |
|||
</Menu> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@default-height: 42px !important; |
|||
|
|||
@small-height: 36px !important; |
|||
|
|||
@large-height: 36px !important; |
|||
|
|||
.item-style() { |
|||
li { |
|||
display: inline-block; |
|||
width: 100%; |
|||
height: @default-height; |
|||
margin: 0 !important; |
|||
line-height: @default-height; |
|||
|
|||
span { |
|||
line-height: @default-height; |
|||
} |
|||
|
|||
> div { |
|||
margin: 0 !important; |
|||
} |
|||
|
|||
&:not(.ant-menu-item-disabled):hover { |
|||
color: @text-color-base; |
|||
background-color: @item-hover-bg; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.context-menu { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 200; |
|||
display: block; |
|||
width: 156px; |
|||
margin: 0; |
|||
list-style: none; |
|||
background-color: @component-background; |
|||
border: 1px solid rgba(0, 0, 0, 0.08); |
|||
border-radius: 0.25rem; |
|||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.1), |
|||
0 1px 5px 0 rgba(0, 0, 0, 0.06); |
|||
background-clip: padding-box; |
|||
user-select: none; |
|||
|
|||
.item-style(); |
|||
|
|||
.ant-divider { |
|||
margin: 0 0; |
|||
} |
|||
|
|||
&__popup { |
|||
.ant-divider { |
|||
margin: 0 0; |
|||
} |
|||
|
|||
.item-style(); |
|||
} |
|||
|
|||
.ant-menu-submenu-title, |
|||
.ant-menu-item { |
|||
padding: 0 !important; |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,65 +0,0 @@ |
|||
@default-height: 42px !important; |
|||
|
|||
@small-height: 36px !important; |
|||
|
|||
@large-height: 36px !important; |
|||
|
|||
.item-style() { |
|||
li { |
|||
display: inline-block; |
|||
width: 100%; |
|||
height: @default-height; |
|||
margin: 0 !important; |
|||
line-height: @default-height; |
|||
|
|||
span { |
|||
line-height: @default-height; |
|||
} |
|||
|
|||
> div { |
|||
margin: 0 !important; |
|||
} |
|||
|
|||
&:not(.ant-menu-item-disabled):hover { |
|||
color: @text-color-base; |
|||
background-color: @item-hover-bg; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.context-menu { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 200; |
|||
display: block; |
|||
width: 156px; |
|||
margin: 0; |
|||
list-style: none; |
|||
background-color: @component-background; |
|||
border: 1px solid rgba(0, 0, 0, 0.08); |
|||
border-radius: 0.25rem; |
|||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.1), |
|||
0 1px 5px 0 rgba(0, 0, 0, 0.06); |
|||
background-clip: padding-box; |
|||
user-select: none; |
|||
|
|||
.item-style(); |
|||
|
|||
.ant-divider { |
|||
margin: 0 0; |
|||
} |
|||
|
|||
&__popup { |
|||
.ant-divider { |
|||
margin: 0 0; |
|||
} |
|||
|
|||
.item-style(); |
|||
} |
|||
|
|||
.ant-menu-submenu-title, |
|||
.ant-menu-item { |
|||
padding: 0 !important; |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
import type { PropType } from 'vue'; |
|||
import type { Axis, ContextMenuItem } from './types'; |
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
export const contextMenuProps = { |
|||
width: propTypes.number.def(156), |
|||
customEvent: { |
|||
type: Object as PropType<Event>, |
|||
default: null, |
|||
}, |
|||
styles: propTypes.style, |
|||
showIcon: propTypes.bool.def(true), |
|||
axis: { |
|||
// The position of the right mouse button click
|
|||
type: Object as PropType<Axis>, |
|||
default() { |
|||
return { x: 0, y: 0 }; |
|||
}, |
|||
}, |
|||
items: { |
|||
// The most important list, if not, will not be displayed
|
|||
type: Array as PropType<ContextMenuItem[]>, |
|||
default() { |
|||
return []; |
|||
}, |
|||
}, |
|||
}; |
|||
Loading…
Reference in new issue