前言:
最近遇到个需求,需要实现一个用户可选择的菜单,这个菜单是在拖拽放置后弹出,让用户进行选择,决定放置的位置,可以通过点击视口其他位置关闭菜单
先看看怎么用的:
根据menuSelected决定后面如何操作
const menuSelected = await this.callMenu({
pos: { x: e.clientX, y: e.clientY }
});
第一步,菜单组件实现:
这一步非常简单,直接按照平常实现组件即可,但是事件处理方法先不需要写里面内容:
<template>
<ul
class="menu-list"
:style="{
left: pos.x + 'px',
top: pos.y + 'px',
}"
>
<template v-for="option of menuOptions">
<li
v-else
class="menu-item"
:key="option.name"
@mousedown="handleMenuSelect(option)"
>
<svg-icon
:icon-class="(option.icon || option.name.toLowerCase()) + '-icon'"
></svg-icon>
<span class="name">{{ option.name }}</span>
<span class="short-cut">{{ option.shortCut }}</span>
</li>
</template>
</ul>
</template>
<script>
export default {
data: {
menuOptions: [
{
name: MENU_OPTIONS.REPLACE,
shortCut: ""
},
{
name: MENU_OPTIONS.COPY,
shortCut: "⌘C"
},
... // 其他菜单内容,这里省略
]
},
methods: {
handleMenuSelect(){} // 这里方法写空的,占位防止报错,具体内容不在这里写
}
}
</script>
第二步:
创建一个js文件,引入这个组件,继承他的构造器:
import MenuList from "./MenuList.vue";
const MenuListCtor = Vue.extend(MenuList);
第三步:
挂载一个方法到Vue的原型链上,为了让实例可以直接调用,另外让这个方法返回一个promise(考虑promise的resolve的触发节点和reject的触发节点):
Vue.prototype.callMenu = function({ hover, pos }) {
return new Promise((resolve, reject) => {
});
};
第四步:
生成这个继承后组件的实例,并为其绑定之前SFC内的handleMeuSelect方法
const instance = new MenuListCtor();
instance.hover = hover;
instance.pos = pos;
instance.handleMeuSelect = ({ name }) => resolve(name) // 覆盖了之前SFC内的方法
第五步:
创建一个未被插入的dom节点(通过$mount方法),并挂载到body上:
const { $el } = instance.$mount();
document.body.appendChild($el);
$el.addEventListener("click", e => e.stopPropagation()); // 防止点击穿透,触发到document的隐藏菜单事件
第六步:
处理菜单的关闭,绑定到body上:
function eventHandler() {
instance.$destroy();
$el.parentNode?.removeChild($el);
reject();
}
document.body.addEventListener("mousedown", eventHandler, { once: true });
美化:
加入缩放自动关闭菜单和超出视口的ui优化,提升体验
if (window.innerHeight - pos.y < pos.y) {
pos.y = pos.y - $el.offsetHeight - 20;
if (pos.y < 0) {
$el.style.height = $el.offsetHeight + pos.y + "px";
pos.y = 0;
}
} else {
if (pos.y + $el.offsetHeight > window.innerHeight) {
$el.style.height = window.innerHeight - pos.y - 20 + "px";
}
}
window.addEventListener("resize", eventHandler, { once: true });
完整代码:
还加入了重复判断(通过closed变量)有一些优化处理,之前没介绍
import Vue from "vue";
import MenuList from "./MenuList.vue";
const MenuListCtor = Vue.extend(MenuList);
let closed = true;
Vue.prototype.callMenu = function({ hover, pos }) {
return new Promise((resolve, reject) => {
// 防止重复处理:
if (!closed) {
return reject();
}
closed = false;
// 组件处理
const instance = new MenuListCtor();
instance.hover = hover;
instance.pos = pos;
instance.handleClick = ({ name }) => resolve(name);
// DOM处理
const { $el } = instance.$mount();
document.body.appendChild($el);
$el.addEventListener("click", e => e.stopPropagation());
// 美化样式
if (window.innerHeight - pos.y < pos.y) {
pos.y = pos.y - $el.offsetHeight - 20;
if (pos.y < 0) {
$el.style.height = $el.offsetHeight + pos.y + "px";
pos.y = 0;
}
} else {
if (pos.y + $el.offsetHeight > window.innerHeight) {
$el.style.height = window.innerHeight - pos.y - 20 + "px";
}
}
// 事件处理
document.body.addEventListener("mousedown", eventHandler, { once: true });
window.addEventListener("resize", eventHandler, { once: true });
function eventHandler() {
instance.$destroy();
$el.parentNode?.removeChild($el);
closed = true;
reject();
}
});
};