1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
FrankerFaceZ/src/utilities/object.js
SirStendec 734a73eb0e 4.3.2
* Fixed: Maintain the mouse hover state for chat when moving the mouse over the context menu from an in-line chat action.
* Fixed: Do not allow mouse interaction with the notice at the bottom of chat when chat if frozen, ensuring that users can interact with the bottom chat line.
* Fixed: Block and Hide Thumbnails buttons not appearing on Directory pages.
* API Fixed: `deep_copy()` converting `null` into `{}`.
* API Changed: Tooltips can now emit hover events.
2019-06-12 21:13:53 -04:00

531 lines
No EOL
11 KiB
JavaScript

'use strict';
const HOP = Object.prototype.hasOwnProperty;
// Source: https://gist.github.com/jed/982883 (WTFPL)
export function generateUUID(input) {
return input // if the placeholder was passed, return
? ( // a random number from 0 to 15
input ^ // unless b is 8,
Math.random() // in which case
* 16 // a random number from
>> input/4 // 8 to 11
).toString(16) // in hexadecimal
: ( // or otherwise a concatenated string:
[1e7] + // 10000000 +
-1e3 + // -1000 +
-4e3 + // -4000 +
-8e3 + // -80000000 +
-1e11 // -100000000000,
).replace( // replacing
/[018]/g, // zeroes, ones, and eights with
generateUUID // random hex digits
);
}
export function has(object, key) {
return object ? HOP.call(object, key) : false;
}
export function sleep(delay) {
return new Promise(s => setTimeout(s, delay));
}
export function make_enum(...array) {
const out = {};
for(let i=0; i < array.length; i++) {
const word = array[i];
out[word] = i;
out[i] = word;
}
return out;
}
export function timeout(promise, delay) {
return new Promise((resolve, reject) => {
let resolved = false;
const timer = setTimeout(() => {
if ( ! resolved ) {
resolved = true;
reject(new Error('timeout'));
}
}, delay);
promise.then(result => {
if ( ! resolved ) {
resolved = true;
clearTimeout(timer);
resolve(result);
}
}).catch(err => {
if ( ! resolved ) {
resolved = true;
clearTimeout(timer);
reject(err);
}
});
});
}
/**
* Return a wrapper for a function that will only execute the function
* a period of time after it has stopped being called.
* @param {Function} fn The function to wrap.
* @param {Integer} delay The time to wait, in milliseconds
* @param {Boolean} immediate If immediate is true, trigger the function immediately rather than eventually.
* @returns {Function} wrapped function
*/
export function debounce(fn, delay, immediate) {
let timer;
if ( immediate ) {
const later = () => timer = null;
if ( immediate === 2 )
// Special Mode! Run immediately OR later.
return function(...args) {
if ( timer ) {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, args); // eslint-disable-line no-invalid-this
}, delay);
} else {
fn.apply(this, args); // eslint-disable-line no-invalid-this
timer = setTimeout(later, delay);
}
}
return function(...args) {
if ( ! timer )
fn.apply(this, args); // eslint-disable-line no-invalid-this
else
clearTimeout(timer);
timer = setTimeout(later, delay);
}
}
return function(...args) {
if ( timer )
clearTimeout(timer);
timer = setTimeout(fn.bind(this, ...args), delay); // eslint-disable-line no-invalid-this
}
}
/**
* Make sure that a given asynchronous function is only called once
* at a time.
*/
export function once(fn) {
let waiters;
return function(...args) {
return new Promise(async (s,f) => {
if ( waiters )
return waiters.push([s,f]);
waiters = [[s,f]];
let result;
try {
result = await fn.call(this, ...args); // eslint-disable-line no-invalid-this
} catch(err) {
for(const w of waiters)
w[1](err);
waiters = null;
return;
}
for(const w of waiters)
w[0](result);
waiters = null;
})
}
}
/**
* Check that two arrays are the same length and that each array has the same
* items in the same indices.
* @param {Array} a The first array
* @param {Array} b The second array
* @returns {boolean} Whether or not they match
*/
export function array_equals(a, b) {
if ( ! Array.isArray(a) || ! Array.isArray(b) || a.length !== b.length )
return false;
let i = a.length;
while(i--)
if ( a[i] !== b[i] )
return false;
return true;
}
export function shallow_object_equals(a, b) {
if ( typeof a !== 'object' || typeof b !== 'object' || ! array_equals(Object.keys(a), Object.keys(b)) )
return false;
for(const key in a)
if ( a[key] !== b[key] )
return false;
return true;
}
export function map_equals(a, b) {
if ( !(a instanceof Map) || !(b instanceof Map) || a.size !== b.size )
return false;
for(const [key, val] of a)
if ( ! b.has(key) || b.get(key) !== val )
return false;
return true;
}
export function set_equals(a,b) {
if ( !(a instanceof Set) || !(b instanceof Set) || a.size !== b.size )
return false;
for(const v of a)
if ( ! b.has(v) )
return false;
return true;
}
/**
* Special logic to ensure that a target object is matched by a filter.
* @param {object} filter The filter object
* @param {object} target The object to check it against
* @returns {boolean} Whether or not it matches
*/
export function filter_match(filter, target) {
for(const key in filter) {
if ( HOP.call(filter, key) ) {
const filter_value = filter[key],
target_value = target[key],
type = typeof filter_value;
if ( type === 'function' ) {
if ( ! filter_value(target_value) )
return false;
} else if ( Array.isArray(filter_value) ) {
if ( Array.isArray(target_value) ) {
for(const val of filter_value)
if ( ! target_value.includes(val) )
return false;
} else if ( ! filter_value.include(target_value) )
return false;
} else if ( typeof target_value !== type )
return false;
else if ( type === 'object' ) {
if ( ! filter_match(filter_value, target_value) )
return false;
} else if ( filter_value !== target_value )
return false;
}
}
return true;
}
export function substr_count(str, needle) {
let i = 0, idx = 0;
while( idx < str.length ) {
const x = str.indexOf(needle, idx);
if ( x === -1 )
break;
i++;
idx = x + 1;
}
return i;
}
/**
* Get a value from an object at a path.
* @param {string|Array} path The path to follow, using periods to go down a level.
* @param {object|Array} object The starting object.
* @returns {*} The value at that point in the path, or undefined if part of the path doesn't exist.
*/
export function get(path, object) {
if ( typeof path === 'string' )
path = path.split('.');
for(let i=0, l = path.length; i < l; i++) {
const part = path[i];
if ( part === '@each' ) {
const p = path.slice(i + 1);
if ( p.length ) {
if ( Array.isArray )
object = object.map(x => get(p, x));
else {
const new_object = {};
for(const key in object)
if ( HOP.call(object, key) )
new_object[key] = get(p, object[key]);
object = new_object;
}
}
break;
} else if ( part === '@last' )
object = object[object.length - 1];
else
object = object[path[i]];
if ( ! object )
break;
}
return object;
}
export function deep_copy(object, seen) {
if ( object === null )
return null;
else if ( object === undefined )
return undefined;
if ( typeof object !== 'object' )
return object;
if ( ! seen )
seen = new Set;
if ( seen.has(object) )
throw new Error('recursive structure detected');
seen.add(object);
if ( Array.isArray(object) )
return object.map(x => deep_copy(x, new Set(seen)));
const out = {};
for(const key in object)
if ( HOP.call(object, key) ) {
const val = object[key];
if ( typeof val === 'object' )
out[key] = deep_copy(val, new Set(seen));
else
out[key] = val;
}
return out;
}
export function maybe_call(fn, ctx, ...args) {
if ( typeof fn === 'function' ) {
if ( ctx )
return fn.call(ctx, ...args);
return fn(...args);
}
return fn;
}
const SPLIT_REGEX = /[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g;
export function split_chars(str) {
if ( str === '' )
return [];
return str.match(SPLIT_REGEX);
}
export function pick_random(obj) {
if ( ! obj )
return null;
if ( ! Array.isArray(obj) )
return obj[pick_random(Object.keys(obj))]
return obj[Math.floor(Math.random() * obj.length)];
}
export const escape_regex = RegExp.escape || function escape_regex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const CONTROL_CHARS = '/$^+.()=!|';
export function glob_to_regex(input) {
if ( typeof input !== 'string' )
throw new TypeError('input must be a string');
let output = '',
groups = 0;
for(let i=0, l=input.length; i<l; i++) {
const char = input[i];
if ( CONTROL_CHARS.includes(char) )
output += `\\${char}`;
else if ( char === '?' )
output += '.';
else if ( char === '[' || char === ']' )
output += char;
else if ( char === '{' ) {
output += '(?:';
groups++;
} else if ( char === '}' ) {
if ( groups > 0 ) {
output += ')';
groups--;
}
} else if ( char === ',' && groups > 0 )
output += '|';
else if ( char === '*' ) {
let count = 1;
while(input[i+1] === '*') {
count++;
i++;
}
if ( count > 1 )
output += '.*?';
else
output += '[^ ]*?';
} else
output += char;
}
while(groups > 0) {
output += ')';
groups--;
}
return output;
}
export class SourcedSet {
constructor() {
this._cache = [];
}
_rebuild() {
if ( ! this._sources )
return;
this._cache = [];
for(const items of this._sources.values())
for(const i of items)
if ( ! this._cache.includes(i) )
this._cache.push(i);
}
get(key) { return this._sources && this._sources.get(key) }
has(key) { return this._sources ? this._sources.has(key) : false }
sourceIncludes(key, val) {
const src = this._sources && this._sources.get(key);
return src && src.includes(val);
}
includes(val) {
return this._cache.includes(val);
}
delete(key) {
if ( this._sources && this._sources.has(key) ) {
this._sources.delete(key);
this._rebuild();
}
}
extend(key, ...items) {
if ( ! this._sources )
this._sources = new Map;
const had = this.has(key);
this._sources.set(key, [false, items]);
if ( had )
this._rebuild();
else
for(const i of items)
if ( ! this._cache.includes(i) )
this._cache.push(i);
}
set(key, val) {
if ( ! this._sources )
this._sources = new Map;
const had = this.has(key);
this._sources.set(key, [val]);
if ( had )
this._rebuild();
else if ( ! this._cache.includes(val) )
this._cache.push(val);
}
push(key, val) {
if ( ! this._sources )
return this.set(key, val);
const old_val = this._sources.get(key);
if ( old_val === undefined )
return this.set(key, val);
else if ( old_val.includes(val) )
return;
old_val.push(val);
if ( ! this._cache.includes(val) )
this._cache.push(val);
}
remove(key, val) {
if ( ! this.has(key) )
return;
const old_val = this._sources.get(key),
idx = old_val.indexOf(val);
if ( idx === -1 )
return;
old_val.splice(idx, 1);
this._rebuild();
}
}