commit
a1e32f8b89
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* BetterDiscord Emote Autocomplete Module
|
||||||
|
* 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 { Settings } from 'modules';
|
||||||
|
|
||||||
|
import BuiltinModule from './BuiltinModule';
|
||||||
|
import EmoteModule from './EmoteModule';
|
||||||
|
import GlobalAc from '../ui/autocomplete';
|
||||||
|
|
||||||
|
const EMOTE_SOURCES = [
|
||||||
|
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
|
||||||
|
'https://cdn.frankerfacez.com/emoticon/:id/1',
|
||||||
|
'https://cdn.betterttv.net/emote/:id/1x'
|
||||||
|
]
|
||||||
|
|
||||||
|
export default new class EmoteAc extends BuiltinModule {
|
||||||
|
|
||||||
|
get settingPath() { return ['emotes', 'default', 'emoteac'] }
|
||||||
|
|
||||||
|
async enabled(e) {
|
||||||
|
GlobalAc.add(';', this);
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled(e) {
|
||||||
|
GlobalAc.remove(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for autocomplete
|
||||||
|
* @param {any} regex
|
||||||
|
*/
|
||||||
|
acsearch(regex) {
|
||||||
|
const acType = Settings.getSetting('emotes', 'default', 'emoteactype').value;
|
||||||
|
if (regex.length <= 0) {
|
||||||
|
return {
|
||||||
|
type: 'imagetext',
|
||||||
|
title: [`Your ${acType ? 'most used' : 'favourite'} emotes`, '', `⬅ ${acType ? 'Favourites' : 'Most Used'} ⮕`],
|
||||||
|
items: EmoteModule[acType ? 'mostUsed' : 'favourites'].sort((a, b) => b.useCount - a.useCount).slice(0, 10).map(mu => {
|
||||||
|
return {
|
||||||
|
key: acType ? mu.key : mu.name,
|
||||||
|
value: {
|
||||||
|
src: EMOTE_SOURCES[mu.type].replace(':id', mu.id),
|
||||||
|
replaceWith: `;${acType ? mu.key : mu.name};`,
|
||||||
|
hint: mu.useCount ? `Used ${mu.useCount} times` : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = EmoteModule.search(regex);
|
||||||
|
return {
|
||||||
|
type: 'imagetext',
|
||||||
|
title: ['Matching', regex.length],
|
||||||
|
items: results.map(result => {
|
||||||
|
result.value.src = EMOTE_SOURCES[result.value.type].replace(':id', result.value.id);
|
||||||
|
result.value.replaceWith = `;${result.key};`;
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(sterm) {
|
||||||
|
if (sterm.length > 1) return false;
|
||||||
|
Settings.getSetting('emotes', 'default', 'emoteactype').value = !Settings.getSetting('emotes', 'default', 'emoteactype').value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<img class="emoji" :class="{jumboable}" :src="src" :alt="`;${name};`" v-tooltip="{ content: `;${name};`, delay: { show: 750, hide: 0 } }" />
|
<img class="bd-emote emoji" :class="{jumboable}" :src="src" :alt="`;${name};`" v-tooltip="{ content: `;${name};`, delay: { show: 750, hide: 0 } }" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { request } from 'vendor';
|
||||||
|
|
||||||
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
||||||
import { DiscordApi, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch, Cache, Patcher, Database } from 'modules';
|
import { DiscordApi, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch, Cache, Patcher, Database } from 'modules';
|
||||||
import { VueInjector } from 'ui';
|
import { VueInjector, DiscordContextMenu } from 'ui';
|
||||||
|
|
||||||
import Emote from './EmoteComponent.js';
|
import Emote from './EmoteComponent.js';
|
||||||
import Autocomplete from '../ui/components/common/Autocomplete.vue';
|
import Autocomplete from '../ui/components/common/Autocomplete.vue';
|
||||||
|
@ -52,8 +52,27 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
get settingPath() { return ['emotes', 'default', 'enable'] }
|
get settingPath() { return ['emotes', 'default', 'enable'] }
|
||||||
|
|
||||||
async enabled() {
|
async enabled() {
|
||||||
// Add ; prefix for autocomplete
|
|
||||||
GlobalAc.add(';', this);
|
// Add favourite button to context menu
|
||||||
|
this.removeFavCm = DiscordContextMenu.add('BD:EmoteModule:FavCM', target => [
|
||||||
|
{
|
||||||
|
text: 'Favourite',
|
||||||
|
type: 'toggle',
|
||||||
|
checked: target && target.alt ? this.favourites.find(e => e.name === target.alt.replace(/;/g, '')) : false,
|
||||||
|
onChange: (checked, target) => {
|
||||||
|
const { alt } = target;
|
||||||
|
if (!alt) return false;
|
||||||
|
|
||||||
|
const name = alt.replace(/;/g, '');
|
||||||
|
|
||||||
|
if (!checked) return this.removeFavourite(name);
|
||||||
|
|
||||||
|
const emote = this.findByName(name, true);
|
||||||
|
if (!emote) return false;
|
||||||
|
return this.addFavourite(emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
], filter => filter.closest('.bd-emote'));
|
||||||
|
|
||||||
if (!this.database.size) {
|
if (!this.database.size) {
|
||||||
await this.loadLocalDb();
|
await this.loadLocalDb();
|
||||||
|
@ -68,13 +87,28 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
|
|
||||||
this.patchMessageContent();
|
this.patchMessageContent();
|
||||||
this.patchSendAndEdit();
|
this.patchSendAndEdit();
|
||||||
|
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: WebpackModules.getSelector('imageWrapper') });
|
||||||
|
MonkeyPatch('BD:EMOTEMODULE', ImageWrapper.component.prototype).after('render', this.beforeRenderImageWrapper.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
addFavourite(emote) {
|
||||||
|
if (this.favourites.find(e => e.name === emote.name)) return true;
|
||||||
|
this.favourites.push(emote);
|
||||||
|
Database.insertOrUpdate({ 'id': 'EmoteModule' }, { 'id': 'EmoteModule', favourites: this.favourites, mostused: this.mostUsed })
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFavourite(name) {
|
||||||
|
if (!this.favourites.find(e => e.name === name)) return false;
|
||||||
|
this._favourites = this._favourites.filter(e => e.name !== name);
|
||||||
|
Database.insertOrUpdate({ 'id': 'EmoteModule' }, { 'id': 'EmoteModule', favourites: this.favourites, mostused: this.mostUsed })
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async disabled() {
|
async disabled() {
|
||||||
// Unpatch all patches
|
// Unpatch all patches
|
||||||
for (const patch of Patcher.getPatchesByCaller('BD:EMOTEMODULE')) patch.unpatch();
|
for (const patch of Patcher.getPatchesByCaller('BD:EMOTEMODULE')) patch.unpatch();
|
||||||
// Remove ; prefix from autocomplete
|
if (this.removeFavCm) this.removeFavCm();
|
||||||
GlobalAc.remove(';');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,7 +142,6 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
filter.className &&
|
filter.className &&
|
||||||
filter.className.includes('markup') &&
|
filter.className.includes('markup') &&
|
||||||
filter.children.length >= 2);
|
filter.children.length >= 2);
|
||||||
|
|
||||||
if (!markup) return;
|
if (!markup) return;
|
||||||
markup.children[1] = this.processMarkup(markup.children[1]);
|
markup.children[1] = this.processMarkup(markup.children[1]);
|
||||||
}
|
}
|
||||||
|
@ -160,7 +193,7 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
const arr = new Uint8Array(new ArrayBuffer(res.length));
|
const arr = new Uint8Array(new ArrayBuffer(res.length));
|
||||||
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
|
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
|
||||||
const suffix = arr[0] === 71 && arr[1] === 73 && arr[2] === 70 ? '.gif' : '.png';
|
const suffix = arr[0] === 71 && arr[1] === 73 && arr[2] === 70 ? '.gif' : '.png';
|
||||||
Uploader.upload(args[0], FileActions.makeFile(arr, `${emote.name}${suffix}`));
|
Uploader.upload(args[0], FileActions.makeFile(arr, `${emote.name}.bdemote${suffix}`));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +211,20 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
return orig(...args);
|
return orig(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle imagewrapper render
|
||||||
|
*/
|
||||||
|
beforeRenderImageWrapper(component, args, retVal) {
|
||||||
|
if (!component.props || !component.props.src) return;
|
||||||
|
|
||||||
|
const src = component.props.original || component.props.src.split('?')[0];
|
||||||
|
if (!src || !src.includes('.bdemote.')) return;
|
||||||
|
const emoteName = src.split('/').pop().split('.')[0];
|
||||||
|
const emote = this.findByName(emoteName);
|
||||||
|
if (!emote) return;
|
||||||
|
retVal.props.children = emote.render();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add/update emote to most used
|
* Add/update emote to most used
|
||||||
* @param {Object} emote emote to add/update
|
* @param {Object} emote emote to add/update
|
||||||
|
@ -205,6 +252,7 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
processMarkup(markup) {
|
processMarkup(markup) {
|
||||||
const newMarkup = [];
|
const newMarkup = [];
|
||||||
if (!(markup instanceof Array)) return markup;
|
if (!(markup instanceof Array)) return markup;
|
||||||
|
|
||||||
const jumboable = !markup.some(child => {
|
const jumboable = !markup.some(child => {
|
||||||
if (typeof child !== 'string') return false;
|
if (typeof child !== 'string') return false;
|
||||||
return / \w+/g.test(child);
|
return / \w+/g.test(child);
|
||||||
|
@ -278,39 +326,6 @@ export default new class EmoteModule extends BuiltinModule {
|
||||||
return simple ? { type, id, name } : new Emote(type, id, name);
|
return simple ? { type, id, name } : new Emote(type, id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for autocomplete
|
|
||||||
* @param {any} regex
|
|
||||||
*/
|
|
||||||
acsearch(regex) {
|
|
||||||
if (regex.length <= 0) {
|
|
||||||
return {
|
|
||||||
type: 'imagetext',
|
|
||||||
title: ['Your most used emotes'],
|
|
||||||
items: this.mostUsed.sort((a,b) => b.useCount - a.useCount).slice(0, 10).map(mu => {
|
|
||||||
return {
|
|
||||||
key: `${mu.key} | ${mu.useCount}`,
|
|
||||||
value: {
|
|
||||||
src: EMOTE_SOURCES[mu.type].replace(':id', mu.id),
|
|
||||||
replaceWith: `;${mu.key};`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = this.search(regex);
|
|
||||||
return {
|
|
||||||
type: 'imagetext',
|
|
||||||
title: ['Matching', regex.length],
|
|
||||||
items: results.map(result => {
|
|
||||||
result.value.src = EMOTE_SOURCES[result.value.type].replace(':id', result.value.id);
|
|
||||||
result.value.replaceWith = `;${result.key};`;
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for anything else
|
* Search for anything else
|
||||||
* @param {any} regex
|
* @param {any} regex
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { default as TwentyFourHour } from './24Hour';
|
||||||
import { default as KillClyde } from './KillClyde';
|
import { default as KillClyde } from './KillClyde';
|
||||||
import { default as BlockedMessages } from './BlockedMessages';
|
import { default as BlockedMessages } from './BlockedMessages';
|
||||||
import { default as VoiceDisconnect } from './VoiceDisconnect';
|
import { default as VoiceDisconnect } from './VoiceDisconnect';
|
||||||
|
import { default as EmoteAc } from './EmoteAc';
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
static initAll() {
|
static initAll() {
|
||||||
|
@ -21,5 +22,6 @@ export default class {
|
||||||
KillClyde.init();
|
KillClyde.init();
|
||||||
BlockedMessages.init();
|
BlockedMessages.init();
|
||||||
VoiceDisconnect.init();
|
VoiceDisconnect.init();
|
||||||
|
EmoteAc.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -170,6 +170,20 @@
|
||||||
"text": "Image Emote",
|
"text": "Image Emote",
|
||||||
"hint": "Send single emotes as images if you have the permission",
|
"hint": "Send single emotes as images if you have the permission",
|
||||||
"value": true
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "emoteac",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Emote Autocomplete",
|
||||||
|
"hint": "Autocomplete emotes when typing with ; prefix",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "emoteactype",
|
||||||
|
"type": "bool",
|
||||||
|
"text": "Show most used instead of favourites",
|
||||||
|
"hint": "Toggle with arrow keys in autocomplete menu",
|
||||||
|
"value": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ class BetterDiscord {
|
||||||
}
|
}
|
||||||
showDummyNotif();
|
showDummyNotif();
|
||||||
|
|
||||||
DiscordContextMenu.add([
|
DiscordContextMenu.add('DummyThing', [
|
||||||
{
|
{
|
||||||
text: 'Hello',
|
text: 'Hello',
|
||||||
onClick: () => { Toasts.info('Hello!'); }
|
onClick: () => { Toasts.info('Hello!'); }
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
top: 22px;
|
top: 22px;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 3000;
|
z-index: 1001;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
transform: translateX(-100%) translateY(-100%);
|
transform: translateX(-100%) translateY(-100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -123,11 +123,11 @@
|
||||||
|
|
||||||
&.bd-stop {
|
&.bd-stop {
|
||||||
.bd-sidebarRegion {
|
.bd-sidebarRegion {
|
||||||
z-index: 3004;
|
z-index: 1003;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bd-contentRegion {
|
.bd-contentRegion {
|
||||||
z-index: 3003;
|
z-index: 1002;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ bd-tooltips {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 9001;
|
z-index: 1004;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bd-tooltip,
|
.bd-tooltip,
|
||||||
|
@ -19,7 +19,7 @@ bd-tooltips {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
z-index: 9001;
|
z-index: 1002;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: none;
|
content: none;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.bd-autocomplete {
|
.bd-ac {
|
||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -10,16 +10,16 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #2f3136;
|
background-color: #2f3136;
|
||||||
|
|
||||||
.bd-autocompleteInner {
|
.bd-acInner {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.bd-autocompleteRow {
|
.bd-acRow {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
|
||||||
.bd-autocompleteSelector {
|
.bd-acSelector {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bd-autocompleteTitle {
|
.bd-acTitle {
|
||||||
color: #72767d;
|
color: #72767d;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bd-autocompleteField {
|
.bd-acField {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
color: #f6f6f7;
|
color: #f6f6f7;
|
||||||
|
@ -73,6 +73,10 @@
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: #f6f6f7;
|
color: #f6f6f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bd-acHint {
|
||||||
|
color: #72767d;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,11 @@ export default new class AutoComplete {
|
||||||
return this.sets.hasOwnProperty(prefix);
|
return this.sets.hasOwnProperty(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggle(prefix, sterm) {
|
||||||
|
if (!this.sets[prefix].toggle) return false;
|
||||||
|
return this.sets[prefix].toggle(sterm);
|
||||||
|
}
|
||||||
|
|
||||||
items(prefix, sterm) {
|
items(prefix, sterm) {
|
||||||
if (!this.validPrefix(prefix)) return [];
|
if (!this.validPrefix(prefix)) return [];
|
||||||
return this.sets[prefix].acsearch(sterm);
|
return this.sets[prefix].acsearch(sterm);
|
||||||
|
|
|
@ -9,21 +9,23 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bd-autocomplete">
|
<div class="bd-ac">
|
||||||
<div v-if="search.items.length" class="bd-autocompleteInner">
|
<div v-if="search.items.length" class="bd-acInner">
|
||||||
<div class="bd-autocompleteRow">
|
<div class="bd-acRow">
|
||||||
<div class="bd-autocompleteSelector">
|
<div class="bd-acSelector">
|
||||||
<div class="bd-autocompleteTitle">
|
<div class="bd-acTitle">
|
||||||
{{search.title[0] || search.title}}
|
{{search.title[0] || search.title}}
|
||||||
<strong>{{search.title[1] || sterm}}</strong>
|
<strong v-if="search.title.length >= 2">{{search.title[1] || sterm}}</strong>
|
||||||
|
<strong v-if="search.title.length === 3" :style="{float: 'right'}">{{search.title[2]}}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(item, index) in search.items" class="bd-autocompleteRow" @mouseover="selectedIndex = index" @click="inject">
|
<div v-for="(item, index) in search.items" class="bd-acRow" @mouseover="selectedIndex = index" @click="inject">
|
||||||
<div class="bd-autocompleteSelector bd-selectable" :class="{'bd-selected': index === selectedIndex}">
|
<div class="bd-acSelector bd-selectable" :class="{'bd-selected': index === selectedIndex}">
|
||||||
<div class="bd-autocompleteField">
|
<div class="bd-acField">
|
||||||
<img v-if="search.type === 'imagetext'" :src="item.value.src" :alt="item.key" />
|
<img v-if="search.type === 'imagetext'" :src="item.src || item.value.src" :alt="item.key || item.text || item.alt" />
|
||||||
<div class="bd-flexGrow">{{item.key}}</div>
|
<div class="bd-flexGrow">{{item.key || item.text}}</div>
|
||||||
|
<div class="bd-acHint" v-if="item.hint || (item.value && item.value.hint)">{{item.hint || item.value.hint}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,7 +68,9 @@
|
||||||
const { which, key } = e;
|
const { which, key } = e;
|
||||||
|
|
||||||
if (key === 'ArrowDown' || key === 'ArrowUp') this.traverse(key);
|
if (key === 'ArrowDown' || key === 'ArrowUp') this.traverse(key);
|
||||||
else if (key !== 'Tab' && key !== 'Enter') return;
|
else if (key === 'ArrowLeft' || key === 'ArrowRight') {
|
||||||
|
if (!this.toggle(e)) return;
|
||||||
|
} else if (key !== 'Tab' && key !== 'Enter') return;
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -119,6 +123,12 @@
|
||||||
this.insertText(this.ta.selectionStart - this.fsterm.length, this.search.items[this.selectedIndex].value.replaceWith);
|
this.insertText(this.ta.selectionStart - this.fsterm.length, this.search.items[this.selectedIndex].value.replaceWith);
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.search = { type: null, items: [] };
|
this.search = { type: null, items: [] };
|
||||||
|
},
|
||||||
|
toggle(e) {
|
||||||
|
const { selectionEnd, value } = e.target;
|
||||||
|
const sterm = value.slice(0, selectionEnd).split(/\s+/g).pop();
|
||||||
|
const prefix = sterm.slice(0, 1);
|
||||||
|
return this.controller.toggle(prefix, sterm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@
|
||||||
<div class="bd-cmGroup" ref="test">
|
<div class="bd-cmGroup" ref="test">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<CMButton v-if="!item.type || item.type === 'button'" :item="item" :onClick="() => { item.onClick(); closeMenu(); }" />
|
<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) }" />
|
<CMToggle v-else-if="item.type === 'toggle'" :item="item" :checked="item.checked" :onClick="() => { item.checked = item.onChange(!item.checked, target) }" />
|
||||||
<div v-else-if="item.type === 'sub'" class="bd-cmItem bd-cmSub" @mouseenter="e => subMenuMouseEnter(e, index, item)" @mouseleave="e => subMenuMouseLeave(e, index, item)">
|
<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}}
|
{{item.text}}
|
||||||
<MiChevronDown />
|
<MiChevronDown />
|
||||||
<div ref="test2" class="bd-cm" v-if="index === visibleSub" :style="subStyle">
|
<div class="bd-cm" v-if="index === visibleSub" :style="subStyle">
|
||||||
<template v-for="(item, index) in item.items">
|
<template v-for="(item, index) in item.items">
|
||||||
<CMButton v-if="!item.type || item.type === 'button'" :item="item" :onClick="() => { item.onClick(); closeMenu(); }" />
|
<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) }" />
|
<CMToggle v-else-if="item.type === 'toggle'" :item="item" :checked="item.checked" :onClick="() => { item.checked = item.onChange(!item.checked, target) }" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
subStyle: {}
|
subStyle: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: ['items', 'closeMenu', 'left', 'top'],
|
props: ['items', 'closeMenu', 'left', 'top', 'target'],
|
||||||
components: { CMButton, CMToggle, MiChevronDown },
|
components: { CMButton, CMToggle, MiChevronDown },
|
||||||
methods: {
|
methods: {
|
||||||
subMenuMouseEnter(e, index, sub) {
|
subMenuMouseEnter(e, index, sub) {
|
||||||
|
@ -57,3 +57,5 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
// return typeof this.item.checked === 'function' ? this.item.checked(target) : this.item.checked;
|
||||||
|
|
|
@ -11,9 +11,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bd-cmItem bd-cmToggle" @click="onClick">
|
<div class="bd-cmItem bd-cmToggle" @click="onClick">
|
||||||
<div class="bd-cmLabel">{{item.text}}</div>
|
<div class="bd-cmLabel">{{item.text}}</div>
|
||||||
<div class="bd-cmCheckbox">
|
<div class="bd-cmCheckbox" :checked="checked">
|
||||||
<div class="bd-cmCheckboxInner">
|
<div class="bd-cmCheckboxInner">
|
||||||
<input type="checkbox" :checked="item.checked || item.enabled"/>
|
<input type="checkbox" :checked="checked"/>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,6 +22,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['item', 'onClick']
|
props: ['item', 'onClick', 'checked']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -35,12 +35,18 @@ export class DiscordContextMenu {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add items to Discord context menu
|
* add items to Discord context menu
|
||||||
|
* @param {any} id unique id for group
|
||||||
* @param {any} items items to add
|
* @param {any} items items to add
|
||||||
* @param {Function} [filter] filter function for target filtering
|
* @param {Function} [filter] filter function for target filtering
|
||||||
*/
|
*/
|
||||||
static add(items, filter) {
|
static add(id, items, filter) {
|
||||||
if (!this.patched) this.patch();
|
if (!this.patched) this.patch();
|
||||||
this.menus.push({ items, filter });
|
this.menus.push({ id, items, filter });
|
||||||
|
return () => this.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static remove(id) {
|
||||||
|
this._menus = this._menus.filter(menu => menu.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get menus() {
|
static get menus() {
|
||||||
|
@ -66,17 +72,19 @@ export class DiscordContextMenu {
|
||||||
|
|
||||||
static renderCm(component, args, retVal, res) {
|
static renderCm(component, args, retVal, res) {
|
||||||
if (!retVal.props || !res.props) return;
|
if (!retVal.props || !res.props) return;
|
||||||
const { target } = res.props;
|
const { target } = component.props;
|
||||||
const { top, left } = retVal.props.style;
|
const { top, left } = retVal.props.style;
|
||||||
if (!target || !top || !left) return;
|
if (!target || !top || !left) return;
|
||||||
if (!retVal.props.children) return;
|
if (!retVal.props.children) return;
|
||||||
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
|
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
|
||||||
|
|
||||||
for (const menu of this.menus.filter(menu => { if (!menu.filter) return true; return menu.filter(target)})) {
|
for (const menu of this.menus.filter(menu => { if (!menu.filter) return true; return menu.filter(target)})) {
|
||||||
retVal.props.children.push(VueInjector.createReactElement(CMGroup, {
|
retVal.props.children.push(VueInjector.createReactElement(CMGroup, {
|
||||||
|
target,
|
||||||
top,
|
top,
|
||||||
left,
|
left,
|
||||||
closeMenu: () => WebpackModules.getModuleByProps(['closeContextMenu']).closeContextMenu(),
|
closeMenu: () => WebpackModules.getModuleByProps(['closeContextMenu']).closeContextMenu(),
|
||||||
items: menu.items
|
items: typeof menu.items === 'function' ? menu.items(target) : menu.items
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue