mirror of
https://github.com/thelounge/thelounge
synced 2024-11-22 20:13:07 +00:00
221 lines
5.4 KiB
Vue
221 lines
5.4 KiB
Vue
<template>
|
|
<div
|
|
v-if="isOpen"
|
|
id="context-menu-container"
|
|
:class="{passthrough}"
|
|
@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)"
|
|
>
|
|
<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"
|
|
@mouseenter="hoverItem(id)"
|
|
@click="clickItem(item)"
|
|
>
|
|
{{ item.label }}
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import {
|
|
generateUserContextMenu,
|
|
generateChannelContextMenu,
|
|
generateInlineChannelContextMenu,
|
|
} from "../js/helpers/contextMenu.js";
|
|
import eventbus from "../js/eventbus";
|
|
|
|
export default {
|
|
name: "ContextMenu",
|
|
props: {
|
|
message: Object,
|
|
},
|
|
data() {
|
|
return {
|
|
isOpen: false,
|
|
passthrough: false,
|
|
previousActiveElement: null,
|
|
items: [],
|
|
activeItem: -1,
|
|
style: {
|
|
left: 0,
|
|
top: 0,
|
|
},
|
|
};
|
|
},
|
|
mounted() {
|
|
eventbus.on("escapekey", this.close);
|
|
eventbus.on("contextmenu:cancel", this.close);
|
|
eventbus.on("contextmenu:user", this.openUserContextMenu);
|
|
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
|
|
eventbus.on("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
|
},
|
|
destroyed() {
|
|
eventbus.off("escapekey", this.close);
|
|
eventbus.off("contextmenu:cancel", this.close);
|
|
eventbus.off("contextmenu:user", this.openUserContextMenu);
|
|
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
|
|
eventbus.off("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
|
|
|
this.close();
|
|
},
|
|
methods: {
|
|
enablePointerEvents() {
|
|
this.passthrough = false;
|
|
document.body.removeEventListener("pointerup", this.enablePointerEvents, {
|
|
passive: true,
|
|
});
|
|
},
|
|
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,
|
|
});
|
|
}
|
|
|
|
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);
|
|
},
|
|
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: [],
|
|
}
|
|
);
|
|
this.open(data.event, items);
|
|
},
|
|
open(event, items) {
|
|
event.preventDefault();
|
|
|
|
this.previousActiveElement = document.activeElement;
|
|
this.items = items;
|
|
this.activeItem = 0;
|
|
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";
|
|
this.$refs.contextMenu.focus();
|
|
});
|
|
},
|
|
close() {
|
|
if (!this.isOpen) {
|
|
return;
|
|
}
|
|
|
|
this.isOpen = false;
|
|
this.items = [];
|
|
|
|
if (this.previousActiveElement) {
|
|
this.previousActiveElement.focus();
|
|
this.previousActiveElement = null;
|
|
}
|
|
},
|
|
hoverItem(id) {
|
|
this.activeItem = id;
|
|
},
|
|
clickItem(item) {
|
|
this.close();
|
|
|
|
if (item.action) {
|
|
item.action();
|
|
} else if (item.link) {
|
|
this.$router.push(item.link);
|
|
}
|
|
},
|
|
clickActiveItem() {
|
|
if (this.items[this.activeItem]) {
|
|
this.clickItem(this.items[this.activeItem]);
|
|
}
|
|
},
|
|
navigateMenu(direction) {
|
|
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") {
|
|
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();
|
|
}
|
|
},
|
|
positionContextMenu(event) {
|
|
const element = event.target;
|
|
const menuWidth = this.$refs.contextMenu.offsetWidth;
|
|
const menuHeight = this.$refs.contextMenu.offsetHeight;
|
|
|
|
if (element && element.classList.contains("menu")) {
|
|
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>
|