BetterDiscordApp-v2/client/src/ui/contextmenus.js

106 lines
3.6 KiB
JavaScript

/*
* BetterDiscord Context Menus
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Utils, ClientLogger as Logger } from 'common';
import { Reflection, MonkeyPatch } from 'modules';
import { VueInjector, Toasts } from 'ui';
import CMGroup from './components/contextmenu/Group.vue';
export class BdContextMenu {
/**
* Show a context menu
* @param {MouseEvent|Object} e MouseEvent or Object { x: 0, y: 0 }
* @param {Object[]} groups Groups of items to show in context menu
*/
static show(e, groups) {
const x = e.x || e.clientX;
const y = e.y || e.clientY;
this.activeMenu.menu = { x, y, groups };
}
static get activeMenu() {
return this._activeMenu || (this._activeMenu = { menu: null });
}
static install(Vue) {
Vue.directive('contextmenu', {
bind(el, binding) {
el.addEventListener('contextmenu', event => {
Logger.log('BdContextMenu', ['Showing context menu', event, el, binding]);
BdContextMenu.show(event, binding.value);
});
}
});
}
}
export class DiscordContextMenu {
/**
* add items to Discord context menu
* @param {any} id unique id for group
* @param {any} items items to add
* @param {Function} [filter] filter function for target filtering
*/
static add(items, filter) {
if (!this.patched) this.patch();
const menu = { items, filter };
this.menus.push(menu);
return menu;
}
static remove(menu) {
Utils.removeFromArray(this.menus, menu);
}
static get menus() {
return this._menus || (this._menus = []);
}
static async patch() {
if (this.patched) return;
this.patched = true;
const self = this;
MonkeyPatch('BD:DiscordCMOCM', Reflection.module.byProps('openContextMenu')).instead('openContextMenu', (_, [e, fn], originalFn) => {
const overrideFn = function () {
const res = fn.apply(this, arguments);
if (!res.hasOwnProperty('type')) return res;
if (!res.type.prototype || !res.type.prototype.render || res.type.prototype.render.__patched) return res;
MonkeyPatch('BD:DiscordCMRender', res.type.prototype).after('render', (c, a, r) => self.renderCm(c, a, r, res));
res.type.prototype.render.__patched = true;
return res;
}
return originalFn(e, overrideFn);
});
}
static renderCm(component, args, retVal, res) {
if (!retVal.props || !res.props) return;
const { target } = component.props;
const { top, left } = retVal.props.style;
if (!target || !top || !left) return;
if (!retVal.props.children) return;
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
for (const menu of this.menus.filter(menu => !menu.filter || menu.filter(target))) {
retVal.props.children.push(VueInjector.createReactElement(CMGroup, {
target,
top,
left,
closeMenu: () => Reflection.module.byProps('closeContextMenu').closeContextMenu(),
items: typeof menu.items === 'function' ? menu.items(target) : menu.items
}));
}
}
}