This commit is contained in:
Zack 2019-04-10 15:38:08 +00:00 committed by GitHub
commit 8725a3a648
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1062 additions and 29 deletions

View File

@ -8,11 +8,53 @@
* LICENSE file in the root directory of this source tree.
*/
/* These are Discord's permissions as of March 13th 2019
{
"CREATE_INSTANT_INVITE": 1,
"KICK_MEMBERS": 2,
"BAN_MEMBERS": 4,
"ADMINISTRATOR": 8,
"MANAGE_CHANNELS": 16,
"MANAGE_GUILD": 32,
"CHANGE_NICKNAME": 67108864,
"MANAGE_NICKNAMES": 134217728,
"MANAGE_ROLES": 268435456,
"MANAGE_WEBHOOKS": 536870912,
"MANAGE_EMOJIS": 1073741824,
"VIEW_AUDIT_LOG": 128,
"VIEW_CHANNEL": 1024,
"SEND_MESSAGES": 2048,
"SEND_TSS_MESSAGES": 4096,
"MANAGE_MESSAGES": 8192,
"EMBED_LINKS": 16384,
"ATTACH_FILES": 32768,
"READ_MESSAGE_HISTORY": 65536,
"MENTION_EVERYONE": 131072,
"USE_EXTERNAL_EMOJIS": 262144,
"ADD_REACTIONS": 64,
"CONNECT": 1048576,
"SPEAK": 2097152,
"MUTE_MEMBERS": 4194304,
"DEAFEN_MEMBERS": 8388608,
"MOVE_MEMBERS": 16777216,
"USE_VAD": 33554432,
"PRIORITY_SPEAKER": 256
}
*/
const PermissionMap = {
IDENTIFY: {
HEADER: 'Access your account information',
BODY: 'Allows :NAME: to read your account information (excluding user token).'
},
NAVIGATION: {
HEADER: 'Navigate within Discord',
BODY: 'Allows :NAME: to navigate Discord and open things like user settings.'
},
MANAGE_SETTINGS: {
HEADER: 'Change user settings',
BODY: 'Allows :NAME: to change Discord setting like light/dark theme and cozy/compact mode.'
},
READ_MESSAGES: {
HEADER: 'Read all messages',
BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
@ -29,9 +71,37 @@ const PermissionMap = {
HEADER: 'Edit messages',
BODY: 'Allows :NAME: to edit messages on your behalf.'
},
JOIN_SERVERS: {
HEADER: 'Join servers for you',
BODY: 'Allows :NAME: to join servers on your behalf.'
ATTACH_FILES: {
HEADER: 'Attach files',
BODY: 'Allows :NAME: to upload files on your behalf.'
},
ADD_REACTIONS: {
HEADER: 'Add reactions',
BODY: 'Allows :NAME: to add reactions on your behalf.'
},
VIEW_SERVERS: {
HEADER: 'View servers',
BODY: 'Allows :NAME: to see what servers you are a member of.'
},
MANAGE_SERVERS: {
HEADER: 'Join and leave servers',
BODY: 'Allows :NAME: to join and leave servers on your behalf.'
},
VIEW_CHANNELS: {
HEADER: 'View channels',
BODY: 'Allows :NAME: to see what channels you have available.'
},
FS_ACCESS: {
HEADER: 'Access your filesystem',
BODY: 'Allows :NAME: to read and write files to your computer.'
},
VIEW_RELATIONSHIPS: {
HEADER: 'VIEW your relationships',
BODY: 'Allows :NAME: to view your friends and blocked users.'
},
MANAGE_RELATIONSHIPS: {
HEADER: 'Manage your relationships',
BODY: 'Allows :NAME: to add/remove friends, and block/unblock users on your behalf.'
}
}

View File

@ -9,8 +9,8 @@
*/
import { EmoteModule } from 'builtin';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme, Screen } from 'structs';
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Tooltip } 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';
@ -28,6 +28,7 @@ import GlobalAc from '../ui/autocomplete';
import Vue from 'vue';
import path from 'path';
import Globals from './globals';
import { remote } from 'electron';
export default class PluginApi {
@ -74,6 +75,7 @@ export default class PluginApi {
get Reflection() { return Reflection }
get DOM() { return DOM }
get VueInjector() { return VueInjector }
get Screen() { return Screen }
get observer() {
return this._observer || (this._observer = new DOMObserver());
@ -112,7 +114,12 @@ export default class PluginApi {
wait: (...args) => Utils.wait.apply(Utils, args),
until: (...args) => Utils.until.apply(Utils, args),
findInTree: (...args) => Utils.findInTree.apply(Utils, args),
findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args)
findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args),
formatString: (...args) => Utils.formatString.apply(Utils, args),
getNestedProp: (...args) => Utils.getNestedProp.apply(Utils, args),
extend: (...args) => Utils.extend.apply(Utils, args),
memoizeObject: (...args) => Utils.memoizeObject.apply(Utils, args),
stableSort: (...args) => Utils.stableSort.apply(Utils, args)
};
}
@ -355,6 +362,17 @@ export default class PluginApi {
};
}
/**
* Tooltips
*/
addTooltip(node, text, options = {}) {
return new Tooltip(node, text, options);
}
get Tooltip() {
return Tooltip;
}
/**
* Notifications
*/
@ -549,6 +567,26 @@ export default class PluginApi {
};
}
/**
* NodeModules
*/
getNodeModule(module_id) {
return {}; // Don't give open access, when plugin permissions available remove this line
// This should require extra permissions
//return require(module_id);
}
listNodeModules() {
return []; // Don't give open access, when plugin permissions available implement this properly
}
get NodeModules() {
return {
getModule: this.getNodeModule.bind(this),
listModules: this.listNodeModules.bind(this)
// path: path (Perhaps we add this stuff here as well)
};
}
/**
* Patcher
*/
@ -616,6 +654,22 @@ export default class PluginApi {
return VueInjector.createReactElement(Vue.component(id, component), props);
}
/**
* Adds a callback to a set of listeners for onSwitch.
* @param {callable} callback - basic callback to happen on channel switch
*/
static addOnSwitchListener(callback) {
remote.getCurrentWebContents().on('did-navigate-in-page', callback);
}
/**
* Removes the listener added by {@link addOnSwitchListener}.
* @param {callable} callback - callback to remove from the listener list
*/
static removeOnSwitchListener(callback) {
remote.getCurrentWebContents().removeListener('did-navigate-in-page', callback);
}
}
// Stop plugins from modifying the plugin API for all plugins

View File

@ -155,6 +155,7 @@ const KnownModules = {
/* DOM/React Components */
/* ==================== */
LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
Tooltips: m => m.hide && m.show && !m.search && !m.submit && !m.search && !m.activateRagingDemon && !m.dismiss,
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),

View File

@ -0,0 +1,13 @@
/**
* Representation of the screen such as width and height.
*
* Note: This comes from Zerebos' library.
*/
class Screen {
/** Document/window width */
static get width() { return Math.max(document.documentElement.clientWidth, window.innerWidth || 0); }
/** Document/window height */
static get height() { return Math.max(document.documentElement.clientHeight, window.innerHeight || 0); }
}
export default Screen;

View File

@ -1,4 +1,5 @@
export { default as List } from './list';
export { default as Screen } from './screen';
export * from './events/index';
export * from './settings/index';

View File

@ -11,25 +11,68 @@
import { Utils, ClientLogger as Logger } from 'common';
class BdNode {
constructor(tag, className, id) {
this.element = document.createElement(tag);
if (className) this.element.className = className;
if (id) this.element.id = id;
constructor(node, attributes) {
this.element = node;
DOM.setAttributes(node, attributes);
}
appendTo(e) {
const el = DOM.getElement(e);
if (!el) return null;
el.append(this.element);
return this.element;
}
get index() { return DOM.index(this.element); }
get innerWidth() { return DOM.innerWidth(this.element); }
get innerHeight() { return DOM.innerHeight(this.element); }
get outerWidth() { return DOM.outerWidth(this.element); }
get outerHeight() { return DOM.outerHeight(this.element); }
get offset() { return DOM.offset(this.element); }
prependTo(e) {
const el = DOM.getElement(e);
if (!el) return null;
el.prepend(this.element);
return this.element;
}
get width() { return DOM.width(this.element); }
set width(value) { return DOM.width(this.element, value); }
get height() { return DOM.height(this.element); }
set height(value) { return DOM.height(this.element, value); }
get text() { return DOM.text(this.element); }
set text(value) { return DOM.text(this.element, value); }
css(attribute, value) { return DOM.css(this.element, attribute, value); }
addClass(...classes) { return DOM.addClass(this.element, ...classes); }
removeClass(...classes) { return DOM.removeClass(this.element, ...classes); }
toggleClass(className, indicator) { return DOM.toggleClass(this.element, className, indicator); }
replaceClass(oldClass, newClass) { return DOM.replaceClass(this.element, oldClass, newClass); }
hasClass(className) { return DOM.hasClass(this.element, className); }
insertAfter(otherNode) { return DOM.insertAfter(this.element, otherNode); }
after(newNode) { return DOM.after(this.element, newNode); }
next(selector = '') { return DOM.next(this.element, selector); }
nextAll() { return DOM.nextAll(this.element); }
nextUntil(selector) { return DOM.nextUntil(this.element, selector); }
previous(selector = '') { return DOM.previous(this.element, selector); }
previousAll() { return DOM.previousAll(this.element); }
previousUntil(selector) { return DOM.previousUntil(this.element, selector); }
parent(selector = '') { return DOM.parent(this.element, selector); }
parents(selector ='') { return DOM.parents(this.element, selector); }
parentsUntil(selector) { return DOM.parentsUntil(this.element, selector); }
findChild(selector) { return DOM.findChild(this.element, selector); }
findChildren(selector) { return DOM.findChildren(this.element, selector); }
siblings(selector = '*') { return DOM.siblings(this.element, selector); }
on(event, delegate, callback) { return DOM.on(this.element, event, delegate, callback); }
once(event, delegate, callback) { return DOM.once(this.element, event, delegate, callback); }
off(event, delegate, callback) { return DOM.off(this.element, event, delegate, callback); }
find(selector) { return DOM.find(this.element, selector); }
findAll(selector) { return DOM.findAll(this.element, selector); }
appendTo(otherNode) { return DOM.appendTo(this.element, otherNode); }
prependTo(otherNode) { return DOM.prependTo(this.element, otherNode); }
onMountChange(callback, onMount = true) { return DOM.onMountChange(this.element, callback, onMount); }
onMount(callback) { return DOM.onMount(this.element, callback); }
onAdded(callback) { return DOM.onAdded(this.element, callback); }
onUnmount(callback) { return DOM.onUnmount(this.element, callback); }
onRemoved(callback) { return DOM.onRemoved(this.element, callback); }
}
export class DOMObserver {
@ -188,19 +231,43 @@ export default class DOM {
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) {
/**
* Essentially a shorthand for `document.querySelector`. If the `baseElement` is not provided
* `document` is used by default.
* @param {string} selector - Selector to query
* @param {Element} [baseElement] - Element to base the query from
* @returns {(Element|null)} - The found element or null if not found
*/
static getElement(e, baseElement = document) {
if (e instanceof BdNode) return e.element;
if (e instanceof window.Node) return e;
if ('string' !== typeof e) return null;
return document.querySelector(e);
return baseElement.querySelector(e);
}
static getElements(e) {
return document.querySelectorAll(e);
/**
* Alias for {@link module:DOM.getElement}.
*/
static query(e, baseElement = document) {return this.getElement(e, baseElement);}
/**
* Essentially a shorthand for `document.querySelectorAll`. If the `baseElement` is not provided
* `document` is used by default.
* @param {string} selector - Selector to query
* @param {Element} [baseElement] - Element to base the query from
* @returns {Array<Element>} - Array of all found elements
*/
static getElements(e, baseElement = document) {
return baseElement.querySelectorAll(e);
}
static createElement(tag = 'div', className = null, id = null) {
return new BdNode(tag, className, id);
/**
* Alias for {@link module:DOM.getElements}.
*/
static queryAll(e, baseElement = document) {return this.getElements(e, baseElement);}
static createElement(tag = 'div', attributes = {}) {
return new BdNode(document.createElement(tag), attributes);
}
static deleteStyle(id) {
@ -244,4 +311,636 @@ export default class DOM {
}
}
/**
* Builds a classname string from any number of arguments. This includes arrays and objects.
* When given an array all values from the array are added to the list.
* When given an object they keys are added as the classnames if the value is truthy.
* Copyright (c) 2018 Jed Watson https://github.com/JedWatson/classnames MIT License
* @param {...Any} argument - anything that should be used to add classnames.
*/
static className() {
const classes = [];
const hasOwnProp = {}.hasOwnProperty;
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];
if (!arg) continue;
const argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(arg);
}
else if (Array.isArray(arg) && arg.length) {
const inner = this.classNames.apply(null, arg);
if (inner) classes.push(inner);
}
else if (argType === 'object') {
for (const key in arg) {
if (hasOwnProp.call(arg, key) && arg[key]) classes.push(key);
}
}
}
return classes.join(' ');
}
/**
* Functions below come from Zerebos' library module DOMTools.
*/
/**
* Parses a string of HTML and returns the results. If the second parameter is true,
* the parsed HTML will be returned as a document fragment {@see https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment}.
* This is extremely useful if you have a list of elements at the top level, they can then be appended all at once to another node.
*
* If the second parameter is false, then the return value will be the list of parsed
* nodes and there were multiple top level nodes, otherwise the single node is returned.
* @param {string} html - HTML to be parsed
* @param {boolean} [fragment=false] - Whether or not the return should be the raw `DocumentFragment`
* @returns {(DocumentFragment|NodeList|HTMLElement)} - The result of HTML parsing
*/
static parseHTML(html, fragment = false) {
const template = document.createElement('template');
template.innerHTML = html;
const node = template.content.cloneNode(true);
if (fragment) return node;
return node.childNodes.length > 1 ? node.childNodes : node.childNodes[0];
}
/**
* Takes a string of html and escapes it using the brower's own escaping mechanism.
* @param {String} html - html to be escaped
*/
static escapeHTML(html) {
const textNode = document.createTextNode('');
const spanElement = document.createElement('span');
spanElement.append(textNode);
textNode.nodeValue = html;
return spanElement.innerHTML;
}
/**
* This is my shit version of not having to use `$` from jQuery. Meaning
* that you can pass a selector and it will automatically run {@link module:DOM.query}.
* It also means that you can pass a string of html and it will perform and return `parseHTML`.
* @see module:DOM.parseHTML
* @see module:DOM.query
* @param {string} selector - Selector to query or HTML to parse
* @returns {(DocumentFragment|NodeList|HTMLElement)} - Either the result of `parseHTML` or `query`
*/
static Q(selector) {
const element = this.parseHTML(selector);
const isHTML = element instanceof NodeList ? Array.from(element).some(n => n.nodeType === 1) : element.nodeType === 1;
if (isHTML) return element;
return this.query(selector);
}
/**
* Adds a list of classes from the target element.
* @param {Element} element - Element to edit classes of
* @param {...string} classes - Names of classes to add
* @returns {Element} - `element` to allow for chaining
*/
static addClass(element, ...classes) {
for (let c = 0; c < classes.length; c++) classes[c] = classes[c].toString().split(' ');
classes = classes.flatten().filter(c => c);
element.classList.add(...classes);
return element;
}
/**
* Removes a list of classes from the target element.
* @param {Element} element - Element to edit classes of
* @param {...string} classes - Names of classes to remove
* @returns {Element} - `element` to allow for chaining
*/
static removeClass(element, ...classes) {
for (let c = 0; c < classes.length; c++) classes[c] = classes[c].toString().split(' ');
classes = classes.flatten().filter(c => c);
element.classList.remove(...classes);
return element;
}
/**
* When only one argument is present: Toggle class value;
* i.e., if class exists then remove it and return false, if not, then add it and return true.
* When a second argument is present:
* If the second argument evaluates to true, add specified class value, and if it evaluates to false, remove it.
* @param {Element} element - Element to edit classes of
* @param {string} classname - Name of class to toggle
* @param {boolean} [indicator] - Optional indicator for if the class should be toggled
* @returns {Element} - `element` to allow for chaining
*/
static toggleClass(element, classname, indicator) {
classname = classname.toString().split(' ').filter(c => c);
if (typeof(indicator) !== 'undefined') classname.forEach(c => element.classList.toggle(c, indicator));
else classname.forEach(c => element.classList.toggle(c));
return element;
}
/**
* Checks if an element has a specific class
* @param {Element} element - Element to edit classes of
* @param {string} classname - Name of class to check
* @returns {boolean} - `true` if the element has the class, `false` otherwise.
*/
static hasClass(element, classname) {
return classname.toString().split(' ').filter(c => c).every(c => element.classList.contains(c));
}
/**
* Replaces one class with another
* @param {Element} element - Element to edit classes of
* @param {string} oldName - Name of class to replace
* @param {string} newName - New name for the class
* @returns {Element} - `element` to allow for chaining
*/
static replaceClass(element, oldName, newName) {
element.classList.replace(oldName, newName);
return element;
}
/**
* Appends `thisNode` to `thatNode`
* @param {Node} thisNode - Node to be appended to another node
* @param {Node} thatNode - Node for `thisNode` to be appended to
* @returns {Node} - `thisNode` to allow for chaining
*/
static appendTo(thisNode, thatNode) {
if (typeof(thatNode) == 'string') thatNode = this.query(thatNode);
if (!thatNode) return null;
thatNode.append(thisNode);
return thisNode;
}
/**
* Prepends `thisNode` to `thatNode`
* @param {Node} thisNode - Node to be prepended to another node
* @param {Node} thatNode - Node for `thisNode` to be prepended to
* @returns {Node} - `thisNode` to allow for chaining
*/
static prependTo(thisNode, thatNode) {
if (typeof(thatNode) == 'string') thatNode = this.query(thatNode);
if (!thatNode) return null;
thatNode.prepend(thisNode);
return thisNode;
}
/**
* Insert after a specific element, similar to jQuery's `thisElement.insertAfter(otherElement)`.
* @param {Node} thisNode - The node to insert
* @param {Node} targetNode - Node to insert after in the tree
* @returns {Node} - `thisNode` to allow for chaining
*/
static insertAfter(thisNode, targetNode) {
targetNode.parentNode.insertBefore(thisNode, targetNode.nextSibling);
return thisNode;
}
/**
* Insert after a specific element, similar to jQuery's `thisElement.after(newElement)`.
* @param {Node} thisNode - The node to insert
* @param {Node} newNode - Node to insert after in the tree
* @returns {Node} - `thisNode` to allow for chaining
*/
static after(thisNode, newNode) {
thisNode.parentNode.insertBefore(newNode, thisNode.nextSibling);
return thisNode;
}
/**
* Gets the next sibling element that matches the selector.
* @param {Element} element - Element to get the next sibling of
* @param {string} [selector=''] - Optional selector
* @returns {Element} - The sibling element
*/
static next(element, selector = '') {
return selector ? element.querySelector(`+ ${selector}`) : element.nextElementSibling;
}
/**
* Gets all subsequent siblings.
* @param {Element} element - Element to get next siblings of
* @returns {NodeList} - The list of siblings
*/
static nextAll(element) {
return element.querySelectorAll('~ *');
}
/**
* Gets the subsequent siblings until an element matches the selector.
* @param {Element} element - Element to get the following siblings of
* @param {string} selector - Selector to stop at
* @returns {Array<Element>} - The list of siblings
*/
static nextUntil(element, selector) {
const next = [];
while (element.nextElementSibling && !element.nextElementSibling.matches(selector)) next.push(element = element.nextElementSibling);
return next;
}
/**
* Gets the previous sibling element that matches the selector.
* @param {Element} element - Element to get the previous sibling of
* @param {string} [selector=''] - Optional selector
* @returns {Element} - The sibling element
*/
static previous(element, selector = '') {
const previous = element.previousElementSibling;
if (selector) return previous && previous.matches(selector) ? previous : null;
return previous;
}
/**
* Gets all preceeding siblings.
* @param {Element} element - Element to get preceeding siblings of
* @returns {NodeList} - The list of siblings
*/
static previousAll(element) {
const previous = [];
while (element.previousElementSibling) previous.push(element = element.previousElementSibling);
return previous;
}
/**
* Gets the preceeding siblings until an element matches the selector.
* @param {Element} element - Element to get the preceeding siblings of
* @param {string} selector - Selector to stop at
* @returns {Array<Element>} - The list of siblings
*/
static previousUntil(element, selector) {
const previous = [];
while (element.previousElementSibling && !element.previousElementSibling.matches(selector)) previous.push(element = element.previousElementSibling);
return previous;
}
/** Shorthand for {@link module:DOM.indexInParent} */
static index(node) {return this.indexInParent(node);}
/**
* Find which index in children a certain node is. Similar to jQuery's `$.index()`
* @param {HTMLElement} node - The node to find its index in parent
* @returns {number} Index of the node
*/
static indexInParent(node) {
const children = node.parentNode.childNodes;
let num = 0;
for (let i = 0; i < children.length; i++) {
if (children[i] == node) return num;
if (children[i].nodeType == 1) num++;
}
return -1;
}
/**
* Gets the parent of the element if it matches the selector,
* otherwise returns null.
* @param {Element} element - Element to get parent of
* @param {string} [selector=''] - Selector to match parent
* @returns {(Element|null)} - The sibling element or null
*/
static parent(element, selector = '') {
return !selector || element.parentElement.matches(selector) ? element.parentElement : null;
}
/**
* Gets all ancestors of Element that match the selector if provided.
* @param {Element} element - Element to get all parents of
* @param {string} [selector=''] - Selector to match the parents to
* @returns {Array<Element>} - The list of parents
*/
static parents(element, selector = '') {
const parents = [];
if (selector) while (element.parentElement && element.parentElement.closest(selector)) parents.push(element = element.parentElement.closest(selector));
else while (element.parentElement) parents.push(element = element.parentElement);
return parents;
}
/**
* Gets the ancestors until an element matches the selector.
* @param {Element} element - Element to get the ancestors of
* @param {string} selector - Selector to stop at
* @returns {Array<Element>} - The list of parents
*/
static parentsUntil(element, selector) {
const parents = [];
while (element.parentElement && !element.parentElement.matches(selector)) parents.push(element = element.parentElement);
return parents;
}
/**
* Gets all children of Element that match the selector if provided.
* @param {Element} element - Element to get all children of
* @param {string} selector - Selector to match the children to
* @returns {Array<Element>} - The list of children
*/
static findChild(element, selector) {
return element.querySelector(`:scope > ${selector}`);
}
/**
* Gets all children of Element that match the selector if provided.
* @param {Element} element - Element to get all children of
* @param {string} selector - Selector to match the children to
* @returns {Array<Element>} - The list of children
*/
static findChildren(element, selector) {
return element.querySelectorAll(`:scope > ${selector}`);
}
/**
* Gets all siblings of the element that match the selector.
* @param {Element} element - Element to get all siblings of
* @param {string} [selector='*'] - Selector to match the siblings to
* @returns {Array<Element>} - The list of siblings
*/
static siblings(element, selector = '*') {
return Array.from(element.parentElement.children).filter(e => e != element && e.matches(selector));
}
/**
* Sets or gets css styles for a specific element. If `value` is provided
* then it sets the style and returns the element to allow for chaining,
* otherwise returns the style.
* @param {Element} element - Element to set the CSS of
* @param {string} attribute - Attribute to get or set
* @param {string} [value] - Value to set for attribute
* @returns {Element|string} - When setting a value, element is returned for chaining, otherwise the value is returned.
*/
static css(element, attribute, value) {
if (typeof(value) == 'undefined') return global.getComputedStyle(element)[attribute];
element.style[attribute] = value;
return element;
}
/**
* Sets or gets the width for a specific element. If `value` is provided
* then it sets the width and returns the element to allow for chaining,
* otherwise returns the width.
* @param {Element} element - Element to set the CSS of
* @param {string} [value] - Width to set
* @returns {Element|string} - When setting a value, element is returned for chaining, otherwise the value is returned.
*/
static width(element, value) {
if (typeof(value) == 'undefined') return parseInt(getComputedStyle(element).width);
element.style.width = value;
return element;
}
/**
* Sets or gets the height for a specific element. If `value` is provided
* then it sets the height and returns the element to allow for chaining,
* otherwise returns the height.
* @param {Element} element - Element to set the CSS of
* @param {string} [value] - Height to set
* @returns {Element|string} - When setting a value, element is returned for chaining, otherwise the value is returned.
*/
static height(element, value) {
if (typeof(value) == 'undefined') return parseInt(getComputedStyle(element).height);
element.style.height = value;
return element;
}
/**
* Sets the inner text of an element if given a value, otherwise returns it.
* @param {Element} element - Element to set the text of
* @param {string} [text] - Content to set
* @returns {string} - Either the string set by this call or the current text content of the node.
*/
static text(element, text) {
if (typeof(text) == 'undefined') return element.textContent;
return element.textContent = text;
}
/**
* Returns the innerWidth of the element.
* @param {Element} element - Element to retrieve inner width of
* @return {number} - The inner width of the element.
*/
static innerWidth(element) {
return element.clientWidth;
}
/**
* Returns the innerHeight of the element.
* @param {Element} element - Element to retrieve inner height of
* @return {number} - The inner height of the element.
*/
static innerHeight(element) {
return element.clientHeight;
}
/**
* Returns the outerWidth of the element.
* @param {Element} element - Element to retrieve outer width of
* @return {number} - The outer width of the element.
*/
static outerWidth(element) {
return element.offsetWidth;
}
/**
* Returns the outerHeight of the element.
* @param {Element} element - Element to retrieve outer height of
* @return {number} - The outer height of the element.
*/
static outerHeight(element) {
return element.offsetHeight;
}
/**
* Gets the offset of the element in the page.
* @param {Element} element - Element to get offset of
* @return {Offset} - The offset of the element
*/
static offset(element) {
return element.getBoundingClientRect();
}
static get listeners() { return this._listeners || (this._listeners = {}); }
/**
* This is similar to jQuery's `on` function and can *hopefully* be used in the same way.
*
* Rather than attempt to explain, I'll show some example usages.
*
* The following will add a click listener (in the `myPlugin` namespace) to `element`.
* `DOM.on(element, 'click.myPlugin', () => {console.log('clicked!');});`
*
* The following will add a click listener (in the `myPlugin` namespace) to `element` that only fires when the target is a `.block` element.
* `DOM.on(element, 'click.myPlugin', '.block', () => {console.log('clicked!');});`
*
* The following will add a click listener (without namespace) to `element`.
* `DOM.on(element, 'click', () => {console.log('clicked!');});`
*
* The following will add a click listener (without namespace) to `element` that only fires once.
* `const cancel = DOM.on(element, 'click', () => {console.log('fired!'); cancel();});`
*
* @param {Element} element - Element to add listener to
* @param {string} event - Event to listen to with option namespace (e.g. 'event.namespace')
* @param {(string|callable)} delegate - Selector to run on element to listen to
* @param {callable} [callback] - Function to fire on event
* @returns {module:DOM~CancelListener} - A function that will undo the listener
*/
static on(element, event, delegate, callback) {
const [type, namespace] = event.split('.');
const hasDelegate = delegate && callback;
if (!callback) callback = delegate;
const eventFunc = !hasDelegate ? callback : function(event) {
if (event.target.matches(delegate)) {
callback(event);
}
};
element.addEventListener(type, eventFunc);
const cancel = () => {
element.removeEventListener(type, eventFunc);
};
if (namespace) {
if (!this.listeners[namespace]) this.listeners[namespace] = [];
const newCancel = () => {
cancel();
this.listeners[namespace].splice(this.listeners[namespace].findIndex(l => l.event == type && l.element == element), 1);
};
this.listeners[namespace].push({
event: type,
element: element,
cancel: newCancel
});
return newCancel;
}
return cancel;
}
/**
* Functionality for this method matches {@link module:DOM.on} but automatically cancels itself
* and removes the listener upon the first firing of the desired event.
*
* @param {Element} element - Element to add listener to
* @param {string} event - Event to listen to with option namespace (e.g. 'event.namespace')
* @param {(string|callable)} delegate - Selector to run on element to listen to
* @param {callable} [callback] - Function to fire on event
* @returns {module:DOM~CancelListener} - A function that will undo the listener
*/
static once(element, event, delegate, callback) {
const [type, namespace] = event.split('.');
const hasDelegate = delegate && callback;
if (!callback) callback = delegate;
const eventFunc = !hasDelegate ? function(event) {
callback(event);
element.removeEventListener(type, eventFunc);
} : function(event) {
if (!event.target.matches(delegate)) return;
callback(event);
element.removeEventListener(type, eventFunc);
};
element.addEventListener(type, eventFunc);
const cancel = () => {
element.removeEventListener(type, eventFunc);
};
if (namespace) {
if (!this.listeners[namespace]) this.listeners[namespace] = [];
const newCancel = () => {
cancel();
this.listeners[namespace].splice(this.listeners[namespace].findIndex(l => l.event == type && l.element == element), 1);
};
this.listeners[namespace].push({
event: type,
element: element,
cancel: newCancel
});
return newCancel;
}
return cancel;
}
static __offAll(event, element) {
const [type, namespace] = event.split('.');
let matchFilter = listener => listener.event == type, defaultFilter = _ => _;
if (element) matchFilter = l => l.event == type && l.element == element, defaultFilter = l => l.element == element;
const listeners = this.listeners[namespace] || [];
const list = type ? listeners.filter(matchFilter) : listeners.filter(defaultFilter);
for (let c = 0; c < list.length; c++) list[c].cancel();
}
/**
* This is similar to jQuery's `off` function and can *hopefully* be used in the same way.
*
* Rather than attempt to explain, I'll show some example usages.
*
* The following will remove a click listener called `onClick` (in the `myPlugin` namespace) from `element`.
* `DOM.off(element, 'click.myPlugin', onClick);`
*
* The following will remove a click listener called `onClick` (in the `myPlugin` namespace) from `element` that only fired when the target is a `.block` element.
* `DOM.off(element, 'click.myPlugin', '.block', onClick);`
*
* The following will remove a click listener (without namespace) from `element`.
* `DOM.off(element, 'click', onClick);`
*
* The following will remove all listeners in namespace `myPlugin` from `element`.
* `DOM.off(element, '.myPlugin');`
*
* The following will remove all click listeners in namespace `myPlugin` from *all elements*.
* `DOM.off('click.myPlugin');`
*
* The following will remove all listeners in namespace `myPlugin` from *all elements*.
* `DOM.off('.myPlugin');`
*
* @param {(Element|string)} element - Element to remove listener from
* @param {string} [event] - Event to listen to with option namespace (e.g. 'event.namespace')
* @param {(string|callable)} [delegate] - Selector to run on element to listen to
* @param {callable} [callback] - Function to fire on event
* @returns {Element} - The original element to allow for chaining
*/
static off(element, event, delegate, callback) {
if (typeof(element) == 'string') return this.__offAll(element);
const [type, namespace] = event.split('.');
if (namespace) return this.__offAll(event, element);
const hasDelegate = delegate && callback;
if (!callback) callback = delegate;
const eventFunc = !hasDelegate ? callback : function(event) {
if (event.target.matches(delegate)) {
callback(event);
}
};
element.removeEventListener(type, eventFunc);
return element;
}
/**
* Adds a listener for when the node is added/removed from the document body.
* The listener is automatically removed upon firing.
* @param {HTMLElement} node - node to wait for
* @param {callable} callback - function to be performed on event
* @param {boolean} onMount - determines if it should fire on Mount or on Unmount
*/
static onMountChange(node, callback, onMount = true) {
const wrappedCallback = () => {
this.observer.unsubscribe(wrappedCallback);
callback();
};
return this.observer.subscribe(wrappedCallback, mutation => {
const nodes = Array.from(onMount ? mutation.addedNode : mutation.removedNodes);
const directMatch = nodes.indexOf(node) > -1;
const parentMatch = nodes.some(parent => parent.contains(node));
return directMatch || parentMatch;
});
}
/** Shorthand for {@link module:DOM.onMountChange} with third parameter `true` */
static onMount(node, callback) { return this.onMountChange(node, callback); }
/** Shorthand for {@link module:DOM.onMountChange} with third parameter `false` */
static onUnmount(node, callback) { return this.onMountChange(node, callback, false); }
/** Alias for {@link module:DOM.onMount} */
static onAdded(node, callback) { return this.onMount(node, callback); }
/** Alias for {@link module:DOM.onUnmount} */
static onRemoved(node, callback) { return this.onUnmount(node, callback); }
}

View File

@ -12,6 +12,8 @@ import { Filters, ClientLogger as Logger } from 'common';
import { ReactComponents } from 'modules';
class Reflection {
static get reactRootInstance() {return document.getElementById('app-mount')._reactRootContainer._internalRoot.current;}
static reactInternalInstance(node) {
if (!node) return null;
if (!Object.keys(node) || !Object.keys(node).length) return null;

83
client/src/ui/tooltip.js Normal file
View File

@ -0,0 +1,83 @@
/**
* Tooltips that automatically show and hide themselves on mouseenter and mouseleave events.
* Will also remove themselves if the node to watch is removed from DOM through
* a DOMObserver.
*
* Note: This comes from Zerebos' library.
*
* @module Tooltip
* @version 0.0.1
*/
import { Reflection } from 'modules';
import { Screen } from 'structs';
import DOM from './dom';
export default class Tooltip {
/**
*
* @constructor
* @param {HTMLElement} node - DOM node to monitor and show the tooltip on
* @param {string} tip - string to show in the tooltip
* @param {object} options - additional options for the tooltip
* @param {string} [options.style=black] - correlates to the discord styling
* @param {string} [options.side=top] - can be any of top, right, bottom, left
* @param {boolean} [options.preventFlip=false] - prevents moving the tooltip to the opposite side if it is too big or goes offscreen
* @param {boolean} [options.disabled=false] - whether the tooltip should be disabled from showing on hover
*/
constructor(node, text, options = {}) {
this.node = node;
const {style = 'black', side = 'top', disabled = false} = options;
this.label = text;
this.style = style;
this.side = side;
this.disabled = disabled;
this.id = Reflection.modules.KeyGenerator();
this.hide = this.hide.bind(this);
this.node.addEventListener('mouseenter', () => {
if (this.disabled) return;
this.show();
DOM.onUnmount(this.node, this.hide);
});
this.node.addEventListener('mouseleave', () => {
this.hide();
DOM.observer.unsubscribe(this.hide);
});
}
/**
* Disabled the tooltip and prevents it from showing on hover.
*/
disable() {
this.disabled = true;
}
/**
* Enables the tooltip and allows it to show on hover.
*/
enable() {
this.disabled = false;
}
/** Hides the tooltip. Automatically called on mouseleave. */
hide() {
Reflection.modules.Tooltips.hide(this.id);
}
/** Shows the tooltip. Automatically called on mouseenter. */
show() {
const {left, top, width, height} = this.node.getBoundingClientRect();
Reflection.modules.Tooltips.show(this.id, {
position: this.side,
text: this.label,
color: this.style,
targetWidth: width,
targetHeight: height,
windowWidth: Screen.width,
windowHeight: Screen.height,
x: left,
y: top
});
}
}

View File

@ -3,6 +3,7 @@ export { default as BdUI } from './bdui';
export { default as BdMenu, BdMenuItems } from './bdmenu';
export { default as Modals } from './modals';
export { default as Toasts } from './toasts';
export { default as Tooltip } from './tooltip';
export { default as Notifications } from './notifications';
export * from './contextmenus';

View File

@ -54,6 +54,24 @@ export class Utils {
return camelCased;
}
/**
* Format strings with placeholders (`{{placeholder}}`) into full strings.
* Quick example: `formatString("Hello, {{user}}", {user: "Zerebos"})`
* would return "Hello, Zerebos".
* @param {string} string - string to format
* @param {object} values - object literal of placeholders to replacements
* @returns {string} the properly formatted string
*/
static formatString(string, values) {
for (const val in values) {
let replacement = values[val];
if (Array.isArray(replacement)) replacement = JSON.stringify(replacement);
if (typeof(replacement) === 'object' && replacement !== null) replacement = replacement.toString();
string = string.replace(new RegExp(`{{${val}}}`, 'g'), replacement);
}
return string;
}
/**
* Finds a value, subobject, or array from a tree that matches a specific filter. Great for patching render functions.
* @param {object} tree React tree to look through. Can be a rendered object or an internal instance.
@ -96,6 +114,18 @@ export class Utils {
return tempReturn;
}
/**
* Gets a nested property (if it exists) safely. Path should be something like `prop.prop2.prop3`.
* Numbers can be used for arrays as well like `prop.prop2.array.0.id`.
* @param {Object} obj - object to get nested property of
* @param {string} path - representation of the property to obtain
*/
static getNestedProp(obj, path) {
return path.split(/\s?\.\s?/).reduce(function(obj, prop) {
return obj && obj[prop];
}, obj);
}
/**
* Checks if two or more values contain the same data.
* @param {Any} ...value The value to compare
@ -126,6 +156,28 @@ export class Utils {
return true;
}
/**
* Deep extends an object with a set of other objects. Objects later in the list
* of `extenders` have priority, that is to say if one sets a key to be a primitive,
* it will be overwritten with the next one with the same key. If it is an object,
* and the keys match, the object is extended. This happens recursively.
* @param {object} extendee - Object to be extended
* @param {...object} extenders - Objects to extend with
* @returns {object} - A reference to `extendee`
*/
static extend(extendee, ...extenders) {
for (let i = 0; i < extenders.length; i++) {
for (const key in extenders[i]) {
if (extenders[i].hasOwnProperty(key)) {
if (typeof extendee[key] === 'object' && typeof extenders[i][key] === 'object') this.extend(extendee[key], extenders[i][key]);
else if (typeof extenders[i][key] === 'object') extendee[key] = {}, this.extend(extendee[key], extenders[i][key]);
else extendee[key] = extenders[i][key];
}
}
}
return extendee;
}
/**
* Clones an object and all it's properties.
* @param {Any} value The value to clone
@ -212,6 +264,36 @@ export class Utils {
});
}
/**
* Generates an automatically memoizing version of an object.
* @param {Object} object - object to memoize
* @returns {Proxy} the proxy to the object that memoizes properties
*/
static memoizeObject(object) {
const proxy = new Proxy(object, {
get: function(obj, mod) {
if (!obj.hasOwnProperty(mod)) return undefined;
if (Object.getOwnPropertyDescriptor(obj, mod).get) {
const value = obj[mod];
delete obj[mod];
obj[mod] = value;
}
return obj[mod];
},
set: function(obj, mod, value) {
if (obj.hasOwnProperty(mod)) return;
obj[mod] = value;
return obj[mod];
}
});
Object.defineProperty(proxy, 'hasOwnProperty', {value: function(prop) {
return this[prop] !== undefined;
}});
return proxy;
}
static wait(time = 0) {
return new Promise(resolve => setTimeout(resolve, time));
}
@ -227,6 +309,33 @@ export class Utils {
return value;
}
/**
* Stably sorts arrays since `.sort()` has issues.
* @param {Array} list - array to sort
* @param {function} comparator - comparator to sort by
*/
static stableSort(list, comparator) {
const length = list.length;
const entries = Array(length);
let index;
// wrap values with initial indices
for (index = 0; index < length; index++) {
entries[index] = [index, list[index]];
}
// sort with fallback based on initial indices
entries.sort(function (a, b) {
const comparison = Number(this(a[1], b[1]));
return comparison || a[0] - b[0];
}.bind(comparator));
// re-map original array to stable sorted values
for (index = 0; index < length; index++) {
list[index] = entries[index][1];
}
}
/**
* Finds the index of array of bytes in another array
* @param {Array} haystack The array to find aob in

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "betterdiscord",
"version": "2.0.0-beta.4",
"version": "2.0.0-beta.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {