<template> <div v-if="isOpen" id="context-menu-container" @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} from "../js/helpers/contextMenu.js"; import eventbus from "../js/eventbus"; export default { name: "ContextMenu", props: { message: Object, }, data() { return { isOpen: false, previousActiveElement: null, items: [], activeItem: -1, style: { left: 0, top: 0, }, }; }, mounted() { eventbus.on("escapekey", this.close); eventbus.on("contextmenu:user", this.openUserContextMenu); eventbus.on("contextmenu:channel", this.openChannelContextMenu); }, destroyed() { eventbus.off("escapekey", this.close); eventbus.off("contextmenu:user", this.openUserContextMenu); eventbus.off("contextmenu:channel", this.openChannelContextMenu); this.close(); }, methods: { openChannelContextMenu(data) { const items = generateChannelContextMenu(this.$root, data.channel, data.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} ); 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>