202 lines
5.6 KiB
Vue
202 lines
5.6 KiB
Vue
<template>
|
|
<div
|
|
class="ctx-menu"
|
|
:style="style"
|
|
:hidden="!ctxMenuData"
|
|
v-click-outside="resetCtx"
|
|
>
|
|
<!-- Check if there are options data -->
|
|
<div v-if="ctxMenuData">
|
|
<!-- Use template tag to loop through the options -->
|
|
<div v-for="(item, index) in ctxMenuData">
|
|
<!--
|
|
Then check opton type default is undeifined then if the type is not divider
|
|
-->
|
|
<!-- Make sure to create a unique ref is will be needed later -->
|
|
<div
|
|
v-if="item.type !== 'divider'"
|
|
:key="index"
|
|
:ref="'ctx_' + index"
|
|
:id="'ctx_' + index"
|
|
>
|
|
<span class="ctx-menu-option">
|
|
<svg class="nav-icon-container" :class="'icon icon-' + item.icon">
|
|
<use :xlink:href="'#icon-' + item.icon"></use>
|
|
</svg>
|
|
{{ item.title }}
|
|
</span>
|
|
</div>
|
|
<div v-else :key="'else-' + index" class="ctx-menu-divider">
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
// Access the eventBus instance
|
|
|
|
export default {
|
|
data: () => ({
|
|
style: null,
|
|
ctxMenuData: null,
|
|
// the options schema
|
|
// [
|
|
// {
|
|
// title: string,
|
|
// type: string, // default is undefined
|
|
// handler: function
|
|
// }
|
|
// ]
|
|
ctxMenuRect: null,
|
|
// {
|
|
// y:number,
|
|
// x:number
|
|
// }
|
|
}),
|
|
methods: {
|
|
resetCtx() {
|
|
this.ctxMenuData = null;
|
|
this.ctxMenuRect = null;
|
|
},
|
|
onContextMenu(ev, ctxMenuData) {
|
|
// prevent default behaviours
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
this.ctxMenuData = ctxMenuData;
|
|
this.ctxMenuRect = {
|
|
x: ev.x,
|
|
y: ev.y,
|
|
};
|
|
|
|
// then reevaluate and set context-menu position
|
|
this.reevaluatePosition();
|
|
|
|
// populate the option
|
|
this.$nextTick(() => {
|
|
this.onData();
|
|
});
|
|
},
|
|
async reevaluatePosition() {
|
|
if (this.ctxMenuRect) {
|
|
// using $nextTick to daley and make sure that the context-menu
|
|
// options are fully rendered which will help us
|
|
// to get the accurate height
|
|
await this.$nextTick();
|
|
await this.$nextTick();
|
|
let { x, y } = this.ctxMenuRect;
|
|
// get the window current inner height and width
|
|
|
|
let innerHeight = window?.innerHeight;
|
|
let innerWidth = window?.innerWidth;
|
|
// get the component height and width through element.getClientRects
|
|
let height = 0,
|
|
width = 0;
|
|
|
|
let clientRect = this.$el.getClientRects();
|
|
if (clientRect && clientRect.length) {
|
|
height = clientRect[0].height;
|
|
width = clientRect[0].width;
|
|
}
|
|
// then subtract window inner height and width with
|
|
// context-menu event source points (x, y)
|
|
let dY = innerHeight - y;
|
|
let dX = innerWidth - x;
|
|
// check if the context-menu height is not
|
|
// longer than the available
|
|
if (dY < height) {
|
|
y = y - height;
|
|
}
|
|
if (dX < width) {
|
|
x = x - width;
|
|
}
|
|
// set the position
|
|
this.style = { left: x + "px", top: y + "px" };
|
|
}
|
|
},
|
|
async onData() {
|
|
const vm = this;
|
|
|
|
// validate if the ctxMenuData is an array and the lenght is not less then 1
|
|
if (Array.isArray(vm.ctxMenuData) && vm.ctxMenuData.length) {
|
|
// loop through the options
|
|
vm.ctxMenuData.forEach((item, index) => {
|
|
// if vm option type is equal's to divider and the handler property value is a function
|
|
if (item.type !== "divider" && typeof item.handler === "function") {
|
|
// select the option element with the help of the refs id
|
|
let refName = `ctx_${index}`;
|
|
let refs = vm.$refs[refName];
|
|
// accessing $refs prooerty with object square bracket notation alwasys returns arrays of
|
|
// HTML Elements of Vue components instance
|
|
// so you have to validate
|
|
if (Array.isArray(refs) && refs.length) {
|
|
let el = refs[0];
|
|
// then attach click event and pass an arrow function as a the
|
|
// event handler callback
|
|
el.addEventListener(
|
|
"click",
|
|
() => {
|
|
// then on click on the option
|
|
// envoke the handler
|
|
// and reset the the ctxMenuData to hide the context-menu
|
|
item.handler();
|
|
vm.resetCtx();
|
|
},
|
|
false
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
// Listen on contextmenu event through the $root instance
|
|
const { $eventBus } = useNuxtApp();
|
|
$eventBus.emit("contextmenu", (data) => {
|
|
// if the data is null reset and handler the action
|
|
if (data === null) this.resetCtx();
|
|
else this.onContextMenu(data.event, data.ctxMenuData);
|
|
});
|
|
},
|
|
beforeDestroy() {
|
|
// this.$root.$off("contextmenu", () => {});
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.ctx-menu {
|
|
min-width: 150px;
|
|
height: fit-content;
|
|
padding: 10px 0;
|
|
position: fixed;
|
|
background-color: #fff;
|
|
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26), 0 2px 10px 0 rgba(0, 0, 0, 0.16);
|
|
border-radius: 3px;
|
|
z-index: 1111111;
|
|
}
|
|
.ctx-menu-option {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 35px;
|
|
padding: 0 10px;
|
|
cursor: pointer;
|
|
}
|
|
.ctx-menu-option:hover {
|
|
background-color: #f3f9ff;
|
|
}
|
|
.ctx-menu-divider {
|
|
height: 16px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.ctx-menu-divider > span {
|
|
width: 50px;
|
|
height: 1px;
|
|
background-color: #dcdfe6;
|
|
}
|
|
</style>
|