Skip to content

Instantly share code, notes, and snippets.

@brandonchinn178
Last active October 25, 2017 03:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brandonchinn178/79f8d706e7b5643cb7c08044dbad37f0 to your computer and use it in GitHub Desktop.
Save brandonchinn178/79f8d706e7b5643cb7c08044dbad37f0 to your computer and use it in GitHub Desktop.
Vue Context Menu custom block

Vue Context Menu

This Gist describes how to implement context menus in Vue using custom blocks; i.e. without needing to put the context menu explicitly into the template

Motivation

In making my Vue project, I wanted to be able to create custom context menus (right-click menus) without putting it into the template. To me, a context menu is relevant to the entire component and shouldn't actually go into the template. So I wanted to use a custom block to specify a context menu for the component as a whole. However, there were no libraries available to specify context menus in a context block; they always needed to be in the template, referenced by the ref keyword. This is my attempt at implementing it.

Limitations

Right now, my code is only suited for a single context menu per component, but it should be fairly easy to refactor to allow multiple context menus per component.

Future

It'd be nice to make this into a full library that other people can npm install, but for now, it's simply a proof of concept of the idea.

import Vue from "vue";
import { directive as onClickOutside } from "vue-on-click-outside"; // npm install
import ContextMenu from "ContextMenu.vue";
import MyComponent from "MyComponent.vue";
Vue.component("context-menu", ContextMenu);
Vue.directive("on-click-outside", onClickOutside);
let App = Vue.extend(MyComponent);
new App({
el: "#app",
});
var compiler = require("vue-template-compiler");
/**
* A loader for injecting a <context-menu> section into a Vue component.
*/
function getContextMenuLoader(source) {
// Note: this function will be converted into a string and used directly in the
// generated JS file. Use ES5.
var _cmLoader = function(Component) {
var mixin = {
created: function() {
this.contextMenu = null;
},
mounted: function() {
this.contextMenu = this.$children.find(function(child) {
return child.$options._isContextMenu;
});
},
};
Component.options.mixins = [mixin].concat(Component.options.mixins || []);
var oldRender = Component.options.render;
Component.options.render = function(createElement) {
function getContents() {
// RENDER CODE
}
var contents = getContents.call(this);
return createElement("div", [
createElement("context-menu", contents.children),
oldRender.call(this, createElement),
]);
};
};
var renderCode = compiler.compile("<ul>" + source + "</ul>");
return _cmLoader.toString().replace("// RENDER CODE", renderCode.render);
}
module.exports = function(source, map) {
this.callback(null, "module.exports = " + getContextMenuLoader.call(this, source), map);
};
<template>
<ul
v-if="!cmHide"
v-on-click-outside="hide"
class="context-menu"
:style="position"
>
<slot></slot>
</ul>
</template>
<script>
export default {
_isContextMenu: true,
updated() {
if (!this._initClick) {
// Whenever <li> is clicked, hide context menu
let menuItems = _.filter(this.$slots.default, ["tag", "li"]);
_.each(menuItems, node => {
let callbacks = node.data.on.click.fns;
if (!_.isArray(callbacks)) {
callbacks = [callbacks];
node.data.on.click.fns = callbacks;
}
callbacks.push($event => this.hide());
});
this._initClick = true;
}
},
data() {
return {
_initClick: false,
cmHide: true,
position: {
left: null,
top: null,
},
};
},
methods: {
open(e) {
this.cmHide = false;
// left/top relative to parent
let offset = $(this.$parent.$el).offset();
this.position.left = e.pageX - offset.left;
this.position.top = e.pageY - offset.top;
// could do other smart stuff like position to the left if
// menu would go off screen
},
hide() {
this.cmHide = true;
},
},
};
</script>
<style lang="scss" scoped>
.context-menu {
position: absolute;
list-style: none;
z-index: 100;
li {
cursor: pointer;
}
}
</style>
<template>
<p @contextmenu.prevent="this.contextMenu.open($event)">Right click here</p>
</template>
<context-menu>
<li v-if="foo" @click="doFoo">Foo</li>
<li @click="doBar">Bar</li>
</context-menu>
<script>
export default {
data: {
foo: true,
},
methods: {
doFoo: function() {
console.log("foo");
},
doBar: function() {
console.log("bar");
},
},
}
</script>
var path = require("path");
module.exports = {
...
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: "vue-loader",
options: {
loaders: {
"context-menu": path.resolve("context-menu-loader.js"),
},
},
},
}
],
}
};
@brandonchinn178
Copy link
Author

brandonchinn178 commented Oct 25, 2017

Now a node module! Check out my repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment