thelounge/client/components/ContextMenu.vue
2019-11-25 20:13:13 +02:00

168 lines
3.6 KiB
Vue

<template>
<div
v-if="isOpen"
id="context-menu-container"
@click="containerClick"
@contextmenu.prevent="containerClick"
>
<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"
@mouseover="hoverItem(id)"
@click="clickItem(item)"
>
{{ item.label }}
</li>
</template>
</ul>
</div>
</template>
<script>
import Mousetrap from "mousetrap";
export default {
name: "ContextMenu",
props: {
message: Object,
},
data() {
return {
isOpen: false,
previousActiveElement: null,
items: [],
activeItem: -1,
style: {
left: 0,
top: 0,
},
};
},
mounted() {
Mousetrap.bind("esc", this.close);
Mousetrap.bind(["up", "down", "tab", "shift+tab"], this.navigateMenu);
},
destroyed() {
Mousetrap.unbind("esc", this.close);
Mousetrap.unbind(["up", "down", "tab", "shift+tab"], this.navigateMenu);
},
methods: {
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) {
if (item.action) {
item.action();
this.close();
} else if (item.link) {
this.$router.push(item.link);
this.close();
}
},
clickActiveItem() {
if (this.items[this.activeItem]) {
this.clickItem(this.items[this.activeItem]);
}
},
navigateMenu(event, key) {
event.preventDefault();
const direction = key === "down" || key === "tab" ? 1 : -1;
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.action && !nextItem.link) {
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.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>