vue3 多级右键菜单 升级版
2022-05-09 本文已影响0人
无题syl
多级菜单指令化
Directive
v-contextmenu="菜单数据"
参考一个开源项目 PPTIST
结果样式
image.png目录结构
指令
image.png
菜单template 组件
image.png
引入
vue3 使用指令 要在main.js中引入
import Directive from '@/plugins/directive'
app.use(Directive)
一 · 指令目录结构 文件
contextmenu.ts
import { Directive, createVNode, render, DirectiveBinding } from 'vue'
import ContextmenuComponent from '@/components/Contextmenu/index.vue'
const CTX_CONTEXTMENU_HANDLER = 'CTX_CONTEXTMENU_HANDLER'
const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {
event.stopPropagation()
event.preventDefault()
const menus = binding.value(el)
if (!menus) return
let container: HTMLDivElement | null = null
// 移除右键菜单并取消相关的事件监听
const removeContextmenu = () => {
if (container) {
document.body.removeChild(container)
container = null
}
el.classList.remove('contextmenu-active')
document.body.removeEventListener('scroll', removeContextmenu)
window.removeEventListener('resize', removeContextmenu)
}
// 创建自定义菜单
const options = {
axis: { x: event.x, y: event.y },
el,
menus,
removeContextmenu,
}
container = document.createElement('div')
const vm = createVNode(ContextmenuComponent, options, null)
render(vm, container)
document.body.appendChild(container)
// 为目标节点添加菜单激活状态的className
el.classList.add('contextmenu-active')
// 页面变化时移除菜单
document.body.addEventListener('scroll', removeContextmenu)
window.addEventListener('resize', removeContextmenu)
}
const ContextmenuDirective: Directive = {
mounted(el: HTMLElement, binding) {
el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)
el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
},
unmounted(el: HTMLElement) {
if (el && el[CTX_CONTEXTMENU_HANDLER]) {
el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
delete el[CTX_CONTEXTMENU_HANDLER]
}
},
}
export default ContextmenuDirective
plugin directive index.ts
import { App } from 'vue'
import Contextmenu from './contextmenu'
import ClickOutside from './clickOutside'
export default {
install(app: App) {
app.directive('contextmenu', Contextmenu)
app.directive('click-outside', ClickOutside)
}
}
二· 菜单组件 单文件组件
MenuContent.vue
<template>
<ul class="menu-content">
<template v-for="(menu, index) in menus" :key="menu.text || index">
<li
v-if="!menu.hide"
class="menu-item"
@click.stop="handleClickMenuItem(menu)"
:class="{'divider': menu.divider, 'disable': menu.disable}"
>
<div
class="menu-item-content"
:class="{
'has-children': menu.children,
'has-handler': menu.handler,
}"
v-if="!menu.divider"
>
<span class="text">{{menu.text}}</span>
<span class="sub-text" v-if="menu.subText && !menu.children">{{menu.subText}}</span>
<menu-content
class="sub-menu"
:menus="menu.children"
v-if="menu.children && menu.children.length"
:handleClickMenuItem="handleClickMenuItem"
/>
</div>
</li>
</template>
</ul>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { ContextmenuItem } from './types'
export default defineComponent({
name: 'menu-content',
props: {
menus: {
type: Array as PropType<ContextmenuItem[]>,
required: true,
},
handleClickMenuItem: {
type: Function,
required: true,
},
},
})
</script>
<style lang="scss" scoped>
$menuWidth: 170px;
$menuHeight: 30px;
$subMenuWidth: 120px;
.menu-content {
width: $menuWidth;
padding: 5px 0;
background: #fff;
border: 1px solid $borderColor;
box-shadow: $boxShadow;
border-radius: 2px;
list-style: none;
margin: 0;
}
.menu-item {
padding: 0 20px;
color: #555;
font-size: 12px;
transition: all $transitionDelayFast;
white-space: nowrap;
height: $menuHeight;
line-height: $menuHeight;
background-color: #fff;
cursor: pointer;
&:not(.disable):hover > .menu-item-content > .sub-menu {
display: block;
}
&:not(.disable):hover > .has-children.has-handler::after {
transform: scale(1);
}
&:hover:not(.disable) {
background-color: rgba($color: $themeColor, $alpha: .2);
}
&.divider {
height: 1px;
overflow: hidden;
margin: 5px;
background-color: #e5e5e5;
line-height: 0;
padding: 0;
}
&.disable {
color: #b1b1b1;
cursor: no-drop;
}
}
.menu-item-content {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
&.has-children::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-width: 1px;
border-style: solid;
border-color: #666 #666 transparent transparent;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%) rotate(45deg);
}
&.has-children.has-handler::after {
content: '';
display: inline-block;
width: 1px;
height: 24px;
background-color: #f1f1f1;
position: absolute;
right: 18px;
top: 3px;
transform: scale(0);
transition: transform $transitionDelay;
}
.sub-text {
opacity: 0.6;
}
.sub-menu {
width: $subMenuWidth;
position: absolute;
display: none;
left: 112%;
top: -6px;
}
}
</style>
index.vue
<template>
<div
class="mask"
@contextmenu.prevent="removeContextmenu()"
@mousedown="removeContextmenu()"
></div>
<div
class="contextmenu"
:style="{
left: style.left + 'px',
top: style.top + 'px',
}"
@contextmenu.prevent
>
<MenuContent
:menus="menus"
:handleClickMenuItem="handleClickMenuItem"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { ContextmenuItem, Axis } from './types'
import MenuContent from './MenuContent.vue'
export default defineComponent({
name: 'contextmenu',
components: {
MenuContent,
},
props: {
axis: {
type: Object as PropType<Axis>,
required: true,
},
el: {
type: Object as PropType<HTMLElement>,
required: true,
},
menus: {
type: Array as PropType<ContextmenuItem[]>,
required: true,
},
removeContextmenu: {
type: Function,
required: true,
},
},
setup(props) {
const style = computed(() => {
const MENU_WIDTH = 170
const MENU_HEIGHT = 30
const DIVIDER_HEIGHT = 11
const PADDING = 5
const { x, y } = props.axis
const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
const dividerCount = props.menus.filter(menu => menu.divider).length
const menuWidth = MENU_WIDTH
const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
const screenWidth = document.body.clientWidth
const screenHeight = document.body.clientHeight
return {
left: screenWidth <= x + menuWidth ? x - menuWidth : x,
top: screenHeight <= y + menuHeight ? y - menuHeight : y,
}
})
const handleClickMenuItem = (item: ContextmenuItem) => {
if (item.disable) return
if (item.children && !item.handler) return
if (item.handler) item.handler(props.el)
props.removeContextmenu()
}
return {
style,
handleClickMenuItem,
}
},
})
</script>
<style lang="scss">
.mask {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: 9998;
}
.contextmenu {
position: fixed;
z-index: 9999;
user-select: none;
}
</style>
type.ts
export interface ContextmenuItem {
text?: string;
subText?: string;
divider?: boolean;
disable?: boolean;
hide?: boolean;
children?: ContextmenuItem[];
handler?: (el: HTMLElement) => void;
}
export interface Axis {
x: number;
y: number;
}
三·用法
v-contextmenu="contextmenusThumbnails"
import { ContextmenuItem } from '@/components/Contextmenu/types'
const contextmenusThumbnails = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '开始演示',
subText: 'Ctrl + F',
handler: enterScreening,
},
]
}