BetterDiscordApp-rauenzi/renderer/src/modules/utilities.js

218 lines
8.7 KiB
JavaScript
Raw Normal View History

2023-05-16 08:47:05 +02:00
import Logger from "../../../common/logger";
2019-06-04 21:17:23 +02:00
2019-05-29 05:48:41 +02:00
export default class Utilities {
2019-05-28 20:19:48 +02:00
/**
* Generates an automatically memoizing version of an object.
* @author Zerebos
* @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) {
2019-05-30 07:06:17 +02:00
const value = obj[mod];
2019-05-28 20:19:48 +02:00
delete obj[mod];
obj[mod] = value;
}
return obj[mod];
},
set: function(obj, mod, value) {
2019-06-20 20:03:48 +02:00
if (obj.hasOwnProperty(mod)) return Logger.error("MemoizedObject", "Trying to overwrite existing property");
2019-05-28 20:19:48 +02:00
obj[mod] = value;
return obj[mod];
}
});
Object.defineProperty(proxy, "hasOwnProperty", {value: function(prop) {
return this[prop] !== undefined;
}});
return proxy;
}
2019-06-20 04:19:34 +02:00
2019-06-24 21:47:24 +02:00
/**
* 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];
2021-10-22 22:34:48 +02:00
}
}
}
}
return extendee;
}
/**
* 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 extendTruthy(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.extendTruthy(extendee[key], extenders[i][key]);
}
else if (typeof extenders[i][key] === "object") {
extendee[key] = {};
this.extendTruthy(extendee[key], extenders[i][key]);
}
else if (extenders[i][key]) {
extendee[key] = extenders[i][key];
}
2019-06-24 21:47:24 +02:00
}
}
}
return extendee;
}
2019-06-20 04:19:34 +02:00
/**
* Format strings with placeholders (`{{placeholder}}`) into full strings.
* Quick example: `PluginUtilities.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;
}
/**
2022-10-02 09:34:34 +02:00
* Finds a value, subobject, or array from a tree that matches a specific filter. This is a DFS.
2019-06-20 04:19:34 +02:00
* @param {object} tree Tree that should be walked
* @param {callable} searchFilter Filter to check against each object and subobject
* @param {object} options Additional options to customize the search
* @param {Array<string>|null} [options.walkable=null] Array of strings to use as keys that are allowed to be walked on. Null value indicates all keys are walkable
* @param {Array<string>} [options.ignore=[]] Array of strings to use as keys to exclude from the search, most helpful when `walkable = null`.
*/
static findInTree(tree, searchFilter, {walkable = null, ignore = []} = {}) {
if (typeof searchFilter === "string") {
if (tree.hasOwnProperty(searchFilter)) return tree[searchFilter];
}
else if (searchFilter(tree)) {
return tree;
}
if (typeof tree !== "object" || tree == null) return undefined;
let tempReturn;
2019-06-20 04:19:34 +02:00
if (tree instanceof Array) {
for (const value of tree) {
tempReturn = this.findInTree(value, searchFilter, {walkable, ignore});
if (typeof tempReturn != "undefined") return tempReturn;
}
}
else {
const toWalk = walkable == null ? Object.keys(tree) : walkable;
for (const key of toWalk) {
2019-07-03 16:15:48 +02:00
if (typeof(tree[key]) == "undefined" || ignore.includes(key)) continue;
2019-06-20 04:19:34 +02:00
tempReturn = this.findInTree(tree[key], searchFilter, {walkable, ignore});
if (typeof tempReturn != "undefined") return tempReturn;
}
}
return tempReturn;
}
/**
2022-10-02 09:34:34 +02:00
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds.
*
* Adapted from the version by David Walsh (https://davidwalsh.name/javascript-debounce-function)
*
* @param {function} executor
* @param {number} delay
2019-06-20 04:19:34 +02:00
*/
2022-10-02 09:34:34 +02:00
static debounce(executor, delay) {
let timeout;
return function(...args) {
const callback = () => {
timeout = null;
Reflect.apply(executor, null, args);
};
clearTimeout(timeout);
timeout = setTimeout(callback, delay);
};
2019-06-20 04:19:34 +02:00
}
/**
2022-10-02 09:34:34 +02:00
* Takes a string of html and escapes it using the brower's own escaping mechanism.
* @param {String} html - html to be escaped
2019-06-20 04:19:34 +02:00
*/
2022-10-02 09:34:34 +02:00
static escapeHTML(html) {
const textNode = document.createTextNode("");
const spanElement = document.createElement("span");
spanElement.append(textNode);
textNode.nodeValue = html;
return spanElement.innerHTML;
2019-06-20 04:19:34 +02:00
}
/**
2022-10-02 09:34:34 +02:00
* 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.
2019-06-20 04:19:34 +02:00
*/
2022-10-02 09:34:34 +02:00
static className() {
const classes = [];
const hasOwn = {}.hasOwnProperty;
2019-06-20 04:19:34 +02:00
2022-10-02 09:34:34 +02:00
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];
if (!arg) continue;
2019-06-20 04:19:34 +02:00
2022-10-02 09:34:34 +02:00
const argType = typeof arg;
2019-06-20 20:03:48 +02:00
2022-10-02 09:34:34 +02:00
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 (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
2019-06-20 20:03:48 +02:00
2022-10-02 09:34:34 +02:00
return classes.join(" ");
}
2019-05-29 05:48:41 +02:00
}