Merge pull request #251 from JsSucks/example-plugin

Render example
This commit is contained in:
Alexei Stukov 2018-08-26 17:56:22 +03:00 committed by GitHub
commit 30716b57e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 256 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import { EmoteModule } from 'builtin';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
import * as CommonComponents from 'commoncomponents';
import { default as Components } from '../ui/components/generic';
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
@ -24,6 +25,9 @@ import DiscordApi from './discordapi';
import { ReactComponents, ReactHelpers } from './reactcomponents';
import { Patcher, MonkeyPatch } from './patcher';
import GlobalAc from '../ui/autocomplete';
import Vue from 'vue';
import path from 'path';
import Globals from './globals';
export default class PluginApi {
@ -61,6 +65,7 @@ export default class PluginApi {
get EventsWrapper() { return EventsWrapper }
get CommonComponents() { return CommonComponents }
get Components() { return Components }
get Filters() { return Filters }
get Discord() { return DiscordApi }
get DiscordApi() { return DiscordApi }
@ -105,7 +110,9 @@ export default class PluginApi {
removeFromArray: (...args) => Utils.removeFromArray.apply(Utils, args),
defineSoftGetter: (...args) => Utils.defineSoftGetter.apply(Utils, args),
wait: (...args) => Utils.wait.apply(Utils, args),
until: (...args) => Utils.until.apply(Utils, args)
until: (...args) => Utils.until.apply(Utils, args),
findInTree: (...args) => Utils.findInTree.apply(Utils, args),
findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args)
};
}
@ -605,6 +612,10 @@ export default class PluginApi {
});
}
Vuewrap(id, component, props) {
return VueInjector.createReactElement(Vue.component(id, component), props);
}
}
// Stop plugins from modifying the plugin API for all plugins

View File

@ -130,6 +130,12 @@ export default class extends ContentManager {
static unloadContentHook(content, reload) {
delete Globals.require.cache[Globals.require.resolve(content.paths.mainPath)];
const uncache = [];
for (const required in Globals.require.cache) {
if (!required.includes(content.paths.contentPath)) continue;
uncache.push(Globals.require.resolve(required));
}
for (const u of uncache) delete Globals.require.cache[u];
}
/**

View File

@ -0,0 +1,21 @@
/**
* BetterDiscord Generic 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-button" :class="classes" @click="onClick">
{{text}}
</div>
</template>
<script>
export default {
props: ['classes', 'text', 'onClick']
}
</script>

View File

@ -0,0 +1,23 @@
/**
* BetterDiscord Generic Button 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-buttonGroup" :class="classes">
<Button v-for="(button, index) in buttons" :text="button.text" :classes="button.classes" :onClick="button.onClick" :key="index"/>
</div>
</template>
<script>
import Button from './Button.vue';
export default {
props: ['buttons', 'classes'],
components: { Button }
}
</script>

View File

@ -0,0 +1,29 @@
import VrWrapper from '../../vrwrapper';
import ButtonGroupComponent from './ButtonGroup.vue';
class ButtonGroupWrapper extends VrWrapper {
get component() { return ButtonGroupComponent }
constructor(props) {
super();
this.props = props;
}
}
import ButtonComponent from './Button.vue';
class ButtonWrapper extends VrWrapper {
get component() { return ButtonComponent }
constructor(props) {
super();
this.props = props;
}
}
export default class {
static Button(props) {
return new ButtonWrapper(props);
}
static ButtonGroup(props) {
return new ButtonGroupWrapper(props);
}
}

View File

@ -0,0 +1,7 @@
module.exports = (React, props) => {
return React.createElement(
'button',
{ className: 'exampleCustomElement', onClick: props.onClick },
'r'
);
}

View File

@ -0,0 +1,13 @@
module.exports = (VueWrap, props) => {
return VueWrap('somecomponent', {
render: function (createElement) {
return createElement('button', {
class: 'exampleCustomElement',
on: {
click: this.onClick
}
}, 'v');
},
props: ['onClick']
}, props);
}

View File

@ -0,0 +1,17 @@
{
"info": {
"id": "render-example",
"name": "Render Example",
"authors": [
{
"name": "Jiiks",
"discord_id": "81388395867156480",
"github_username": "Jiiks",
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"description": "Example for rendering stuff"
},
"main": "index.js"
}

View File

@ -0,0 +1,128 @@
/**
* This is an example of how you should add custom elements instead of manipulating the DOM directly
*/
// Import custom components
const customVueComponent = require('./components/vuecomponent');
const customReactComponent = require('./components/reactcomponent');
module.exports = (Plugin, Api, Vendor) => {
// Destructure some apis
const { Logger, ReactComponents, Patcher, monkeyPatch, Reflection, Utils, CssUtils, VueInjector, Vuewrap, requireUncached } = Api;
const { Vue } = Vendor;
const { React } = Reflection.modules; // This should be in vendor
return class extends Plugin {
async onStart() {
this.injectStyle();
this.patchGuildTextChannel();
this.patchMessages();
return true;
}
async onStop() {
// The automatic unpatcher is not there yet
Patcher.unpatchAll();
CssUtils.deleteAllStyles();
// Force update elements to remove our changes
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
GuildTextChannel.forceUpdateAll();
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
MessageContent.forceUpdateAll();
return true;
}
/* Inject some style for our custom element */
async injectStyle() {
const css = `
.exampleCustomElement {
background: #7a7d82;
color: #FFF;
border-radius: 5px;
font-size: 12px;
font-weight: 600;
opacity: .5;
&:hover {
opacity: 1;
}
}
.exampleBtnGroup {
.bd-button {
font-size: 14px;
padding: 5px;
}
}
`;
await CssUtils.injectSass(css);
}
async patchGuildTextChannel() {
// Get the GuildTextChannel component and patch it's render function
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
monkeyPatch(GuildTextChannel.component.prototype).after('render', this.injectCustomElements.bind(this));
// Force update to see our changes immediatly
GuildTextChannel.forceUpdateAll();
}
async patchMessages() {
// Get Message component and patch it's render function
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
monkeyPatch(MessageContent.component.prototype).after('render', this.injectGenericComponents.bind(this));
// Force update to see our changes immediatly
MessageContent.forceUpdateAll();
}
/*
* Injecting a custom React element using React.createElement
* https://reactjs.org/docs/react-api.html#createelement
* Injecting a custom Vue element using Vue.component
* https://vuejs.org/v2/guide/render-function.html
**/
injectCustomElements(that, args, returnValue) {
// Get the child we want using a treewalker since we know the child we want has a channel property and children.
const child = Utils.findInReactTree(returnValue, filter => filter.hasOwnProperty('channel') && filter.children);
if (!child) return;
// If children is not an array make it into one
if (!child.children instanceof Array) child.children = [child.children];
// Add our custom components to children
child.children.push(customReactComponent(React, { onClick: e => this.handleClick(e, child.channel) }));
child.children.push(customVueComponent(Vuewrap, { onClick: e => this.handleClick(e, child.channel) }));
}
/**
* Inject generic components provided by BD
*/
injectGenericComponents(that, args, returnValue) {
// If children is not an array make it into one
if (!returnValue.props.children instanceof Array) returnValue.props.children = [returnValue.props.children];
// Add a generic Button component provided by BD
returnValue.props.children.push(Api.Components.ButtonGroup({
classes: [ 'exampleBtnGroup' ], // Additional classes for button group
buttons: [
{
classes: ['exampleBtn'], // Additional classes for button
text: 'Hello World!', // Text for button
onClick: e => Logger.log('Hello World!') // Button click handler
},
{
classes: ['exampleBtn'],
text: 'Button',
onClick: e => Logger.log('Button!')
}
]
}).render()); // Render will return the wrapped component that can then be displayed
}
/**
* Will log the channel object
*/
handleClick(e, channel) {
Logger.log('Clicked!', channel);
}
}
};