描述
右键菜单因该还是是比较常见的功能,一般都是通过触发事件控制一个菜单的显示隐藏,尤其是在可视化编辑器中更为突出,而且vue3
现在社区也不完善,所以就想用vue3
+ts
写一款右键菜单组件,支持vue3
,ts
。
技术栈
- vue3
- typescript
- lodash
- vue3(hooks)
正式开始
1. 新建types.ts
文件,该文件用作vue3-context-menu
的类型声明。
export type Position = {
x: number
y: number
}
export type ContextMenuItem = {
label: string
icon?: string
disabled?: boolean
handler?: (...rest: any[]) => void
children?: ContextMenuItem[]
}
export type ContextMenuProps = {
menuClass?: string
menus: ContextMenuItem[]
position?: Position
width?: number
}
export type ItemContentProps = {
item: ContextMenuItem
handler: (...rest: any[]) => void
}
export type CreateContextOptions = ContextMenuProps & {
event: MouseEvent
}
2. 新建index.tsx
文件,该文件用作vue3-context-menu
的视图层。
全局参数
import { defineComponent, FunctionalComponent, PropType, Transition } from 'vue'
import { Menu } from 'ant-design-vue'
import { ItemContentProps, Position, ContextMenuItem } from './types'
import SvgIcon from '../svg-icon/index.vue'
const Item: FunctionalComponent<ItemContentProps> = props => {
const { item, handler } = props
return (
<div onClick={e => handler(item, e)}>
{!!item.icon && <SvgIcon class="mr8" name={item.icon} />}
<span>{item.label}</span>
</div>
)
}
导出模板组件,该组件未创建挂载
export default defineComponent({
name: 'ContextMenu',
inheritAttrs: false,
emits: ['update:visible'],
props: {
width: {
type: Number,
default: 160
},
menuClass: {
type: String,
default: ''
},
position: {
type: Object as PropType<Position>,
default: () => ({ x: 0, y: 0 })
},
menus: {
type: Array as PropType<ContextMenuItem[]>,
default: () => []
},
visible: {
type: Boolean,
default: false
}
},
mounted() {
document.body.addEventListener('click', this.hide)
},
unmounted() {
document.body.removeEventListener('click', this.hide)
},
methods: {
hide() {
this.$emit('update:visible', false)
},
handleAction(item: ContextMenuItem, e: MouseEvent) {
const { handler, disabled } = item
if (disabled) {
return
}
e.stopPropagation()
e.preventDefault()
handler && handler(item, e)
this.hide()
}
},
render() {
const self = this // eslint-disable-line
const { visible } = this
function renderMenuItem(menus: ContextMenuItem[]) {
return menus.map(item => {
const { disabled, label, children } = item
if (!children || !children.length) {
return (
<Menu.Item disabled={disabled} class="context-menu-item" key={label}>
<Item item={item} handler={self.handleAction} />
</Menu.Item>
)
}
return (
<Menu.SubMenu key={label} disabled={disabled} popupClassName="context-menu-popup">
{{
// slots
title: () => <Item item={item} handler={self.handleAction} />,
default: () => renderMenuItem(children)
}}
</Menu.SubMenu>
)
})
}
return (
<Transition name="com-fade-in" appear>
{visible && (
<div>
<Menu
class={`context-menu ${this.menuClass}`}
mode="vertical"
style={{
width: this.width + 'px',
top: this.position.y + 'px',
left: this.position.x + 'px'
}}
>
{renderMenuItem(this.menus)}
</Menu>
</div>
)}
</Transition>
)
}
})
2. 新建create-context-menu.ts
文件,该文件用作vue3-context-menu
的逻辑层,采用vue中的hooks
思想进行编写。
全局参数
import { h, ComponentPublicInstance, ref, getCurrentInstance } from 'vue'
export type CreateContextMenuProps = ContextMenuProps & {
event: MouseEvent
}
export type ContextMenuInstance = ComponentPublicInstance<
{},
{
open: (opts: ContextMenuProps) => void
close: () => void
}
>
const defaultProps: ContextMenuProps = {
width: 160,
menuClass: '',
position: { x: 0, y: 0 },
menus: []
}
let ins: ContextMenuInstance | null = null
生成右键信息和模板
function createInstance() {
const comp = {
setup() {
const visible = ref(false)
let attrs: Record<string, unknown> = {}
const open = (opts: ContextMenuProps) => {
visible.value = true
attrs = opts
}
const close = () => {
visible.value = false
}
const toggle = (val: boolean) => {
visible.value = val
}
const render = () => {
attrs = {
...attrs,
visible: visible.value,
'onUpdate:visible': toggle
}
return h(ContextMenu, attrs)
}
// 重新渲染函数
;(getCurrentInstance() as any).render = render
return {
open,
close
}
}
}
// mountComponent方法省略,就是一个`vue`中的`createApp`方法,用来挂载组件的
const { instance } = mountComponent(comp, 'context-menu-wrapper-root')
return instance
}
获取createInstance创建的信息
function getInstance() {
if (ins) {
return ins
}
ins = createInstance() as ContextMenuInstance
return ins
}
将创建的右键组件信息,以一个对象方式导出,动态创建组件和删除组件,类似于vue2
中的 vue.extend()
,这样方便我们在操作层创建组件,解决了在视图层直接显示组件的缺点
function createContextMenu(options: CreateContextMenuProps) {
const { event } = options
event.preventDefault()
const props = Object.assign({}, defaultProps, omit(options, ['event']), {
position: {
x: event.clientX,
y: event.clientY
}
})
const mInstance = getInstance()
mInstance.close()
setTimeout(() => {
mInstance.open(props)
}, 20)
return mInstance
}
export { createContextMenu }
使用
// 该代码省略了很多
<template>
<div class="com-page p20">
<a-row type="flex" class="menu-container" align="middle" @contextmenu="onContainerRightClick">
<ComImage className="image" :src="logo" alt="logo" />
<h2 class="title">{{ data.title }}</h2>
<section class="font-size-16">{{ data.description }}</section>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import { createContextMenu } from '@/components/context-menu/create-context-menu'
export default defineComponent({
setup() {
function onContainerRightClick(e: MouseEvent) {
createContextMenu({
event: e,
menus: [
{
label: '编辑',
handler() {
visible.value = true
}
},
{
label: '复制标题',
handler(item, event: MouseEvent) {
copy(form.title, event)
message.destroy()
message.success('复制成功,ctrl+v进行粘贴')
}
}
]
})
}
}
})
</script>
总结
预览地址,账号:lgf@163.com,密码:123456。
vue3-context-menu插件来自ant-simple-pro里面,ant-simple-pro有很多用
vue3+ts
开发的插件。ant-simple-pro简洁,美观,快速上手,支持3大框架。