最近老项目需要用 Vue2 实现图片空间,效果如下:
其中有个点击右键弹出菜单的功能,当所在容器发生滚动事件时,需要隐藏弹出的右键菜单,实现代码如下:
<template>
<div class="ih-contextmenu" @contextmenu.prevent.stop="">
<ul>
<li v-for="(menu, i) in menus" :key="i">
<button
type="button"
v-if="menu != null"
class="ih-contextmenu-command"
@click.prevent.stop="$emit('click', menu.key, data)"
ref="command"
>
<span class="ih-contextmenu-icon" v-if="hasIcon">
<ih-icon :name="menu.icon" v-if="menu.icon" />
</span>
<span class="ih-contextmenu-text">{{ menu.label }}</span>
</button>
</li>
</ul>
</div>
</template>
<script>
import IhIcon from "@/components/IhIcon";
import { alignPoint } from "dom-align";
/**
* 判断元素是否可以滚动
*
* @link https://segmentfault.com/a/1190000021934483
*
* @param {Node} el
* @return {boolean}
*/
function isScrollable(el) {
if (!(el instanceof HTMLElement)) {
console.log("fuck off");
return false;
}
if (el.scrollTop > 0) {
return true;
}
el.scrollTop++;
// 元素不能滚动的话,scrollTop 设置不会生效,还会置为 0
const top = el.scrollTop;
// 重置滚动位置
top && (el.scrollTop = 0);
return top > 0;
}
/**
* 获取滚动容器
* @param el
* @return {HTMLElement|null}
*/
export function getScrollContainer(el) {
let container = el.parentNode;
while (container) {
if (isScrollable(container)) {
return container;
}
container = container.parentNode;
}
return null;
}
export default {
name: "IhContextmenu",
components: {
IhIcon,
},
props: {
menus: {
type: Array,
required: true,
validator(menus) {
return menus.every(
(m) => m == null || (m.label != null && m.key != null)
);
},
},
points: {
type: Object,
required: true,
},
// 附加数据
data: {},
},
computed: {
hasIcon() {
return this.menus.some((m) => m?.icon != null);
},
},
methods: {
requestLayout() {
alignPoint(this.$el, this.points, {
points: ["tl", "tr"],
overflow: { adjustX: true, adjustY: true },
useCssTransform: true,
});
},
requestFocus() {
const commands = this.$refs.command;
if (!commands?.length) return;
commands[0].focus();
},
// 快捷键(快速关闭)
bindKeyboardEvent() {
const window = this.$el.ownerDocument.defaultView;
const tabKeydownListener = (event) => {
if (event.code === "Escape") {
this.$emit("escape");
this.$emit("close");
}
};
window.addEventListener("keydown", tabKeydownListener, true);
this.$once("hook:beforeDestroy", () => {
window.removeEventListener("keydown", tabKeydownListener, true);
});
},
// 滚动关闭
bindScrollEvent() {
const container = getScrollContainer(this.$el) ?? this.$el.ownerDocument.defaultView;
const scrollListener = () => {
// FIXME:利用设置 scrollTop 值来检测元素是否可滚动,导致的 BUG,有点莫名奇妙
// 解决方法:忽略掉第一次触发的滚动事件。
// 判断可滚动参考:https://segmentfault.com/a/1190000021934483
if (this.closableOfScroll) {
this.$emit("close");
} else {
this.closableOfScroll = true;
}
};
if (container != null) {
container.addEventListener("scroll", scrollListener, true);
this.$once("hook:beforeDestroy", () => {
container.removeEventListener("scroll", scrollListener, true);
});
}
},
},
mounted() {
this.requestLayout();
this.requestFocus();
this.bindKeyboardEvent();
this.bindScrollEvent();
},
updated() {
this.requestLayout();
},
};
</script>
在组件 mounted 的时候,调用 this.bindScrollEvent(); 绑定滚动关闭右键菜单的功能后**,必须手动对可滚动容器触发滚动事件**后,才能够唤起右键菜单。
解决方法有点取巧,代码还是在 bindScrollEvent 里面:
// 滚动关闭
bindScrollEvent() {
const container = getScrollContainer(this.$el) ?? this.$el.ownerDocument.defaultView;
const scrollListener = () => {
// FIXME:利用设置 scrollTop 值来检测元素是否可滚动,导致的 BUG,有点莫名奇妙
// 解决方法:忽略掉第一次触发的滚动事件。
// 判断可滚动参考:https://segmentfault.com/a/1190000021934483
if (this.closeByScroll) {
this.$emit("close");
} else {
this.closeByScroll = true;
}
};
if (container != null) {
container.addEventListener("scroll", scrollListener, true);
this.$once("hook:beforeDestroy", () => {
container.removeEventListener("scroll", scrollListener, true);
});
}
},
弄不明白为什么会出现这个问题。那位大佬帮忙看看。
源码:tint/image-hosting: 图片空间组件 (github.com)
另外再问个问题,如何限制键盘Tab键响应的有效范围,例如:
我希望点击键盘的 Tab 按键时,响应的范围在唤起的右键菜单内循环。