thelounge/client/components/ContextMenu.vue

222 lines
5.4 KiB
Vue
Raw Normal View History

2019-11-09 22:21:34 +00:00
<template>
2019-11-18 20:05:47 +00:00
<div
v-if="isOpen"
id="context-menu-container"
:class="{passthrough}"
2019-11-18 20:05:47 +00:00
@click="containerClick"
@contextmenu.prevent="containerClick"
@keydown.exact.up.prevent="navigateMenu(-1)"
@keydown.exact.down.prevent="navigateMenu(1)"
@keydown.exact.tab.prevent="navigateMenu(1)"
@keydown.shift.tab.prevent="navigateMenu(-1)"
2019-11-18 20:05:47 +00:00
>
<ul
id="context-menu"
ref="contextMenu"
role="menu"
:style="style"
tabindex="-1"
@mouseleave="activeItem = -1"
@keydown.enter.prevent="clickActiveItem"
>
<template v-for="(item, id) of items">
<li
:key="item.name"
:class="[
'context-menu-' + item.type,
item.class ? 'context-menu-' + item.class : null,
{active: id === activeItem},
]"
role="menuitem"
2020-01-27 09:44:36 +00:00
@mouseenter="hoverItem(id)"
2019-11-18 20:05:47 +00:00
@click="clickItem(item)"
>
{{ item.label }}
</li>
</template>
2019-11-09 22:21:34 +00:00
</ul>
</div>
</template>
<script>
import {
generateUserContextMenu,
generateChannelContextMenu,
generateInlineChannelContextMenu,
} from "../js/helpers/contextMenu.js";
2020-03-16 17:58:40 +00:00
import eventbus from "../js/eventbus";
2019-11-09 22:21:34 +00:00
export default {
name: "ContextMenu",
props: {
message: Object,
},
data() {
return {
isOpen: false,
passthrough: false,
2019-11-09 22:21:34 +00:00
previousActiveElement: null,
items: [],
2019-11-18 20:05:47 +00:00
activeItem: -1,
2019-11-09 22:21:34 +00:00
style: {
left: 0,
top: 0,
},
};
},
mounted() {
2020-03-16 17:58:40 +00:00
eventbus.on("escapekey", this.close);
eventbus.on("contextmenu:cancel", this.close);
2020-03-16 17:58:40 +00:00
eventbus.on("contextmenu:user", this.openUserContextMenu);
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
eventbus.on("contextmenu:inline-channel", this.openInlineChannelContextMenu);
2019-11-09 22:21:34 +00:00
},
2019-11-18 19:18:35 +00:00
destroyed() {
2020-03-16 17:58:40 +00:00
eventbus.off("escapekey", this.close);
eventbus.off("contextmenu:cancel", this.close);
2020-03-16 17:58:40 +00:00
eventbus.off("contextmenu:user", this.openUserContextMenu);
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
eventbus.off("contextmenu:inline-channel", this.openInlineChannelContextMenu);
this.close();
2019-11-18 19:18:35 +00:00
},
2019-11-09 22:21:34 +00:00
methods: {
enablePointerEvents() {
this.passthrough = false;
document.body.removeEventListener("pointerup", this.enablePointerEvents, {
passive: true,
});
},
2019-11-23 14:26:20 +00:00
openChannelContextMenu(data) {
if (data.event.type === "contextmenu") {
// Pass through all pointer events to allow the network list's
// dragging events to continue triggering.
this.passthrough = true;
document.body.addEventListener("pointerup", this.enablePointerEvents, {
passive: true,
});
}
2019-11-23 14:26:20 +00:00
const items = generateChannelContextMenu(this.$root, data.channel, data.network);
this.open(data.event, items);
},
openInlineChannelContextMenu(data) {
const {network} = this.$store.state.activeChannel;
const items = generateInlineChannelContextMenu(this.$root, data.channel, network);
this.open(data.event, items);
},
2019-11-23 14:26:20 +00:00
openUserContextMenu(data) {
const {network, channel} = this.$store.state.activeChannel;
const items = generateUserContextMenu(
this.$root,
channel,
network,
channel.users.find((u) => u.nick === data.user.nick) || {
nick: data.user.nick,
modes: [],
}
2019-11-23 14:26:20 +00:00
);
this.open(data.event, items);
},
2019-11-09 22:21:34 +00:00
open(event, items) {
2019-11-18 20:05:47 +00:00
event.preventDefault();
this.previousActiveElement = document.activeElement;
2019-11-09 22:21:34 +00:00
this.items = items;
2019-11-18 20:05:47 +00:00
this.activeItem = 0;
2019-11-09 22:21:34 +00:00
this.isOpen = true;
// Position the menu and set the focus on the first item after it's size has updated
this.$nextTick(() => {
const pos = this.positionContextMenu(event);
this.style.left = pos.left + "px";
this.style.top = pos.top + "px";
2019-11-18 20:05:47 +00:00
this.$refs.contextMenu.focus();
2019-11-09 22:21:34 +00:00
});
},
close() {
if (!this.isOpen) {
return;
}
this.isOpen = false;
2019-11-18 20:05:47 +00:00
this.items = [];
2019-11-09 22:21:34 +00:00
if (this.previousActiveElement) {
this.previousActiveElement.focus();
this.previousActiveElement = null;
}
},
2019-11-18 20:05:47 +00:00
hoverItem(id) {
this.activeItem = id;
},
2019-11-09 22:21:34 +00:00
clickItem(item) {
this.close();
2019-11-09 22:21:34 +00:00
if (item.action) {
item.action();
} else if (item.link) {
this.$router.push(item.link);
2019-11-18 20:05:47 +00:00
}
},
clickActiveItem() {
if (this.items[this.activeItem]) {
this.clickItem(this.items[this.activeItem]);
}
},
navigateMenu(direction) {
2019-11-18 20:05:47 +00:00
let currentIndex = this.activeItem;
currentIndex += direction;
const nextItem = this.items[currentIndex];
// If the next item we would select is a divider, skip over it
if (nextItem && nextItem.type === "divider") {
2019-11-18 20:05:47 +00:00
currentIndex += direction;
}
if (currentIndex < 0) {
currentIndex += this.items.length;
}
if (currentIndex > this.items.length - 1) {
currentIndex -= this.items.length;
}
this.activeItem = currentIndex;
},
containerClick(event) {
if (event.currentTarget === event.target) {
this.close();
2019-11-09 22:21:34 +00:00
}
},
positionContextMenu(event) {
const element = event.target;
const menuWidth = this.$refs.contextMenu.offsetWidth;
const menuHeight = this.$refs.contextMenu.offsetHeight;
if (element && element.classList.contains("menu")) {
2019-11-09 22:21:34 +00:00
return {
left: element.getBoundingClientRect().left - (menuWidth - element.offsetWidth),
top: element.getBoundingClientRect().top + element.offsetHeight,
};
}
const offset = {left: event.pageX, top: event.pageY};
if (window.innerWidth - offset.left < menuWidth) {
offset.left = window.innerWidth - menuWidth;
}
if (window.innerHeight - offset.top < menuHeight) {
offset.top = window.innerHeight - menuHeight;
}
return offset;
},
},
};
</script>