Custom context menu

This commit is contained in:
Jiiks 2018-08-22 00:23:08 +03:00
parent 3c38ecdd97
commit 4c5300be12
13 changed files with 384 additions and 3 deletions

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications } from 'ui';
import { DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu } from 'ui';
import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
@ -28,7 +28,7 @@ class BetterDiscord {
Logger.log('main', 'BetterDiscord starting');
this._bd = {
DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications,
DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu,
Events, CssEditor, Globals, Settings, Database, Updater,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager,

View File

@ -0,0 +1,184 @@
.bd-cm {
background: #18191c;
box-shadow: 0 0 1px rgba(0,0,0,.82), 0 1px 4px rgba(0,0,0,.1);
border-radius: 5px;
position: fixed;
width: 170px;
z-index: 1005;
user-select: none;
&.bd-cmRenderLeft {
.bd-cm {
margin-left: -170px;
}
}
.bd-cm {
left: 170px;
max-height: 270px;
overflow-y: auto;
contain: layout;
flex: 1;
min-height: 1px;
margin-left: 170px;
&::-webkit-scrollbar {
height: 8px;
width: 8px;
}
&::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: rgba(32,34,37,.6);
border: 2px solid transparent;
border-radius: 4px;
cursor: move;
}
&::-webkit-scrollbar-track {
background-clip: padding-box;
border-radius: 7px;
border: 2px solid transparent;
}
}
.bd-cmGroup:not(:first-child):not(:empty) {
&:not(:first-child) {
&:not(:empty) {
border-top: 1px solid hsla(0,0%, 96.1%, .08);
}
}
}
.bd-cmSub {
.bd-materialDesignIcon {
position: absolute;
right: 0;
bottom: 2px;
fill: hsla(0, 0%, 100%, .6);
svg {
height: 20px;
transform: rotate(-90deg);
}
}
&:hover {
svg {
fill: #fff;
}
}
}
.bd-cmItem {
cursor: default;
color: hsla(0,0%,100%,.6);
border-radius: 5px;
box-sizing: border-box;
font-size: 13px;
font-weight: 500;
line-height: 16px;
margin: 2px 0;
overflow: hidden;
padding: 6px 9px;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
display: flex;
.bd-cmHint {
opacity: .8;
color: hsla(0,0%,100%,.6);
}
span {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
}
img {
height: 16px;
}
&:hover {
background: #040405;
color: #fff;
}
}
.bd-cmToggle {
align-items: center;
display: flex;
justify-content: space-between;
padding: 5px 9px;
.bd-cmLabel {
overflow: hidden;
padding-right: 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
.bd-cmCheckbox {
cursor: pointer;
margin-left: 3px;
pointer-events: none;
align-items: center;
cursor: pointer;
display: flex;
.bd-cmCheckboxInner {
flex-shrink: 0;
height: 18px;
position: relative;
vertical-align: top;
width: 18px;
&:before,
&:after {
content: '';
}
input {
display: none;
&:checked {
+ span {
background-color: #7289da;
border-color: #7289da;
&:after {
border-color: #fff;
border-style: solid;
border-width: 0 2px 2px 0;
content: "";
display: table;
height: 10px;
left: 4px;
position: absolute;
top: 0;
transform: rotate(45deg);
width: 4px;
}
}
}
}
span {
border: 2px solid hsla(0,0%,100%,.2);
border-radius: 2px;
bottom: 0;
box-sizing: border-box;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: .24s;
}
}
}
}
}

View File

@ -13,3 +13,4 @@
@import './toasts';
@import './badges';
@import './notifications';
@import './contextmenu';

View File

@ -29,3 +29,7 @@
.bd-inline {
display: inline;
}
.bd-hidden {
display: none;
}

View File

@ -12,7 +12,7 @@ import { Events, DiscordApi, Settings } from 'modules';
import { remote } from 'electron';
import DOM from './dom';
import Vue from './vue';
import { BdSettingsWrapper, BdModals, BdToasts, BdNotifications } from './components';
import { BdSettingsWrapper, BdModals, BdToasts, BdNotifications, BdContextMenu } from './components';
export default class {
@ -53,6 +53,7 @@ export default class {
DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals);
DOM.createElement('div', null, 'bd-toasts').appendTo(DOM.bdToasts);
DOM.createElement('div', null, 'bd-notifications').appendTo(DOM.bdNotifications);
DOM.createElement('div', null, 'bd-contextmenu').appendTo(DOM.bdContextMenu);
DOM.createElement('bd-tooltips').appendTo(DOM.bdBody);
this.toasts = new (Vue.extend(BdToasts))({
@ -71,6 +72,10 @@ export default class {
el: '#bd-notifications'
});
this.contextmenu = new (Vue.extend(BdContextMenu))({
el: '#bd-contextmenu'
});
return this.vueInstance;
}

View File

@ -0,0 +1,50 @@
/**
* BetterDiscord Context Menu Component
* 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.
*/
<template>
<div class="bd-cm" :class="{'bd-cmRenderLeft': renderLeft}" v-if="activeMenu && activeMenu.menu" :style="calculatePosition()">
<CMGroup v-for="(group, index) in activeMenu.menu.groups" :items="group.items" :key="index" :closeMenu="() => {activeMenu.menu = null}" :left="left" :top="top"/>
</div>
</template>
<script>
// Imports
import { BdContextMenu } from 'ui';
import CMGroup from './contextmenu/Group.vue';
export default {
data() {
return {
activeMenu: BdContextMenu.activeMenu,
visibleSub: -1,
left: -1,
top: -1,
renderLeft: false
};
},
components: { CMGroup },
methods: {
calculatePosition() {
if (!this.activeMenu.menu.groups.length) return {};
const height = this.activeMenu.menu.groups.reduce((total, group) => total + group.items.length, 0) * 28;
this.top = window.innerHeight - this.mouseY - height < 0 ? this.mouseY - height : this.mouseY;
this.left = window.innerWidth - this.mouseX - 170 < 0 ? this.mouseX - 170 : this.mouseX;
this.renderLeft = (this.left + 170 * 2) > window.innerWidth;
return { top: `${this.top}px`, left: `${this.left}px` };
}
},
mounted() {
window.addEventListener('contextmenu', e => {
this.mouseX = e.clientX;
this.mouseY = e.clientY;
});
}
}
</script>

View File

@ -0,0 +1,23 @@
/**
* BetterDiscord Context Menu Button Component
* 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.
*/
<template>
<div class="bd-cmItem" :style="{color: item.color || ''}" @click="onClick">
<span>{{item.text}}</span>
<div class="bd-cmHint" v-if="item.hint">{{item.hint}}</div>
<img :src="item.icon" v-else-if="item.icon"/>
</div>
</template>
<script>
export default {
props: ['item', 'onClick']
}
</script>

View File

@ -0,0 +1,59 @@
/**
* BetterDiscord Context Menu Group Component
* 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.
*/
<template>
<div class="bd-cmGroup" ref="test">
<template v-for="(item, index) in items">
<CMButton v-if="!item.type || item.type === 'button'" :item="item" :onClick="() => { item.onClick(); closeMenu(); }" />
<CMToggle v-else-if="item.type === 'toggle'" :item="item" :onClick="() => { item.checked = item.onChange(!item.checked) }" />
<div v-else-if="item.type === 'sub'" class="bd-cmItem bd-cmSub" @mouseenter="e => subMenuMouseEnter(e, index, item)" @mouseleave="e => subMenuMouseLeave(e, index, item)">
{{item.text}}
<MiChevronDown />
<div ref="test2" class="bd-cm" v-if="index === visibleSub" :style="subStyle">
<template v-for="(item, index) in item.items">
<CMButton v-if="!item.type || item.type === 'button'" :item="item" :onClick="() => { item.onClick(); closeMenu(); }" />
<CMToggle v-else-if="item.type === 'toggle'" :item="item" :onClick="() => { item.checked = item.onChange(!item.checked) }" />
</template>
</div>
</div>
</template>
</div>
</template>
<script>
// Imports
import CMButton from './Button.vue';
import CMToggle from './Toggle.vue';
import { MiChevronDown } from '../common';
export default {
data() {
return {
visibleSub: -1,
subStyle: {}
}
},
props: ['items', 'closeMenu', 'left', 'top'],
components: { CMButton, CMToggle, MiChevronDown },
methods: {
subMenuMouseEnter(e, index, sub) {
const subHeight = sub.items.length > 9 ? 270 : sub.items.length * e.target.offsetHeight;
const top = this.top + subHeight + e.target.offsetTop > window.innerHeight ?
this.top - subHeight + e.target.offsetTop + e.target.offsetHeight :
this.top + e.target.offsetTop;
this.subStyle = { top: `${top}px`, left: `${this.left}px` };
this.visibleSub = index;
},
subMenuMouseLeave(e, index, sub) {
this.visibleSub = -1;
}
}
}
</script>

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Context Menu Toggle Component
* 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.
*/
<template>
<div class="bd-cmItem bd-cmToggle" @click="onClick">
<div class="bd-cmLabel">{{item.text}}</div>
<div class="bd-cmCheckbox">
<div class="bd-cmCheckboxInner">
<input type="checkbox" :checked="item.checked || item.enabled"/>
<span></span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['item', 'onClick']
}
</script>

View File

@ -3,3 +3,4 @@ export { default as BdSettings } from './BdSettings.vue';
export { default as BdModals } from './BdModals.vue';
export { default as BdToasts } from './BdToasts.vue';
export { default as BdNotifications } from './BdNotifications.vue';
export { default as BdContextMenu } from './BdContextMenu.vue';

View File

@ -0,0 +1,25 @@
/*
* 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.
*/
export class BdContextMenu {
/**
* Show a context menu
* @param {Object[]} grops Groups of items to show in context menu
*/
static show(groups) {
this.activeMenu.menu = { groups };
}
static get activeMenu() {
return this._activeMenu || (this._activeMenu = { menu: null });
}
}

View File

@ -186,6 +186,7 @@ export default class DOM {
static get bdModals() { return this.getElement('bd-modals') || this.createElement('bd-modals').appendTo(this.bdBody) }
static get bdToasts() { return this.getElement('bd-toasts') || this.createElement('bd-toasts').appendTo(this.bdBody) }
static get bdNotifications() { return this.getElement('bd-notifications') || this.createElement('bd-notifications').appendTo(this.bdBody) }
static get bdContextMenu() { return this.getElement('bd-contextmenu') || this.createElement('bd-contextmenu').appendTo(this.bdBody) }
static getElement(e) {
if (e instanceof BdNode) return e.element;

View File

@ -4,6 +4,7 @@ export { default as BdMenu, BdMenuItems } from './bdmenu';
export { default as Modals } from './modals';
export { default as Toasts } from './toasts';
export { default as Notifications } from './notifications';
export * from './contextmenus';
export { default as VueInjector } from './vueinjector';
export { default as Reflection } from './reflection';