Merge 133132e85c
into 835cc3134c
This commit is contained in:
commit
8725a3a648
|
@ -8,11 +8,53 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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 = {
|
const PermissionMap = {
|
||||||
IDENTIFY: {
|
IDENTIFY: {
|
||||||
HEADER: 'Access your account information',
|
HEADER: 'Access your account information',
|
||||||
BODY: 'Allows :NAME: to read your account information (excluding user token).'
|
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: {
|
READ_MESSAGES: {
|
||||||
HEADER: 'Read all messages',
|
HEADER: 'Read all messages',
|
||||||
BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
|
BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
|
||||||
|
@ -29,9 +71,37 @@ const PermissionMap = {
|
||||||
HEADER: 'Edit messages',
|
HEADER: 'Edit messages',
|
||||||
BODY: 'Allows :NAME: to edit messages on your behalf.'
|
BODY: 'Allows :NAME: to edit messages on your behalf.'
|
||||||
},
|
},
|
||||||
JOIN_SERVERS: {
|
ATTACH_FILES: {
|
||||||
HEADER: 'Join servers for you',
|
HEADER: 'Attach files',
|
||||||
BODY: 'Allows :NAME: to join servers on your behalf.'
|
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.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EmoteModule } from 'builtin';
|
import { EmoteModule } from 'builtin';
|
||||||
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
|
import { SettingsSet, SettingsCategory, Setting, SettingsScheme, Screen } from 'structs';
|
||||||
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
|
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Tooltip } from 'ui';
|
||||||
import * as CommonComponents from 'commoncomponents';
|
import * as CommonComponents from 'commoncomponents';
|
||||||
import { default as Components } from '../ui/components/generic';
|
import { default as Components } from '../ui/components/generic';
|
||||||
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
|
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
|
||||||
|
@ -28,6 +28,7 @@ import GlobalAc from '../ui/autocomplete';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Globals from './globals';
|
import Globals from './globals';
|
||||||
|
import { remote } from 'electron';
|
||||||
|
|
||||||
export default class PluginApi {
|
export default class PluginApi {
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@ export default class PluginApi {
|
||||||
get Reflection() { return Reflection }
|
get Reflection() { return Reflection }
|
||||||
get DOM() { return DOM }
|
get DOM() { return DOM }
|
||||||
get VueInjector() { return VueInjector }
|
get VueInjector() { return VueInjector }
|
||||||
|
get Screen() { return Screen }
|
||||||
|
|
||||||
get observer() {
|
get observer() {
|
||||||
return this._observer || (this._observer = new DOMObserver());
|
return this._observer || (this._observer = new DOMObserver());
|
||||||
|
@ -112,7 +114,12 @@ export default class PluginApi {
|
||||||
wait: (...args) => Utils.wait.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),
|
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
|
* 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
|
* Patcher
|
||||||
*/
|
*/
|
||||||
|
@ -616,6 +654,22 @@ export default class PluginApi {
|
||||||
return VueInjector.createReactElement(Vue.component(id, component), props);
|
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
|
// Stop plugins from modifying the plugin API for all plugins
|
||||||
|
|
|
@ -155,6 +155,7 @@ const KnownModules = {
|
||||||
/* DOM/React Components */
|
/* DOM/React Components */
|
||||||
/* ==================== */
|
/* ==================== */
|
||||||
LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
|
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']),
|
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
|
||||||
ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
|
ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
|
||||||
GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),
|
GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),
|
||||||
|
|
|
@ -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;
|
|
@ -1,4 +1,5 @@
|
||||||
export { default as List } from './list';
|
export { default as List } from './list';
|
||||||
|
export { default as Screen } from './screen';
|
||||||
|
|
||||||
export * from './events/index';
|
export * from './events/index';
|
||||||
export * from './settings/index';
|
export * from './settings/index';
|
||||||
|
|
|
@ -11,25 +11,68 @@
|
||||||
import { Utils, ClientLogger as Logger } from 'common';
|
import { Utils, ClientLogger as Logger } from 'common';
|
||||||
|
|
||||||
class BdNode {
|
class BdNode {
|
||||||
constructor(tag, className, id) {
|
constructor(node, attributes) {
|
||||||
this.element = document.createElement(tag);
|
this.element = node;
|
||||||
if (className) this.element.className = className;
|
DOM.setAttributes(node, attributes);
|
||||||
if (id) this.element.id = id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appendTo(e) {
|
get index() { return DOM.index(this.element); }
|
||||||
const el = DOM.getElement(e);
|
get innerWidth() { return DOM.innerWidth(this.element); }
|
||||||
if (!el) return null;
|
get innerHeight() { return DOM.innerHeight(this.element); }
|
||||||
el.append(this.element);
|
get outerWidth() { return DOM.outerWidth(this.element); }
|
||||||
return this.element;
|
get outerHeight() { return DOM.outerHeight(this.element); }
|
||||||
}
|
get offset() { return DOM.offset(this.element); }
|
||||||
|
|
||||||
prependTo(e) {
|
get width() { return DOM.width(this.element); }
|
||||||
const el = DOM.getElement(e);
|
set width(value) { return DOM.width(this.element, value); }
|
||||||
if (!el) return null;
|
get height() { return DOM.height(this.element); }
|
||||||
el.prepend(this.element);
|
set height(value) { return DOM.height(this.element, value); }
|
||||||
return this.element;
|
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 {
|
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 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 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 BdNode) return e.element;
|
||||||
if (e instanceof window.Node) return e;
|
if (e instanceof window.Node) return e;
|
||||||
if ('string' !== typeof e) return null;
|
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) {
|
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); }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { Filters, ClientLogger as Logger } from 'common';
|
||||||
import { ReactComponents } from 'modules';
|
import { ReactComponents } from 'modules';
|
||||||
|
|
||||||
class Reflection {
|
class Reflection {
|
||||||
|
static get reactRootInstance() {return document.getElementById('app-mount')._reactRootContainer._internalRoot.current;}
|
||||||
|
|
||||||
static reactInternalInstance(node) {
|
static reactInternalInstance(node) {
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
if (!Object.keys(node) || !Object.keys(node).length) return null;
|
if (!Object.keys(node) || !Object.keys(node).length) return null;
|
||||||
|
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ export { default as BdUI } from './bdui';
|
||||||
export { default as BdMenu, BdMenuItems } from './bdmenu';
|
export { default as BdMenu, BdMenuItems } from './bdmenu';
|
||||||
export { default as Modals } from './modals';
|
export { default as Modals } from './modals';
|
||||||
export { default as Toasts } from './toasts';
|
export { default as Toasts } from './toasts';
|
||||||
|
export { default as Tooltip } from './tooltip';
|
||||||
export { default as Notifications } from './notifications';
|
export { default as Notifications } from './notifications';
|
||||||
export * from './contextmenus';
|
export * from './contextmenus';
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,24 @@ export class Utils {
|
||||||
return camelCased;
|
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.
|
* 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.
|
* @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;
|
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.
|
* Checks if two or more values contain the same data.
|
||||||
* @param {Any} ...value The value to compare
|
* @param {Any} ...value The value to compare
|
||||||
|
@ -126,6 +156,28 @@ export class Utils {
|
||||||
return true;
|
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.
|
* Clones an object and all it's properties.
|
||||||
* @param {Any} value The value to clone
|
* @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) {
|
static wait(time = 0) {
|
||||||
return new Promise(resolve => setTimeout(resolve, time));
|
return new Promise(resolve => setTimeout(resolve, time));
|
||||||
}
|
}
|
||||||
|
@ -227,6 +309,33 @@ export class Utils {
|
||||||
return value;
|
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
|
* Finds the index of array of bytes in another array
|
||||||
* @param {Array} haystack The array to find aob in
|
* @param {Array} haystack The array to find aob in
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "betterdiscord",
|
"name": "betterdiscord",
|
||||||
"version": "2.0.0-beta.4",
|
"version": "2.0.0-beta.5",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
Loading…
Reference in New Issue