Today I created a context menu using Vue. Having a context menu appear when right clicking on an element on the page allowed me to simplify the UI of a project I was working on.

Below is an image of the UI before the redesign. On the left side is the content navigator which contains a list of documents and lists. It's hard to tell from the image, but you can drag and drop the items in each list to rearrange them. You can also click the + icon to open up a modal that lets you pin more items to the list.

The old content navigator

In this version you could remove documents and lists from the content navigator by clicking the x.

I wanted to simplify the design the design and get rid of the x.

After thinking about how I delete documents in my code editor, I thought of implementing a context menu in a similar fashion. After some trial and error I was able to put together something usable. In the process I also refactored my code into multiple child components and combined documents and lists into one list. Now I can fit more items in the same space.

The refactored content navigator with context menus.

Although the context menu currently has only two items, I can add more if I need to without impacting the design.

I removed the ability to pin new content to the content navigator (that's what the plus icon next to "Documents" and "Lists" was for), however I plan on adding a global search bar that allows you to search for content and pin it when you're on a page with a content editor.

This is what code for the <ContextMenu/> component looks like (written for Vue2):

<!-- ContextMenu.vue --><template>    <ul class="menu bg-white block border border-gray-300 absolute w-64"         ref="menu" tabindex="-1"         @blur.self="$emit('close')"         @contextmenu.prevent         :style="{top:top, left:left}">        <li v-for="(item, itemIndex) in items"             :key="itemIndex"             class="text-xs border-b last:border-b-0 border-gray-200 p-2 hover:bg-blue-400 hover:text-white"             @click="$emit('action', item.action)">            {{ item.text }}        </li>    </ul></template><script>export default {        props: ['position', 'items'],    data() {        return {            top: "0px",            left: "0px",        }    },    mounted() {        this.$refs.menu.focus();        this.set(this.position.y, this.position.x)    },      methods: {        set: function(top, left) {            // Credit to Simply Software            // https://vuejsexamples.com/vue-js-right-click-menu/            let largestHeight = window.innerHeight - this.$refs.menu.offsetHeight - 25;            let largestWidth = window.innerWidth - this.$refs.menu.offsetWidth - 25;            if (top > largestHeight) top = largestHeight;            if (left > largestWidth) left = largestWidth;            this.top = top + 'px';            this.left = left + 'px';        }    }    }</script><style scoped>.menu:focus {    outline: none;}.menu{    z-index: 999999;}</style>

And here is what it looks like when I add the <ContextMenu/> component inside the <ContentListItem/> component:

<ContextMenu     v-if="showMenu"     :position="menuPosition"     :items="menuItems"     @close="showMenu = false"     @action="$emit('action', $event)"></ContextMenu>

There are two props:

  • position - An object containing x and y coordinates
  • items - A list of menu items.

Each menu item contains two fields, text and action.

{  text: 'Unpin from workspace',  action: 'unpinContent'}

The parent components handle when to show the context menu and the events that mutate the data.