이 스크립트는 직접 설치해서 쓰는 게 아닙니다. 다른 스크립트가 메타 명령 // @require https://update.greasyfork.org/scripts/474549/1263250/akkd-common.js
(으)로 포함하여 쓰는 라이브러리입니다.
// ==UserScript==
// #region Info
// @namespace https://greasyfork.org/en/users/1123632-93akkord
// @exclude *
// @author Michael Barros (https://greasyfork.org/en/users/1123632-93akkord)
// @icon data:image/png;base64,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
// #endregion Info
// ==UserLibrary==
// @name akkd-common
// @description Common functions
// @copyright 2022+, Michael Barros (https://greasyfork.org/en/users/1123632-93akkord)
// @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @version 0.0.20
// ==/UserScript==
// ==/UserLibrary==
// ==OpenUserJS==
// @author 93Akkord
// ==/OpenUserJS==
/// <reference path='C:/Users/mbarros/Documents/DevProjects/Web/Tampermonkey/Palantir/@types/__fullReferencePaths__.js' />
/*
# akkd-common
A collection of commonly used classes and functions.
## Required requires:
- https://code.jquery.com/jquery-3.2.1.min.js
- https://greasyfork.org/scripts/474546-loglevel/code/loglevel.js
*/
// #region Events
// Setup location change events
/**
*
* Example usage:
* ```javascript
* window.addEventListener('popstate', () => {
* window.dispatchEvent(new Event('locationchange'));
* });
*/
(() => {
class LocationChangeEvent extends Event {
constructor(type, prevUrl, newUrl) {
super(type);
this.prevUrl = prevUrl;
this.newUrl = newUrl;
}
}
let prevUrl = document.location.href;
let oldPushState = history.pushState;
history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments);
let newUrl = document.location.href;
window.dispatchEvent(new LocationChangeEvent('pushstate', prevUrl, newUrl));
window.dispatchEvent(new LocationChangeEvent('locationchange', prevUrl, newUrl));
prevUrl = newUrl;
return ret;
};
let oldReplaceState = history.replaceState;
history.replaceState = function replaceState() {
let ret = oldReplaceState.apply(this, arguments);
let newUrl = document.location.href;
window.dispatchEvent(new LocationChangeEvent('replacestate', prevUrl, newUrl));
window.dispatchEvent(new LocationChangeEvent('locationchange', prevUrl, newUrl));
prevUrl = newUrl;
return ret;
};
window.addEventListener('popstate', () => {
let newUrl = document.location.href;
window.dispatchEvent(new LocationChangeEvent('locationchange', prevUrl, newUrl));
prevUrl = newUrl;
});
})();
// #endregion Events
// #region Helper Classes
class Logger {
/**
* Creates an instance of Logger.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Window} _window
* @param {string | null} devTag
* @memberof Logger
*/
constructor(_window = null, devTag = null) {
/**
* @type {Window}
* @private
*/
this.window = _window || getWindow();
/** @type {string | null} */
this.devTag = devTag;
/** @type {string[]} */
this._additionalTags = [];
}
/**
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @type {string[]}
* @public
* @memberof Logger
*/
get additionalTags() {
return this._additionalTags;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string[]} value
* @memberof Logger
*/
set additionalTags(value) {
if (getType(value) != 'array') {
value = [value];
}
this._additionalTags = value;
}
/** @type {string} */
get label() {
return [].concat([this.devTag], this.additionalTags).join(' ');
}
/** @type {(...data: any[]) => void} */
get log() {
if (this.devTag) {
return console.log.bind(console, `${this.label}`);
} else {
return console.log.bind(console);
}
}
/** @type {(...data: any[]) => void} */
get info() {
if (this.devTag) {
return console.info.bind(console, `${this.label}`);
} else {
return console.info.bind(console);
}
}
/** @type {(...data: any[]) => void} */
get error() {
if (this.devTag) {
return console.error.bind(console, `${this.label}`);
} else {
return console.error.bind(console);
}
}
/** @type {(...data: any[]) => void} */
get debug() {
if (this.devTag) {
return console.debug.bind(console, `${this.label}`);
} else {
return console.debug.bind(console);
}
}
/** @type {(...data: any[]) => void} */
get warn() {
if (this.devTag) {
return console.warn.bind(console, `${this.label}`);
} else {
return console.warn.bind(console);
}
}
/**
* Maybe use later?
*
* @memberof Logger
*/
_setupFunctions() {
let self = this;
let funcs = ['log', 'info', 'error', 'debug', 'warn'];
for (let i = 0; i < funcs.length; i++) {
let func = funcs[i];
self[func] = function () {
let args = [...arguments];
if (self.devTag) args.unshift(self.label);
self.window.console.debug.bind(self.window.console, ...args);
};
}
}
}
class Base64 {
static keyStr = 'ABCDEFGHIJKLMNOP' + 'QRSTUVWXYZabcdef' + 'ghijklmnopqrstuv' + 'wxyz0123456789+/' + '=';
/**
*
*
* @static
* @param {string} input
* @returns {string}
* @memberof Base64
*/
static encode(input) {
input = escape(input);
let output = '';
let chr1;
let chr2;
let chr3 = '';
let enc1;
let enc2;
let enc3;
let enc4 = '';
let i = 0;
do {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output + Base64.keyStr.charAt(enc1) + Base64.keyStr.charAt(enc2) + Base64.keyStr.charAt(enc3) + Base64.keyStr.charAt(enc4);
chr1 = chr2 = chr3 = '';
enc1 = enc2 = enc3 = enc4 = '';
} while (i < input.length);
return output;
}
/**
*
*
* @static
* @param {string} input
* @returns {string}
* @memberof Base64
*/
static decode(input) {
let output = '';
let chr1;
let chr2;
let chr3 = '';
let enc1;
let enc2;
let enc3;
let enc4 = '';
let i = 0;
let base64test = /[^A-Za-z0-9\+\/\=]/g;
if (base64test.exec(input)) {
throw new Error(`There were invalid base64 characters in the input text. Valid base64 characters are: ['A-Z', 'a-z', '0-9,' '+', '/', '=']`);
}
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
do {
enc1 = Base64.keyStr.indexOf(input.charAt(i++));
enc2 = Base64.keyStr.indexOf(input.charAt(i++));
enc3 = Base64.keyStr.indexOf(input.charAt(i++));
enc4 = Base64.keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) output = output + String.fromCharCode(chr2);
if (enc4 != 64) output = output + String.fromCharCode(chr3);
chr1 = chr2 = chr3 = '';
enc1 = enc2 = enc3 = enc4 = '';
} while (i < input.length);
return unescape(output);
}
}
class MultiRegExp {
constructor(baseRegExp) {
const { regexp, groupIndexMapper, previousGroupsForGroup } = this._fillGroups(baseRegExp);
this.regexp = regexp;
this.groupIndexMapper = groupIndexMapper;
this.previousGroupsForGroup = previousGroupsForGroup;
}
execForAllGroups(str, includeFullMatch) {
let matches = RegExp.prototype.exec.call(this.regexp, str);
if (!matches) return matches;
let firstIndex = matches.index;
let indexMapper = includeFullMatch
? Object.assign(
{
0: 0,
},
this.groupIndexMapper
)
: this.groupIndexMapper;
let previousGroups = includeFullMatch
? Object.assign(
{
0: [],
},
this.previousGroupsForGroup
)
: this.previousGroupsForGroup;
let res = Object.keys(indexMapper).map((group) => {
let mapped = indexMapper[group];
let match = matches[mapped];
let start = firstIndex + previousGroups[group].reduce((sum, i) => sum + (matches[i] ? matches[i].length : 0), 0);
let end = start + (matches[mapped] ? matches[mapped].length : 0);
let lineColumnStart = LineColumnFinder(str).fromIndex(start);
let lineColumnEnd = LineColumnFinder(str).fromIndex(end - 1);
return {
match,
start,
end,
startLineNumber: lineColumnStart.line,
startColumnNumber: lineColumnStart.col,
endLineNumber: lineColumnEnd.line,
endColumnNumber: lineColumnEnd.col,
};
});
return res;
}
execForGroup(string, group) {
const matches = RegExp.prototype.exec.call(this.regexp, string);
if (!matches) return matches;
const firstIndex = matches.index;
const mapped = group == 0 ? 0 : this.groupIndexMapper[group];
const previousGroups = group == 0 ? [] : this.previousGroupsForGroup[group];
let r = {
match: matches[mapped],
start: firstIndex + previousGroups.reduce((sum, i) => sum + (matches[i] ? matches[i].length : 0), 0),
};
r.end = r.start + (matches[mapped] ? matches[mapped].length : 0);
return r;
}
/**
* Adds brackets before and after a part of string
* @param str string the hole regex string
* @param start int marks the position where ( should be inserted
* @param end int marks the position where ) should be inserted
* @param groupsAdded int defines the offset to the original string because of inserted brackets
* @return {string}
*/
_addGroupToRegexString(str, start, end, groupsAdded) {
start += groupsAdded * 2;
end += groupsAdded * 2;
return str.substring(0, start) + '(' + str.substring(start, end + 1) + ')' + str.substring(end + 1);
}
/**
* converts the given regex to a regex where all not captured string are going to be captured
* it along sides generates a mapper which maps the original group index to the shifted group offset and
* generates a list of groups indexes (including new generated capturing groups)
* which have been closed before a given group index (unshifted)
*
* Example:
* regexp: /a(?: )bc(def(ghi)xyz)/g => /(a(?: )bc)((def)(ghi)(xyz))/g
* groupIndexMapper: {'1': 2, '2', 4}
* previousGroupsForGroup: {'1': [1], '2': [1, 3]}
*
* @param regex RegExp
* @return {{regexp: RegExp, groupIndexMapper: {}, previousGroupsForGroup: {}}}
*/
_fillGroups(regex) {
let regexString;
let modifier;
if (regex.source && regex.flags) {
regexString = regex.source;
modifier = regex.flags;
} else {
regexString = regex.toString();
modifier = regexString.substring(regexString.lastIndexOf(regexString[0]) + 1); // sometimes order matters ;)
regexString = regexString.substr(1, regex.toString().lastIndexOf(regexString[0]) - 1);
}
// regexp is greedy so it should match (? before ( right?
// brackets may be not quoted by \
// closing bracket may look like: ), )+, )+?, ){1,}?, ){1,1111}?
const tester = /(\\\()|(\\\))|(\(\?)|(\()|(\)(?:\{\d+,?\d*}|[*+?])?\??)/g;
let modifiedRegex = regexString;
let lastGroupStartPosition = -1;
let lastGroupEndPosition = -1;
let lastNonGroupStartPosition = -1;
let lastNonGroupEndPosition = -1;
let groupsAdded = 0;
let groupCount = 0;
let matchArr;
const nonGroupPositions = [];
const groupPositions = [];
const groupNumber = [];
let currentLengthIndexes = [];
const groupIndexMapper = {};
const previousGroupsForGroup = {};
while ((matchArr = tester.exec(regexString)) !== null) {
if (matchArr[1] || matchArr[2]) {
// ignore escaped brackets \(, \)
}
if (matchArr[3]) {
// non capturing group (?
let index = matchArr.index + matchArr[0].length - 1;
lastNonGroupStartPosition = index;
nonGroupPositions.push(index);
} else if (matchArr[4]) {
// capturing group (
let index = matchArr.index + matchArr[0].length - 1;
let lastGroupPosition = Math.max(lastGroupStartPosition, lastGroupEndPosition);
// if a (? is found add ) before it
if (lastNonGroupStartPosition > lastGroupPosition) {
// check if between ) of capturing group lies a non capturing group
if (lastGroupPosition < lastNonGroupEndPosition) {
// add groups for x1 and x2 on (?:()x1)x2(?:...
if (lastNonGroupEndPosition - 1 - (lastGroupPosition + 1) > 0) {
modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupPosition + 1, lastNonGroupEndPosition - 1, groupsAdded);
groupsAdded++;
lastGroupEndPosition = lastNonGroupEndPosition - 1; // imaginary position as it is not in regex but modifiedRegex
currentLengthIndexes.push(groupCount + groupsAdded);
}
if (lastNonGroupStartPosition - 1 - (lastNonGroupEndPosition + 1) > 0) {
modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastNonGroupEndPosition + 1, lastNonGroupStartPosition - 2, groupsAdded);
groupsAdded++;
lastGroupEndPosition = lastNonGroupStartPosition - 1; // imaginary position as it is not in regex but modifiedRegex
currentLengthIndexes.push(groupCount + groupsAdded);
}
} else {
modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupPosition + 1, lastNonGroupStartPosition - 2, groupsAdded);
groupsAdded++;
lastGroupEndPosition = lastNonGroupStartPosition - 1; // imaginary position as it is not in regex but modifiedRegex
currentLengthIndexes.push(groupCount + groupsAdded);
}
// if necessary also add group between (? and opening bracket
if (index > lastNonGroupStartPosition + 2) {
modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastNonGroupStartPosition + 2, index - 1, groupsAdded);
groupsAdded++;
lastGroupEndPosition = index - 1; // imaginary position as it is not in regex but modifiedRegex
currentLengthIndexes.push(groupCount + groupsAdded);
}
} else if (lastGroupPosition < index - 1) {
modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupPosition + 1, index - 1, groupsAdded);
groupsAdded++;
lastGroupEndPosition = index - 1; // imaginary position as it is not in regex but modifiedRegex
currentLengthIndexes.push(groupCount + groupsAdded);
}
groupCount++;
lastGroupStartPosition = index;
groupPositions.push(index);
groupNumber.push(groupCount + groupsAdded);
groupIndexMapper[groupCount] = groupCount + groupsAdded;
previousGroupsForGroup[groupCount] = currentLengthIndexes.slice();
} else if (matchArr[5]) {
// closing bracket ), )+, )+?, ){1,}?, ){1,1111}?
let index = matchArr.index + matchArr[0].length - 1;
if ((groupPositions.length && !nonGroupPositions.length) || groupPositions[groupPositions.length - 1] > nonGroupPositions[nonGroupPositions.length - 1]) {
if (lastGroupStartPosition < lastGroupEndPosition && lastGroupEndPosition < index - 1) {
modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupEndPosition + 1, index - 1, groupsAdded);
groupsAdded++;
//lastGroupEndPosition = index - 1; will be set anyway
currentLengthIndexes.push(groupCount + groupsAdded);
}
groupPositions.pop();
lastGroupEndPosition = index;
let toPush = groupNumber.pop();
currentLengthIndexes.push(toPush);
currentLengthIndexes = currentLengthIndexes.filter((index) => index <= toPush);
} else if (nonGroupPositions.length) {
nonGroupPositions.pop();
lastNonGroupEndPosition = index;
}
}
}
return {
regexp: new RegExp(modifiedRegex, modifier),
groupIndexMapper,
previousGroupsForGroup,
};
}
}
class MoveableElement {
/**
* Creates an instance of MoveableElement.
* @param {HTMLElement} element
* @param {boolean} requireKeyDown
* @memberof MoveableElement
*/
constructor(element, requireKeyDown) {
this.element = element;
this.requireKeyDown = requireKeyDown || false;
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.moving = false;
this.keyPressed = false;
this.originalCursor = getStyle(this.element, 'cursor');
this.setupEvents();
}
setupEvents() {
if (!document.body) {
setTimeout(() => {
this.setupEvents();
}, 250);
} else {
document.body.addEventListener('keydown', (ev) => {
if (ev.which == '17') {
this.keyPressed = true;
}
});
document.body.addEventListener('keyup', (ev) => {
this.keyPressed = false;
});
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {MouseEvent} ev
* @memberof MoveableElement
*/
handleMouseDown(ev) {
if (this.keyPressed || !this.requireKeyDown) {
ev.preventDefault();
this.element.style.cursor = 'move';
this.changePointerEvents('none');
document.body.removeEventListener('mouseup', this.handleMouseUp);
document.body.addEventListener('mouseup', this.handleMouseUp);
document.body.removeEventListener('mousemove', this.handleMouseMove);
document.body.removeEventListener('mouseleave', this.handleMouseUp);
document.body.addEventListener('mousemove', this.handleMouseMove);
document.body.addEventListener('mouseleave', this.handleMouseUp);
try {
document.querySelectorAll('iframe')[0].style.pointerEvents = 'none';
} catch (error) {}
}
}
changePointerEvents(value) {
for (let i = 0; i < this.element.children.length; i++) {
const child = this.element.children[i];
child.style.pointerEvents = value;
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {MouseEvent} ev
* @memberof MoveableElement
*/
handleMouseUp(ev) {
this.moving = false;
this.element.style.cursor = this.originalCursor;
this.changePointerEvents('auto');
document.body.removeEventListener('mouseup', this.handleMouseUp);
document.body.removeEventListener('mousemove', this.handleMouseMove);
document.body.removeEventListener('mouseleave', this.handleMouseUp);
try {
document.querySelectorAll('iframe')[0].style.pointerEvents = '';
} catch (error) {}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {MouseEvent} ev
* @memberof MoveableElement
*/
handleMouseMove(ev) {
this.moving = true;
let top = ev.clientY - getStyle(this.element, 'height') / 2;
let bottom = ev.clientX - getStyle(this.element, 'width') / 2;
this.element.style.top = `${top}px`;
this.element.style.left = `${bottom}px`;
}
padCoord(coord) {
return coord.toString().padStart(5, ' ');
}
init() {
this.element.addEventListener('mousedown', this.handleMouseDown);
}
}
class CurrentLine {
/**
* @typedef ILineInfo
* @prop {string} method
* @prop {number} line
* @prop {string} file
* @prop {string} filename
*/
/**
* Returns a single item
*
* @param {number} [level] Useful to return levels up on the stack. If not informed, the first (0, zero index) element of the stack will be returned
* @returns {ILineInfo}
*/
get(level = 0) {
const stack = getStack();
const i = Math.min(level + 1, stack.length - 1);
const item = stack[i];
const result = CurrentLine.parse(item);
return result;
}
/**
* Returns all stack
*
* @returns {ILineInfo[]}
*/
all() {
const stack = getStack();
const result = [];
for (let i = 1; i < stack.length; i++) {
const item = stack[i];
result.push(CurrentLine.parse(item));
}
return result;
}
/**
*
*
* @param {NodeJS.CallSite} item
* @returns {ILineInfo}
*/
static parse(item) {
const result = {
method: item.getMethodName() || item.getFunctionName(),
line: item.getLineNumber(),
file: item.getFileName() || item.getScriptNameOrSourceURL(),
};
result.filename = result.file ? result.file.replace(/^.*\/|\\/gm, '').replace(/\.\w+$/gm, '') : null;
return result;
}
}
/**
*
*
* @returns {NodeJS.CallSite[]}
*/
function getStack() {
const orig = Error.prepareStackTrace;
Error.prepareStackTrace = function (_, stack) {
return stack;
};
const err = new Error();
Error.captureStackTrace(err, arguments.callee);
const stack = err.stack;
Error.prepareStackTrace = orig;
return stack;
}
class ProgressTimer {
/**
* Creates an instance of ProgressTimer.
* @param {number} total
* @memberof ProgressTimer
*/
constructor(total) {
this.startTime;
this.total = total;
this.loaded = 0;
this.estimatedFinishDt = '';
this.progressMessage = '';
}
/**
*
*
* @memberof ProgressTimer
*/
start() {
this.startTime = new Date();
}
/**
*
*
* @param {number} loaded
* @param {string} msg
* @memberof ProgressTimer
*/
updateProgress(loaded, msg) {
this.loaded = loaded;
this.progress = `${((this.loaded * 100) / this.total).toFixed(2)}%`;
this.timeRemaining = this._estimatedTimeRemaining(this.startTime, this.loaded, this.total);
this.downloaded = `${this.loaded}/${this.total}`;
this.completionTime = `${this._dateToISOLikeButLocal(this.estimatedFinishDt)}`;
this.totalRuntime = `${this._ms2Timestamp(this.timeTaken)}`;
this.updateProgressMessage(msg);
this.printProgress();
}
/**
*
*
* @param {string} msg
* @memberof ProgressTimer
*/
updateProgressMessage(msg) {
let msgLines = [];
msgLines.push(` completed: ${this.progress}`);
msgLines.push(` downloaded: ${this.downloaded}`);
msgLines.push(` total runtime: ${this.totalRuntime}`);
msgLines.push(` time remaining: ${this.timeRemaining}`);
msgLines.push(`completion time: ${this.completionTime}`);
if (msg) {
msgLines.push(msg);
}
this.progressMessage = msgLines.join('\n');
}
/**
*
*
* @memberof ProgressTimer
*/
printProgress() {
console.clear();
console.debug(this.progressMessage);
}
/**
*
*
* @param {Date} startTime
* @param {number} itemsProcessed
* @param {number} totalItems
* @returns {string}
* @memberof ProgressTimer
*/
_estimatedTimeRemaining(startTime, itemsProcessed, totalItems) {
// if (itemsProcessed == 0) {
// return '';
// }
let currentTime = new Date();
this.timeTaken = currentTime - startTime;
this.timeLeft = itemsProcessed == 0 ? this.timeTaken * (totalItems - itemsProcessed) : (this.timeTaken / itemsProcessed) * (totalItems - itemsProcessed);
this.estimatedFinishDt = new Date(currentTime.getTime() + this.timeLeft);
return this._ms2Timestamp(this.timeLeft);
}
/**
*
*
* @param {number} ms
* @returns {string}
* @memberof ProgressTimer
*/
_ms2Timestamp(ms) {
// 1- Convert to seconds:
let seconds = ms / 1000;
// 2- Extract hours:
let hours = parseInt(seconds / 3600); // 3,600 seconds in 1 hour
seconds = seconds % 3600; // seconds remaining after extracting hours
// 3- Extract minutes:
let minutes = parseInt(seconds / 60); // 60 seconds in 1 minute
// 4- Keep only seconds not extracted to minutes:
seconds = seconds % 60;
let parts = seconds.toString().split('.');
seconds = parseInt(parts[0]);
let milliseconds = parts.length > 1 ? parts[1].substring(0, 3).padEnd(3, 0) : '000';
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}.${milliseconds}`; // hours + ':' + minutes + ':' + seconds;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Date} date
* @returns {string}
* @memberof ProgressTimer
*/
_dateToISOLikeButLocal(date) {
let offsetMs = date.getTimezoneOffset() * 60 * 1000;
let msLocal = date.getTime() - offsetMs;
let dateLocal = new Date(msLocal);
let iso = dateLocal.toISOString();
let isoLocal = iso.slice(0, 19);
return isoLocal.replace(/T/g, ' ');
}
}
class Benchmark {
constructor({ logger, printResults } = {}) {
this.namedPerformances = {};
this.defaultName = 'default';
this.logger = logger;
this.printResults = printResults == undefined ? (this.logger ? true : false) : printResults;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string=} name
* @memberof Benchmark
*/
start(name) {
name = name || this.defaultName;
this.namedPerformances[name] = {
startAt: this._hrtime(),
};
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string=} name
* @memberof Benchmark
*/
stop(name) {
name = name || this.defaultName;
const startAt = this.namedPerformances[name] && this.namedPerformances[name].startAt;
if (!startAt) throw new Error(`Namespace: ${name} doesnt exist`);
const diff = this._hrtime(startAt);
const time = diff[0] * 1e3 + diff[1] * 1e-6;
const words = this.getWords(diff);
const preciseWords = this.getPreciseWords(diff);
const verboseWords = this.getVerboseWords(diff);
const verboseAbbrWords = this.getVerboseAbbrWords(diff);
if (this.printResults) {
let output = name != 'default' ? `[${name}] execution time:` : `execution time:`;
this.logger(output, time); // words
}
return {
name,
time,
words,
preciseWords,
verboseWords,
verboseAbbrWords,
diff,
};
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {T} func
* @param {{name: string, measure: boolean}=} { name, measure }
* @returns {T}
* @memberof Benchmark
* @template T
*/
wrapFunc(func, { name, measure = true } = {}) {
name = this._getFuncName(func, name);
let self = this;
wrappedFunc.measure = measure;
wrappedFunc.benchmark = {
name,
results: {
runs: [],
avg: null,
min: null,
max: null,
total: null,
times: null,
runCount: 0,
},
reset: function () {
this.results.runs = [];
this.results.avg = null;
this.results.min = null;
this.results.max = null;
this.results.total = null;
this.results.times = null;
this.results.runCount = 0;
},
printResults: function (logger = console.debug) {
let output = this.name != 'default' ? `[${this.name}] execution summary:` : `execution summary:`;
let times = wrappedFunc.benchmark.results.runs.map((run) => {
return run.time;
});
wrappedFunc.benchmark.results.times = times;
wrappedFunc.benchmark.results.avg = self._getAvgTime(times);
wrappedFunc.benchmark.results.total = self._getSumTime(times);
wrappedFunc.benchmark.results.min = Math.min(...times);
wrappedFunc.benchmark.results.max = Math.max(...times);
logger(output, `times: ${this.results.runCount} total: ${self.getWords(this.results.total)} min: ${self.getWords(this.results.min)} max: ${self.getWords(this.results.max)} avg: ${self.getWords(this.results.avg)} total: ${self.getWords(this.results.total)}`);
},
};
function wrappedFunc() {
if (wrappedFunc.measure) {
self.start(name);
}
let res = func(...arguments);
if (wrappedFunc.measure) {
wrappedFunc.benchmark.results.runCount++;
wrappedFunc.benchmark.results.runs.push(self.stop(name));
}
return res;
}
return this._defineWrappedFuncProperties(wrappedFunc, name);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {T} func
* @param {{name: string, measure: boolean}=} { name, measure }
* @returns {T}
* @memberof Benchmark
* @template T
*/
wrapAsyncFunc(func, { name, measure = true } = {}) {
name = this._getFuncName(func, name);
let self = this;
wrappedFunc.measure = measure;
wrappedFunc.benchmark = {
name,
results: {
runs: [],
avg: null,
min: null,
max: null,
total: null,
times: null,
runCount: 0,
},
reset: function () {
this.results.runs = [];
this.results.avg = null;
this.results.min = null;
this.results.max = null;
this.results.total = null;
this.results.times = null;
this.results.runCount = 0;
},
printResults: function (logger = console.debug) {
let output = this.name != 'default' ? `[${this.name}] execution summary:` : `execution summary:`;
let times = wrappedFunc.benchmark.results.runs.map((run) => {
return run.time;
});
wrappedFunc.benchmark.results.times = times;
wrappedFunc.benchmark.results.avg = self._getAvgTime(times);
wrappedFunc.benchmark.results.total = self._getSumTime(times);
wrappedFunc.benchmark.results.min = Math.min(...times);
wrappedFunc.benchmark.results.max = Math.max(...times);
logger(output, `times: ${this.results.runCount} total: ${self.getWords(this.results.total)} min: ${self.getWords(this.results.min)} max: ${self.getWords(this.results.max)} avg: ${self.getWords(this.results.avg)} total: ${self.getWords(this.results.total)}`);
},
};
async function wrappedFunc() {
if (wrappedFunc.measure) {
self.start(name);
}
let res = await func(...arguments);
if (wrappedFunc.measure) {
wrappedFunc.benchmark.results.runCount++;
wrappedFunc.benchmark.results.runs.push(self.stop(name));
}
return res;
}
return this._defineWrappedFuncProperties(wrappedFunc, name);
}
getWords(diff) {
let ms = typeof diff === 'number' ? diff : this._hrtime2Ms(diff);
return this._msToHms(ms);
// return this._prettyHrtime(diff);
}
getPreciseWords(diff) {
return this._prettyHrtime(diff, { precise: true });
}
getVerboseWords(diff) {
return this._prettyHrtime(diff, { verbose: true });
}
getVerboseAbbrWords(diff) {
return this._prettyHrtime(diff, { verbose: true, verboseAbbrv: true, precise: true });
}
_msToHms(ms) {
// Extract days
let days = Math.floor(ms / (86400 * 1000));
ms %= (86400 * 1000);
// Extract hours
let hours = Math.floor(ms / (3600 * 1000));
ms %= (3600 * 1000);
// Extract minutes
let minutes = Math.floor(ms / (60 * 1000));
ms = ms % (60 * 1000);
// Extract seconds
let seconds = Math.floor(ms / 1000);
ms = ms % 1000;
let result = [];
if (days > 0) {
result.push(`${days} day${days === 1 ? '' : 's'}`);
}
if (hours > 0) {
result.push(`${hours} hr${hours === 1 ? '' : 's'}`);
}
if (minutes > 0) {
result.push(`${minutes} min${minutes === 1 ? '' : 's'}`);
}
if (seconds > 0) {
result.push(`${seconds} sec${seconds === 1 ? '' : 's'}`);
}
if (ms > 0) {
result.push(`${ms} ms`);
}
return result.join(', ');
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number[][]} times
* @returns {number}
*/
_getAvgTime(times) {
return this._getSumTime(times) / times.length;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number[][]} times
* @returns {number}
*/
_getSumTime(times) {
return times.reduce((a, b) => a + b);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} ms
* @returns {number[][]}
* @memberof Benchmark
*/
_ms2Hrtime(ms) {
let seconds = Math.round(ms / 1000);
let nanoSeconds = Math.round(ms * 1000000 - seconds * 1000000 * 1000);
return [seconds, nanoSeconds];
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {[number, number]} hrtime
* @returns {number}
* @memberof Benchmark
*/
_hrtime2Ms(hrtime) {
return hrtime[0] * 1000 + hrtime[1] / 1000000;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {T} func
* @param {string=} name
* @returns {T}
* @memberof Benchmark
* @template T
*/
_getFuncName(func, name) {
return name ? name : 'name' in func && func.name.trim() !== '' ? func.name : '[wrapped.func]';
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Function} wrappedFunc
* @param {string} name
* @returns {Function}
* @memberof Benchmark
*/
_defineWrappedFuncProperties(wrappedFunc, name) {
Object.defineProperty(wrappedFunc, 'name', {
value: name,
writable: false,
configurable: false,
enumerable: false,
});
Object.defineProperty(wrappedFunc, 'toString', {
value: () => func.toString(),
writable: false,
configurable: false,
enumerable: false,
});
return wrappedFunc;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {[number, number]=} time
* @returns {[number, number]}
* @memberof Benchmark
*/
_hrtime(time) {
if (typeof process !== 'undefined') return process.hrtime(time);
var performance = typeof performance !== 'undefined' ? performance : {};
let performanceNow = performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow || (() => new Date().getTime());
let clocktime = performanceNow.call(performance) * 1e-3;
let seconds = Math.floor(clocktime);
let nanoseconds = Math.floor((clocktime % 1) * 1e9);
if (time) {
seconds = seconds - time[0];
nanoseconds = nanoseconds - time[1];
if (nanoseconds < 0) {
seconds--;
nanoseconds += 1e9;
}
}
return [seconds, nanoseconds];
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {[number, number]=} time
* @param {{verbose: boolean; verboseAbbrv: boolean; precise: boolean}} { verbose = false, verboseAbbrv = false, precise = false }
* @returns {string}
* @memberof Benchmark
*/
_prettyHrtime(time, { verbose = false, verboseAbbrv = false, precise = false } = {}) {
let i, spot, sourceAtStep, valAtStep, decimals, strAtStep, results, totalSeconds;
let minimalDesc = ['h', 'min', 's', 'ms', 'μs', 'ns'];
let verboseDesc = !verboseAbbrv ? ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] : minimalDesc;
let convert = [60 * 60, 60, 1, 1e6, 1e3, 1];
if (typeof time === 'number') {
time = this._ms2Hrtime(time);
}
if (!Array.isArray(time) || time.length !== 2) return '';
if (typeof time[0] !== 'number' || typeof time[1] !== 'number') return '';
// normalize source array due to changes in node v5.4+
if (time[1] < 0) {
totalSeconds = time[0] + time[1] / 1e9;
time[0] = parseInt(totalSeconds);
time[1] = parseFloat((totalSeconds % 1).toPrecision(9)) * 1e9;
}
results = '';
for (i = 0; i < 6; i++) {
// grabbing first or second spot in source array
spot = i < 3 ? 0 : 1;
sourceAtStep = time[spot];
if (i !== 3 && i !== 0) {
// trim off previous portions
sourceAtStep = sourceAtStep % convert[i - 1];
}
if (i === 2) {
// get partial seconds from other portion of the array
sourceAtStep += time[1] / 1e9;
}
// val at this unit
valAtStep = sourceAtStep / convert[i];
if (valAtStep >= 1) {
if (verbose) {
// deal in whole units, subsequent laps will get the decimal portion
valAtStep = Math.floor(valAtStep);
}
if (!precise) {
// don't fling too many decimals
decimals = valAtStep >= 10 ? 0 : 2;
strAtStep = valAtStep.toFixed(decimals);
} else {
strAtStep = valAtStep.toString();
}
if (strAtStep.indexOf('.') > -1 && strAtStep[strAtStep.length - 1] === '0') {
// remove trailing zeros
strAtStep = strAtStep.replace(/\.?0+$/, '');
}
if (results) {
// append space if we have a previous value
results += ' ';
}
// append the value
results += strAtStep;
// append units
if (verbose) {
results += verboseAbbrv ? `${verboseDesc[i]}` : ` ${verboseDesc[i]}`;
if (!verboseAbbrv && strAtStep !== '1') {
results += 's';
}
} else {
results += ` ${minimalDesc[i]}`;
}
if (!verbose) {
// verbose gets as many groups as necessary, the rest get only one
break;
}
}
}
return results;
}
}
class ArrayStat {
/**
* Creates an instance of ArrayStat.
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number[]} array
* @memberof ArrayStat
*/
constructor(array) {
this.array = array;
}
_getCloned() {
return this.array.slice(0);
}
min() {
return Math.min.apply(null, this.array);
}
max() {
return Math.max.apply(null, this.array);
}
range() {
return this.max(this.array) - this.min(this.array);
}
midrange() {
return this.range(this.array) / 2;
}
sum(array) {
array = array || this.array;
let total = 0;
for (let i = 0, l = array.length; i < l; i++) total += array[i];
return total;
}
mean(array) {
array = array || this.array;
return this.sum(array) / array.length;
}
median() {
let array = this._getCloned();
array.sort(function (a, b) {
return a - b;
});
let mid = array.length / 2;
return mid % 1 ? array[mid - 0.5] : (array[mid - 1] + array[mid]) / 2;
}
modes() {
if (!this.array.length) return [];
let modeMap = {};
let maxCount = 0;
let modes = [];
this.array.forEach(function (val) {
if (!modeMap[val]) modeMap[val] = 1;
else modeMap[val]++;
if (modeMap[val] > maxCount) {
modes = [val];
maxCount = modeMap[val];
} else if (modeMap[val] === maxCount) {
modes.push(val);
maxCount = modeMap[val];
}
});
return modes;
}
letiance() {
let mean = this.mean();
return this.mean(
this._getCloned().map(function (num) {
return Math.pow(num - mean, 2);
})
);
}
standardDeviation() {
return Math.sqrt(this.letiance());
}
meanAbsoluteDeviation() {
let mean = this.mean();
return this.mean(
this._getCloned().map(function (num) {
return Math.abs(num - mean);
})
);
}
zScores() {
let mean = this.mean();
let standardDeviation = this.standardDeviation();
return this._getCloned().map(function (num) {
return (num - mean) / standardDeviation;
});
}
withinStd(val, stdev) {
let low = this.mean() - stdev * this.standardDeviation(); // x.deviation;
let hi = this.mean() + stdev * this.standardDeviation(); // x.deviation;
let res = val > low && val < hi;
console.log(`val: ${val.toString().padEnd(5, ' ')} mean: ${this.mean()} stdev: ${this.standardDeviation()} hi: ${hi} low: ${low} res: ${res}`);
return res;
}
}
memoizeClass(ArrayStat);
let LineColumnFinder = (function LineColumnFinder() {
let isArray = Array.isArray;
let isObject = (val) => val != null && typeof val === 'object' && Array.isArray(val) === false;
let slice = Array.prototype.slice;
/**
* Finder for index and line-column from given string.
*
* You can call this without `new` operator as it returns an instance anyway.
*
* @class
* @param {string} str - A string to be parsed.
* @param {Object|number} [options] - Options.
* This can be an index in the string for shorthand of `lineColumn(str, index)`.
* @param {number} [options.origin=1] - The origin value of line and column.
*/
function LineColumnFinder(str, options) {
if (!(this instanceof LineColumnFinder)) {
if (typeof options === 'number') {
return new LineColumnFinder(str).fromIndex(options);
}
return new LineColumnFinder(str, options);
}
this.str = str || '';
this.lineToIndex = buildLineToIndex(this.str);
options = options || {};
this.origin = typeof options.origin === 'undefined' ? 1 : options.origin;
}
/**
* Find line and column from index in the string.
*
* @param {number} index - Index in the string. (0-origin)
* @return {Object|null}
* Found line number and column number in object `{ line: X, col: Y }`.
* If the given index is out of range, it returns `null`.
*/
LineColumnFinder.prototype.fromIndex = function (index) {
if (index < 0 || index >= this.str.length || isNaN(index)) {
return null;
}
let line = findLowerIndexInRangeArray(index, this.lineToIndex);
return {
line: line + this.origin,
col: index - this.lineToIndex[line] + this.origin,
};
};
/**
* Find index from line and column in the string.
*
* @param {number|Object|Array} line - Line number in the string.
* This can be an Object of `{ line: X, col: Y }`, or
* an Array of `[line, col]`.
* @param {number} [column] - Column number in the string.
* This must be omitted or undefined when Object or Array is given
* to the first argument.
* @return {number}
* Found index in the string. (always 0-origin)
* If the given line or column is out of range, it returns `-1`.
*/
LineColumnFinder.prototype.toIndex = function (line, column) {
if (typeof column === 'undefined') {
if (isArray(line) && line.length >= 2) {
return this.toIndex(line[0], line[1]);
}
if (isObject(line) && 'line' in line && ('col' in line || 'column' in line)) {
return this.toIndex(line.line, 'col' in line ? line.col : line.column);
}
return -1;
}
if (isNaN(line) || isNaN(column)) {
return -1;
}
line -= this.origin;
column -= this.origin;
if (line >= 0 && column >= 0 && line < this.lineToIndex.length) {
let lineIndex = this.lineToIndex[line];
let nextIndex = line === this.lineToIndex.length - 1 ? this.str.length : this.lineToIndex[line + 1];
if (column < nextIndex - lineIndex) {
return lineIndex + column;
}
}
return -1;
};
/**
* Build an array of indexes of each line from a string.
*
* @private
* @param str {string} An input string.
* @return {number[]} Built array of indexes. The key is line number.
*/
function buildLineToIndex(str) {
let lines = str.split('\n');
let lineToIndex = new Array(lines.length);
let index = 0;
for (let i = 0, l = lines.length; i < l; i++) {
lineToIndex[i] = index;
index += lines[i].length + /* "\n".length */ 1;
}
return lineToIndex;
}
/**
* Find a lower-bound index of a value in a sorted array of ranges.
*
* Assume `arr = [0, 5, 10, 15, 20]` and
* this returns `1` for `value = 7` (5 <= value < 10),
* and returns `3` for `value = 18` (15 <= value < 20).
*
* @private
* @param arr {number[]} An array of values representing ranges.
* @param value {number} A value to be searched.
* @return {number} Found index. If not found `-1`.
*/
function findLowerIndexInRangeArray(value, arr) {
if (value >= arr[arr.length - 1]) {
return arr.length - 1;
}
let min = 0,
max = arr.length - 2,
mid;
while (min < max) {
mid = min + ((max - min) >> 1);
if (value < arr[mid]) {
max = mid - 1;
} else if (value >= arr[mid + 1]) {
min = mid + 1;
} else {
// value >= arr[mid] && value < arr[mid + 1]
min = mid;
break;
}
}
return min;
}
return LineColumnFinder;
})();
class CustomContextMenu {
/**
* Example menuItems
*
* ```javascript
* let menuItems = [
* {
* type: 'item',
* label: 'Test1',
* onClick: () => {
* alert('test1');
* },
* },
* {
* type: 'item',
* label: 'Test2',
* onClick: () => {
* console.debug('test2');
* },
* },
* {
* type: 'break',
* },
* {
* type: 'item',
* label: 'Test3',
* onClick: () => {
* console.debug('test3');
* },
* },
* ];
* ```
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} elemToAttachTo
* @param {*} menuItems
* @memberof CustomContextMenu
*/
constructor(elemToAttachTo, menuItems, onContextMenu) {
this.elem = elemToAttachTo;
this.menuItems = menuItems;
this.menu = null;
this.onContextMenu = onContextMenu;
this._createMenu();
this._setupEvents();
this.hide = debounce(this.hide.bind(this), 500, true);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} top
* @param {number} left
* @memberof CustomContextMenu
*/
show(top, left) {
document.body.appendChild(this.menu);
this.menu.style.display = 'block';
this.menu.style.top = `${top}px`;
this.menu.style.left = `${left}px`;
this.menu.setAttribute('tabindex', '0');
this.menu.focus();
}
hide() {
this.menu.style.display = 'none';
if (document.body.contains(this.menu)) {
this.menu.remove();
}
}
_setupEvents() {
this.elem.addEventListener('contextmenu', (ev) => {
ev.preventDefault();
if (this.onContextMenu) {
this.onContextMenu(ev);
}
this.show(ev.pageY, ev.pageX);
});
document.addEventListener('click', (ev) => {
if (document.body.contains(this.menu) && !this._isHover(this.menu)) {
this.hide();
}
});
window.addEventListener('blur', (ev) => {
this.hide();
});
this.menu.addEventListener('blur', (ev) => {
this.hide();
});
}
_createMenu() {
this.menu = this._createMenuContainer();
for (let i = 0; i < this.menuItems.length; i++) {
let itemConfig = this.menuItems[i];
switch (itemConfig.type) {
case 'item':
this.menu.appendChild(this._createItem(itemConfig));
break;
case 'break':
this.menu.appendChild(this._createBreak());
break;
default:
break;
}
}
// document.body.appendChild(this.menu);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {HTMLElement}
* @memberof CustomContextMenu
*/
_createMenuContainer() {
let html = `<div class="context" hidden></div>`;
let elem = this._createElementsFromHTML(html);
return elem;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {*} itemConfig
* @returns {HTMLElement}
* @memberof CustomContextMenu
*/
_createItem(itemConfig) {
let html = `<div class="context_item">
<div class="inner_item">
${itemConfig.label}
</div>
</div>`;
let elem = this._createElementsFromHTML(html);
if (itemConfig.id) {
elem.id = itemConfig.id;
}
if (itemConfig.onClick) {
elem.addEventListener('click', (ev) => {
itemConfig.onClick(ev);
this.hide();
});
}
return elem;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {HTMLElement}
* @memberof CustomContextMenu
*/
_createBreak() {
let html = `<div class="context_hr"></div>`;
let elem = this._createElementsFromHTML(html);
return elem;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} htmlStr
* @returns {HTMLElement}
*/
_createElementsFromHTML(htmlStr) {
let div = document.createElement('div');
div.innerHTML = htmlStr.trim();
return div.firstChild;
}
_isHover(elem) {
return elem.parentElement.querySelector(':hover') === elem;
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @class LocalStorageEx
*/
class LocalStorageEx {
/**
* Creates an instance of LocalStorageEx.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @memberof LocalStorageEx
*/
constructor() {
this.__storage = localStorage;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @readonly
* @memberof LocalStorageEx
*/
get UNDEFINED_SAVED_VALUE() {
return '__** undefined **__';
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @readonly
* @memberof LocalStorageEx
*/
get size() {
let total = 0;
for (let x in this.__storage) {
// Value is multiplied by 2 due to data being stored in `utf-16` format, which requires twice the space.
let amount = this.__storage[x].length * 2;
if (!isNaN(amount) && this.__storage.hasOwnProperty(x)) {
total += amount;
}
}
return total;
}
/**
* Determine if browser supports local storage.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {boolean}
* @memberof LocalStorageEx
*/
isSupported() {
return typeof Storage !== 'undefined';
}
/**
* Check if key exists in local storage.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {*} key
* @returns {boolean}
* @memberof LocalStorageEx
*/
has(key) {
if (typeof key === 'object') {
key = JSON.stringify(key);
}
return this.__storage.hasOwnProperty(key);
}
/**
* Retrieve an object from local storage.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {*} key
* @param {*} [defaultValue=null]
* @returns {*}
* @memberof LocalStorageEx
*/
get(key, defaultValue = null) {
if (typeof key === 'object') {
key = JSON.stringify(key);
}
if (!this.has(key)) {
return defaultValue;
}
let item = this.__storage.getItem(key);
try {
if (item === '__** undefined **__') {
return undefined;
} else {
return JSON.parse(item);
}
} catch (error) {
return item;
}
}
/**
* Save some value to local storage.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} key
* @param {*} value
* @returns {void}
* @memberof LocalStorageEx
*/
set(key, value) {
if (typeof key === 'object') {
key = JSON.stringify(key);
}
if (value === undefined) {
value = this.UNDEFINED_SAVED_VALUE;
} else if (typeof value === 'object') {
value = JSON.stringify(value);
}
this.__storage.setItem(key, value);
}
/**
* Remove element from local storage.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {*} key
* @returns {void}
* @memberof LocalStorageEx
*/
remove(key) {
if (typeof key === 'object') {
key = JSON.stringify(key);
}
this.__storage.removeItem(key);
}
toString() {
return JSON.parse(JSON.stringify(this.__storage));
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @class SessionStorageEx
* @extends {LocalStorageEx}
*/
class SessionStorageEx extends LocalStorageEx {
/**
* Creates an instance of SessionStorageEx.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @memberof SessionStorageEx
*/
constructor() {
super();
this.__storage = sessionStorage;
}
}
class IgnoreCaseMap extends Map {
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} key
* @param {*} value
* @returns {this}
* @memberof IgnoreCaseMap
*/
set(key, value) {
return super.set(key.toLocaleLowerCase(), value);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} key
* @returns {*}
* @memberof IgnoreCaseMap
*/
get(key) {
return super.get(key.toLocaleLowerCase());
}
}
// #endregion Helper Classes
// #region Helper Functions
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} name
* @param {{logLevel: log.LogLevelDesc, tag: string}} logLevel
* @return {log.Logger}
*/
function getLogger(name, { logLevel, tag }) {
prefix.reg(log);
const colors = {
TRACE: '220;86;220',
DEBUG: '86;86;220',
INFO: '134;134;221',
WARN: '220;220;86',
ERROR: '220;86;86',
};
/** @type {prefix.LoglevelPluginPrefixOptions} */
let options = {
// template: tag ? `[%t] %l [${tag}] %n:` : '[%t] %l %n:',
levelFormatter: function (level) {
return level.toUpperCase();
},
nameFormatter: function (name) {
return name || 'root';
},
timestampFormatter: function (date) {
return date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
},
format: function (level, name, timestamp) {
let _timestamp = `\x1B[90m[${timestamp}]\x1B[m`;
let _level = `\x1B[38;2;${colors[level.toUpperCase()]}m${level.toUpperCase()}\x1B[m`;
let _name = `\x1B[38;2;38;177;38m${tag ? `[${tag}-` : '['}${name}]\x1B[m`;
let _format = `${_timestamp} ${_level} ${_name}:`;
return _format;
},
};
const logger = log.getLogger(name);
prefix.apply(logger, options);
logger.setLevel(logLevel || 'WARN');
return logger;
}
function pp(obj, fn) {
fn = fn || console.log;
fn(pformat(obj));
}
function pformat(obj, space = 4) {
return JSON.stringify(obj, null, space);
}
function removeAllButLastStrPattern(string, token) {
let parts = string.split(token);
if (parts[1] === undefined) return string;
else return parts.slice(0, -1).join('') + token + parts.slice(-1);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Array.<T> | Array} arr
* @param {?function(T, T): boolean} callbackObjs
* @return {T[]}
* @template T
*/
function dedupeArr(arr, callbackObjs) {
if (callbackObjs) {
let tempArr = /** @type {[]} */ (arr).filter((value, index) => {
return (
index ===
arr.findIndex((other) => {
return callbackObjs(value, other);
})
);
});
return tempArr;
} else {
return [...new Set(arr)];
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {any} obj
* @returns {boolean}
*/
function isClass(obj) {
return typeof obj === 'function' && /^\s*class\s+/.test(obj.toString());
}
/**
* Checks whether a variable is a class or an instance created with `new`.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {*} value The variable to check.
* @returns {boolean} `true` if the variable is a class or an instance created with `new`, `false` otherwise.
*/
function isClassOrInstance(value) {
// prettier-ignore
if (typeof value === 'function' &&
value.prototype &&
typeof value.prototype.constructor === 'function' &&
value.prototype.constructor !== Array &&
value.prototype.constructor !== Object) {
return true; // It's a class
} else if (typeof value === 'object' &&
value.constructor &&
typeof value.constructor === 'function' &&
value.constructor.prototype &&
typeof value.constructor.prototype.constructor === 'function' &&
value.constructor !== Array &&
value.constructor !== Object) {
return true; // It's an instance created with new
}
return false;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {*} value The variable to check.
* @returns {boolean}
*/
function isFunction(value) {
try {
return typeof value == 'function';
} catch (error) {
return false;
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {object} obj
* @param {{ propsToExclude?: string[]; namesOnly: boolean; removeDuplicates: boolean; asObject: boolean }} [{ propsToExclude = [], namesOnly = false, removeDuplicates = true, asObject = false } = {}]
* @returns
*/
function getObjProps(obj, { propsToExclude = [], namesOnly = false, removeDuplicates = true, asObject = false } = {}) {
// Default
let _propsToExclude = [
//
'__defineGetter__',
'__defineSetter__',
'__lookupSetter__',
'__lookupGetter__',
'__proto__',
'__original__',
'caller',
'callee',
'arguments',
'toString',
'valueOf',
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
];
_propsToExclude = propsToExclude && Array.isArray(propsToExclude) ? _propsToExclude.concat(propsToExclude) : _propsToExclude;
let objHierarchy = getObjHierarchy(obj);
let propNames = getPropNames(objHierarchy);
let plainObj = {};
let objKeys = [];
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {any} obj
* @returns {Array<any>}
*/
function getObjHierarchy(obj) {
let objs = [obj];
obj = isClassOrInstance(obj) ? obj.prototype || obj.__proto__ : obj;
do {
objs.push(obj);
} while ((obj = Object.getPrototypeOf(obj)));
return objs;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Array<any>} objHierarchy
* @returns {string[]}
*/
function getPropNames(objHierarchy) {
/** @type {string[]} */
let propNames = [];
for (let i = 0; i < objHierarchy.length; i++) {
const _obj = objHierarchy[i];
let getPropFuncs = [Object.getOwnPropertyNames, Object.getOwnPropertySymbols];
getPropFuncs.forEach((func) => {
let _propNames = func(_obj);
_propNames.forEach((propName) => {
if (!_propsToExclude.includes(propName) && !propNames.includes(propName)) {
propNames.push(propName);
}
});
});
}
return propNames;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{ name: string, value: any }[]} props
* @return {{ name: string, value: any }[]}
*/
function dedupeProps(props) {
function findNonNullProp(props, name) {
let res = props.find((prop) => prop.name == name && prop.value != null);
if (!res) {
res = props.find((prop) => prop.name == name);
}
return res;
}
function propsContains(props, name) {
return props.some((prop) => prop.name == name);
}
let newProps = [];
for (let i = 0; i < props.length; i++) {
const prop = props[i];
let tempProp = findNonNullProp(props, prop.name);
if (!propsContains(newProps, tempProp.name)) {
newProps.push(tempProp);
}
}
return newProps;
}
function getProps(objHierarchy, doFuncs = false) {
/** @type {{ name: string, value: any }} */
let props = [];
for (let o = 0; o < objHierarchy.length; o++) {
const _obj = objHierarchy[o];
for (let p = 0; p < propNames.length; p++) {
const propName = propNames[p];
let value;
try {
value = _obj[propName];
} catch (error) {}
if (!_propsToExclude.includes(propName)) {
if (asObject) {
if (!objKeys.includes(propName)) {
objKeys.push(propName);
plainObj[propName] = value;
}
} else {
props.push({
name: propName,
value: value,
});
}
}
}
}
if (!asObject) {
if (removeDuplicates) {
props = dedupeProps(props);
}
props = props.filter(function (prop, i, props) {
let exprs = [
//
!_propsToExclude.includes(prop.name),
// props[i + 1] && prop.name != props[i + 1].name,
...(doFuncs ? [isFunction(prop.value)] : [!isFunction(prop.value)]),
];
return exprs.every(Boolean);
});
}
if (asObject) {
return plainObj;
} else {
return props.sort(function (a, b) {
let aName = typeof a.name == 'symbol' ? a.name.toString() : a.name;
let bName = typeof b.name == 'symbol' ? b.name.toString() : b.name;
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
});
}
}
let res;
if (asObject) {
getProps(objHierarchy, true);
getProps(objHierarchy);
res = plainObj;
} else {
res = {
funcs: getProps(objHierarchy, true),
props: getProps(objHierarchy),
};
if (namesOnly) {
res.funcs = res.funcs.filter((func) => func.name.toString() != 'Symbol(Symbol.hasInstance)').map((func) => func.name);
res.props = res.props.filter((prop) => prop.name.toString() != 'Symbol(Symbol.hasInstance)').map((prop) => prop.name);
}
}
objHierarchy = null;
return res;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Window} [_window=window]
* @param {{ namesOnly: boolean; asObject: boolean }} [{ namesOnly = false, asObject = false } = {}]
* @returns
*/
function getUserDefinedGlobalProps(_window = null, { namesOnly = false, asObject = false } = {}) {
_window = _window || getWindow();
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
let plainObj = {};
let objKeys = [];
function getProps(obj, doFuncs = false) {
let props = [];
let _obj = obj;
let getPropFuncs = [Object.getOwnPropertyNames, Object.getOwnPropertySymbols];
getPropFuncs.forEach((func) => {
let propNames = func(_obj);
for (let i = 0; i < propNames.length; i++) {
const propName = propNames[i];
let value;
try {
value = _obj[propName];
} catch (error) {}
if (isNumber(propName) && value?.constructor?.name == 'Window') continue;
if (!iframe.contentWindow.hasOwnProperty(propName)) {
if (asObject) {
if (!objKeys.includes(propName)) {
objKeys.push(propName);
plainObj[propName] = value;
}
} else {
props.push({
name: propName,
value: value,
});
}
}
}
});
if (!asObject) {
props = props.filter(function (prop, i, props) {
let propName1 = prop.name;
let propName2 = props[i + 1] ? props[i + 1].name : undefined;
let propValue1 = prop.value;
let propValue2 = props[i + 1] ? props[i + 1].value : undefined;
let exprs = [
//
// props[i + 1] && propName1 != propName2,
(props[i + 1] && propName1.constructor.name == 'Symbol' && propName2.constructor.name == 'Symbol' && propValue1 != propValue2) || propName1 != propName2,
...(doFuncs ? [isFunction(obj[propName1])] : [!isFunction(obj[propName1])]),
];
return exprs.every(Boolean);
});
}
if (asObject) {
return plainObj;
} else {
return props.sort(function (a, b) {
let aName = typeof a.name == 'symbol' ? a.name.toString() : a.name;
let bName = typeof b.name == 'symbol' ? b.name.toString() : b.name;
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
});
}
}
let res;
if (asObject) {
getProps(_window, true);
getProps(_window);
res = plainObj;
} else {
res = {
funcs: getProps(_window, true),
props: getProps(_window),
};
if (namesOnly) {
res.funcs = res.funcs.filter((func) => func.name.toString() != 'Symbol(Symbol.hasInstance)').map((func) => func.name);
res.props = res.props.filter((prop) => prop.name.toString() != 'Symbol(Symbol.hasInstance)').map((prop) => prop.name);
}
}
document.body.removeChild(iframe);
return res;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {T} obj
* @param {T | boolean} thisArg
* @returns {T}
* @template T
*/
function storeObjOriginalFuncs(obj, thisArg = true) {
let props = getObjProps(obj);
obj.__original__ = {};
for (let i = 0; i < props.funcs.length; i++) {
const func = props.funcs[i];
if (thisArg == true) {
obj.__original__[func.name] = func.value.bind(obj);
} else {
obj.__original__[func.name] = thisArg != false && thisArg != null && thisArg != undefined ? func.value.bind(thisArg) : func.value;
}
}
return obj;
}
function printProps(obj, title) {
let headerFooterBanner = '*********************************************************';
console.log(headerFooterBanner);
console.log(`* ${title || ''}`);
console.log(headerFooterBanner);
for (let key in obj) console.log(key + ': ', [obj[key]]);
console.log(headerFooterBanner);
}
function sortObject(o, desc) {
let sorted = {};
let key;
let a = [];
for (key in o) {
if (o.hasOwnProperty(key)) a.push(key);
}
if (desc) a.sort(sortDescending);
else a.sort(sortAscending);
for (key = 0; key < a.length; key++) sorted[a[key]] = o[a[key]];
return sorted;
}
function sortAscending(a, b) {
if (typeof a == 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (a < b) return -1;
else if (a > b) return 1;
else return 0;
}
function sortDescending(a, b) {
if (typeof a == 'string') {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (a > b) return -1;
else if (a < b) return 1;
else return 0;
}
function getFileExtension(sFile) {
return sFile.replace(/^(.*)(\.[^/.]+)$/, '$2');
}
/**
* Async wait function.
* Example:
* (async () => {
* await wait(4000).then(() => {
* console.log(new Date().toLocaleTimeString());
* }).then(() => {
* console.log('here');
* });
* })();
*
* @param {number} ms - Milliseconds to wait.
* @param {boolean} [synchronous=false] - Wait synchronously.
*/
async function wait(ms, synchronous = false) {
let _wait = (ms, synchronous) => {
if (synchronous) {
let start = Date.now();
let now = start;
while (now - start < ms) now = Date.now();
} else {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
await _wait(ms, synchronous);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {() => bool} condition
* @param {{ timeout?: number; callback: () => T; conditionIsAsync: boolean; }} [{ timeout, callback, conditionIsAsync = false } = {}]
* @returns {T}
* @template T
*/
async function waitUntil(condition, { timeout, callback, conditionIsAsync = false } = {}) {
timeout = timeout || -1;
let maxTime = timeout == -1 ? 20000 : -1;
let startTime = new Date();
let timeRanOut = false;
let done = (() => {
let deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
})();
/** @type {number} */
let timeoutId;
if (timeout && timeout > 0) {
timeoutId = setTimeout(() => {
timeRanOut = true;
return done.reject();
}, timeout);
}
let loop = async () => {
let endTime = new Date();
let elapsed = endTime - startTime;
let conditionResult = conditionIsAsync ? await condition() : condition();
if (conditionResult || timeRanOut || (maxTime != -1 && elapsed > maxTime)) {
clearTimeout(timeoutId);
return done.resolve(callback ? await callback() : undefined);
}
setTimeout(loop, 0);
};
setTimeout(loop, 0);
return done.promise;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {any} obj
* @param {boolean} [getInherited=false]
* @returns {string}
*/
function getType(obj, getInherited = false) {
let _typeVar = (function (global) {
let cache = {};
return function (obj) {
let key;
// null
if (obj == null) return 'null';
// window/global
if (obj == global) return 'global';
// basic: string, boolean, number, undefined
if (!['object', 'function'].includes((key = typeof obj))) return key;
if (obj.constructor != undefined && obj.constructor.name != 'Object' && !getInherited) return obj.constructor.name;
// cached. date, regexp, error, object, array, math
// and get XXXX from [object XXXX], and cache it
return cache[(key = {}.toString.call(obj))] || (cache[key] = key.slice(8, -1));
};
})(globalThis);
return _typeVar(obj);
}
/**
* 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. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*
* @param {function} func
* @param {Number} wait
* @param {Boolean} immediate
* @returns
*/
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this,
args = arguments;
let later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
let callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
function equals(x, y) {
if (x === y) return true;
// if both x and y are null or undefined and exactly the same
if (!(x instanceof Object) || !(y instanceof Object)) return false;
// if they are not strictly equal, they both need to be Objects
if (x.constructor !== y.constructor) return false;
// they must have the exact same prototype chain, the closest we can do is
// test there constructor.
for (let p in x) {
if (!x.hasOwnProperty(p)) continue;
// other properties were tested using x.constructor === y.constructor
if (!y.hasOwnProperty(p)) return false;
// allows to compare x[ p ] and y[ p ] when set to undefined
if (x[p] === y[p]) continue;
// if they have the same strict value or identity then they are equal
if (typeof x[p] !== 'object') return false;
// Numbers, Strings, Functions, Booleans must be strictly equal
if (!equals(x[p], y[p])) return false;
// Objects and Arrays must be tested recursively
}
for (p in y) {
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) return false;
// allows x[ p ] to be set to undefined
}
return true;
}
function isEncoded(uri) {
uri = uri || '';
return uri !== decodeURIComponent(uri);
}
function fullyDecodeURI(uri) {
while (isEncoded(uri)) uri = decodeURIComponent(uri);
return uri;
}
/**
* Get difference in days between two dates.
*
* @param {Date} a
* @param {Date} b
* @returns
*/
function dateDiffInDays(a, b) {
let _MS_PER_DAY = 1000 * 60 * 60 * 24;
// Discard the time and time-zone information.
let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((utc1 - utc2) / _MS_PER_DAY);
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomFloat(min, max) {
return Math.random() * (max - min + 1) + min;
}
function keySort(keys, desc) {
return function (a, b) {
let aVal = null;
let bVal = null;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (i == 0) {
aVal = a[key];
bVal = b[key];
} else {
aVal = aVal[key];
bVal = bVal[key];
}
}
return desc ? ~~(aVal < bVal) : ~~(aVal > bVal);
};
}
function observe(obj, handler) {
return new Proxy(obj, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value;
if (handler) handler();
},
});
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Console} console
*/
function addSaveToConsole(console) {
console.save = function (data, filename) {
if (!data) {
console.error('Console.save: No data');
return;
}
if (!filename) filename = 'console.json';
if (typeof data === 'object') data = JSON.stringify(data, undefined, 4);
let blob = new Blob([data], {
type: 'text/json',
});
let event = document.createEvent('MouseEvents');
let tempElem = document.createElement('a');
tempElem.download = filename;
tempElem.href = window.URL.createObjectURL(blob);
tempElem.dataset.downloadurl = ['text/json', tempElem.download, tempElem.href].join(':');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
tempElem.dispatchEvent(event);
};
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Window} _window
* @param {string} propName
* @param {{} | [] | any} value
*/
function setupWindowProps(_window, propName, value) {
if (getType(value) == 'object') {
if (typeof _window[propName] === 'undefined' || _window[propName] == null) {
_window[propName] = {};
}
let keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!(/** @type {{}} */ (_window[propName].hasOwnProperty(key)))) {
_window[propName][key] = null;
}
if (_window[propName][key] == null) {
_window[propName][key] = value[key];
}
}
} else {
if (typeof _window[propName] === 'undefined' || _window[propName] == null) {
_window[propName] = value;
}
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{ name: string; value: any; }[]} variables
*/
function exposeGlobalVariables(variables) {
variables.forEach((variable, index, variables) => {
try {
setupWindowProps(getWindow(), variable.name, variable.value);
} catch (error) {
logger.error(`Unable to expose variable ${variable.name} into the global scope.`);
}
});
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} str
* @returns {string}
*/
function htmlEntitiesDecode(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} metaName
* @returns {string}
*/
function getMeta(metaName) {
const metas = document.getElementsByTagName('meta');
for (let i = 0; i < metas.length; i++) {
if (metas[i].getAttribute('name') === metaName) {
return metas[i].getAttribute('content');
}
}
return '';
}
/**
*
*
* @returns {Window & typeof globalThis}
*/
function getWindow() {
return globalThis.GM_info && GM_info.script.grant.includes('unsafeWindow') ? unsafeWindow : globalThis;
}
/**
*
*
* @param {Window} _window
*/
function getTopWindow(_window = null) {
_window = _window || getWindow();
try {
if (_window.self !== _window.top) {
_window = getTopWindow(_window.parent);
}
} catch (e) {}
return _window;
}
/**
* Setup global error handler
*
* **Example:**
* ```javascript
* setupGlobalErrorHandler({
* callback: (error) => console.error('Error:', error),
* continuous: true,
* prevent_default: true,
* tag: '[test-global-error-handler]',
* });
* ```
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{ callback: (error: ErrorEx) => void; continuous?: boolean; prevent_default?: boolean; tag?: string; logFunc?: (...data: any[]) => void; _window: Window; }} [{ callback, continuous = true, prevent_default = false, tag = '[akkd]', logFunc = console.error, _window = window } = {}]
*/
function setupGlobalErrorHandler({ callback, continuous = true, prevent_default = false, tag = null, logFunc = console.error, _window = window } = {}) {
// respect existing onerror handlers
let _onerror_original = _window.onerror;
// install our new error handler
_window.onerror = function (event, source, lineno, colno, error) {
if (_onerror_original) {
_onerror_original(event, source, lineno, colno, error);
}
// unset onerror to prevent loops and spamming
let _onerror = _window.onerror;
_window.onerror = null;
// now deal with the error
let errorObject = new ErrorEx(event, source, lineno, colno, error);
let errorMessage = createErrorMessage(errorObject);
if (tag) {
let rgb = '38;177;38';
tag = `\x1B[38;2;${rgb}m${tag}\x1B[m`;
logFunc(tag, errorMessage);
} else {
logFunc(errorMessage);
}
// run callback if provided
if (callback) {
callback(errorObject);
}
// re-install this error handler again if continuous mode
if (continuous) {
_window.onerror = _onerror;
}
// true if normal error propagation should be suppressed
// (i.e. normally console.error is logged by the browser)
return prevent_default;
};
class ErrorEx {
/**
* Creates an instance of ErrorEx.
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string | Event} event
* @param {string} source
* @param {number} lineno
* @param {number} colno
* @param {Error} error
* @memberof ErrorEx
*/
constructor(event, source, lineno, colno, error) {
this.name = error.name;
this.message = error && error.message ? error.message : null;
this.stack = error && error.stack ? error.stack : null;
this.event = event;
this.location = document.location.href;
this.url = source;
this.lineno = lineno;
this.colno = colno;
this.useragent = navigator.userAgent;
this.fileName = error && error.fileName ? error.fileName : null;
this.description = error && error.description ? error.description : null;
this.name = error && error.name ? error.name : null;
this.error = error;
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {ErrorEx} error
* @returns {string}
*/
function createErrorMessage(error) {
let name = error && error.name ? error.name : 'Error';
let message = error && error.message ? error.message : 'Unknown error occured';
let stack = error && error.stack ? error.stack.split('\n').splice(1).join('\n') : 'Error';
let errorMessage = `Uncaught Global ${name}: ${message}\n${stack}`;
return errorMessage;
}
}
function applyCss(cssFiles) {
/** @type {{ css: string, node?: HTMLElement }[]} */
let cssArr = [];
for (let i = 0; i < cssFiles.length; i++) {
let cssStr = GM_getResourceText(cssFiles[i]);
cssArr.push({
css: cssStr,
});
}
addStyles(cssArr);
}
function applyCss2(cssFiles) {
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} cssStyleStr
* @returns {HTMLStyleElement}
*/
function createStyleElementFromCss(cssStyleStr) {
let style = document.createElement('style');
style.innerHTML = cssStyleStr.trim();
return style;
}
let ranOnce = false;
/** @type {HTMLStyleElement[]} */
getWindow().akkd.styleElements = [];
function removeStyleElements() {
for (let i = 0; i < getWindow().akkd.styleElements.length; i++) {
let styleElement = getWindow().akkd.styleElements[i];
styleElement.remove();
}
getWindow().akkd.styleElements = [];
}
function _editStyleSheets() {
$(document).arrive('style, link', async function () {
if (this.tagName == 'LINK' && this.href.includes('.css')) {
removeStyleElements();
for (let i = 0; i < cssFiles.length; i++) {
let cssFile = cssFiles[i];
let css = GM_getResourceText(cssFile);
let styleElem = createStyleElementFromCss(css);
styleElem.id = `akkd-transform-style-${(i + 1).toString().padStart(2, '0')}`;
getWindow().akkd.styleElements.push(styleElem);
document.body.appendChild(styleElem);
}
}
});
if (!ranOnce) {
for (let i = 0; i < cssFiles.length; i++) {
let cssFile = cssFiles[i];
let css = GM_getResourceText(cssFile);
let styleElem = createStyleElementFromCss(css);
styleElem.id = `akkd-transform-style-${(i + 1).toString().padStart(2, '0')}`;
getWindow().akkd.styleElements.push(styleElem);
document.body.appendChild(styleElem);
}
ranOnce = true;
}
}
_editStyleSheets();
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{ css: string, node?: HTMLElement }[]} cssArr
*/
let addStyles = (function () {
/** @type {string[]} */
const addedStyleIds = [];
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{ css: string, node?: HTMLElement }[]} cssArr
* @param {{ useGM: boolean }} cssArr { useGM = true } = {}
*/
function _addStyles(cssArr, { useGM = true } = {}) {
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} css
* @returns {HTMLStyleElement}
*/
function createStyleElementFromCss(css) {
let style = document.createElement('style');
style.innerHTML = css.trim();
return style;
}
function removeStyleElements() {
for (let i = addedStyleIds.length - 1; i >= 0; i--) {
/** @type {HTMLStyleElement} */
let styleElem = document.getElementById(addedStyleIds[i]);
if (styleElem) {
styleElem.remove();
addedStyleIds.splice(i, 1);
}
}
}
function addStyleElements() {
for (let i = 0; i < cssArr.length; i++) {
try {
const css = cssArr[i].css;
const node = cssArr[i].node || document.head;
/** @type {HTMLStyleElement} */
let elem = useGM ? GM_addStyle(css) : createStyleElementFromCss(css);
elem.id = `akkd-custom-style-${(i + 1).toString().padStart(2, '0')}`;
node.append(elem);
addedStyleIds.push(elem.id);
} catch (error) {
console.error(error);
}
}
}
removeStyleElements();
addStyleElements();
return addedStyleIds;
}
return _addStyles;
})();
/**
* Return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {string}
*/
function uuid4() {
let uuid = '';
let ii;
for (ii = 0; ii < 32; ii += 1) {
switch (ii) {
case 8:
case 20:
uuid += '-';
uuid += ((Math.random() * 16) | 0).toString(16);
break;
case 12:
uuid += '-';
uuid += '4';
break;
case 16:
uuid += '-';
uuid += ((Math.random() * 4) | 8).toString(16);
break;
default:
uuid += ((Math.random() * 16) | 0).toString(16);
}
}
return uuid;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{} | []} obj
* @param {string | {oldName: string, newName: string}[]} oldName
* @param {string=} newName
* @returns
*/
function renameProperty(obj, oldName, newName) {
function _renameProperty(obj, oldName, newName) {
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = obj[key];
if (value && typeof value == 'object') {
obj[key] = _renameProperty(value, oldName, newName);
}
if (obj.hasOwnProperty(oldName)) {
obj[newName] = obj[oldName];
delete obj[oldName];
}
}
return obj;
}
let renames = Array.isArray(oldName) ? oldName : [{ oldName, newName }];
for (let i = 0; i < renames.length; i++) {
const rename = renames[i];
obj = _renameProperty(obj, rename.oldName, rename.newName);
}
return obj;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Promise<T>} fn
* @param {{ retries?: number; interval?: number; maxTime?: number; throwError?: boolean; }} { retries = 3, interval = 100, maxTime = null, throwError = false }
* @returns {Promise<T>}
* @template T
*/
async function retry(fn, { retries = 3, interval = 100, maxTime = null, throwError = false }) {
let start = new Date();
let timeLapsed;
async function _retry() {
try {
return await fn;
} catch (error) {
timeLapsed = new Date() - start;
await wait(interval);
if (maxTime) {
if (timeLapsed >= maxTime) {
if (throwError) {
throw error;
} else {
return null;
}
}
} else {
--retries;
if (retries === 0) {
if (throwError) {
throw error;
} else {
return null;
}
}
}
return await _retry();
}
}
return await _retry();
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} htmlStr
* @returns {NodeListOf<ChildNode>}
*/
function createElementsFromHTML(htmlStr) {
let div = document.createElement('div');
div.innerHTML = htmlStr.trim();
return div.childNodes;
}
/**
* Checks if a variable is a number.
*
* @param {*} variable - The variable to check.
* @returns {boolean} - `true` if the variable is a number or a number represented as a string, `false` otherwise.
*/
function isNumber(variable) {
return (typeof variable == 'string' || typeof variable == 'number') && !isNaN(variable - 0) && variable !== '';
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string | number} val
* @returns
*/
function parseNumberSafe(val) {
if (isNumber(val)) {
val = parseFloat(val);
}
return val;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} num
* @returns {boolean}
*/
function isInt(num) {
return Number(num) === num && num % 1 === 0;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} num
* @returns {boolean}
*/
function isFloat(num) {
return Number(num) === num && num % 1 !== 0;
}
/*
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} elem
* @param {string} prop
* @param {Window=} _window
* @returns {string | number | null}
*/
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} elem
* @param {string} prop
* @param {Window} [_window=getWindow()]
* @returns {string | number | null}
*/
function getStyle(elem, prop, _window = null) {
_window = _window || getWindow();
let value = parseNumberSafe(
window
.getComputedStyle(elem, null)
.getPropertyValue(prop)
.replace(/^(\d+)px$/, '$1')
);
return value;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} beforeElem
* @param {HTMLElement=} afterElem
*/
function attachHorizontalResizer(beforeElem, afterElem) {
let resizer = document.createElement('span');
resizer.className = 'akkd-horz-resizer';
beforeElem.after(resizer);
afterElem = afterElem ? afterElem : resizer.nextElementSibling;
// resizer.addEventListener('mousedown', init, false);
// /**
// *
// *
// * @author Michael Barros <michaelcbarros@gmail.com>
// * @param {MouseEvent} ev
// */
// function init(ev) {
// getWindow().addEventListener('mousemove', resize, false);
// getWindow().addEventListener('mouseup', stopResize, false);
// }
// /**
// *
// *
// * @author Michael Barros <michaelcbarros@gmail.com>
// * @param {MouseEvent} ev
// */
// function resize(ev) {
// beforeElem.style.height = `${ev.clientY - beforeElem.offsetTop}px`;
// }
// /**
// *
// *
// * @author Michael Barros <michaelcbarros@gmail.com>
// * @param {MouseEvent} ev
// */
// function stopResize(ev) {
// getWindow().removeEventListener('mousemove', resize, false);
// getWindow().removeEventListener('mouseup', stopResize, false);
// }
let prevX = -1;
let prevY = -1;
let dir = null;
$(resizer).on('mousedown', function (e) {
prevX = e.clientX;
prevY = e.clientY;
dir = 'n'; // $(this).attr('id');
$(document).on('mousemove', resize);
$(document).on('mouseup', stopResize);
});
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {JQuery.MouseMoveEvent<Document, undefined, Document, Document>} ev
*/
function resize(ev) {
if (prevX == -1) return;
let boxX = $(afterElem).position().left;
let boxY = $(afterElem).position().top;
let boxW = $(afterElem).width();
let boxH = $(afterElem).height();
let dx = ev.clientX - prevX;
let dy = ev.clientY - prevY;
switch (dir) {
case 'n':
// north
boxY += dy;
boxH -= dy;
break;
case 's':
// south
boxH += dy;
break;
case 'w':
// west
boxX += dx;
boxW -= dx;
break;
case 'e':
// east
boxW += dx;
break;
default:
break;
}
$(afterElem).css({
// top: boxY + 'px',
// left: boxX + 'px',
// width: boxW + 'px',
height: boxH + 'px',
});
let lines = [
//
// ['newHeight', newHeight],
['clientY', ev.clientY],
['beforeElem.top', roundNumber($(beforeElem).position().top)],
['beforeElem.height', $(beforeElem).height()],
'',
['afterElem.top', roundNumber($(afterElem).position().top)],
['afterElem.height', $(afterElem).height()],
];
// writeDebugMsg(lines);
console.debug([`y: ${ev.clientY}`, `b.top: ${roundNumber($(beforeElem).position().top)}`, `b.height: ${$(beforeElem).height()}`, `a.top: ${roundNumber($(afterElem).position().top)}`, `a.height: ${$(afterElem).height()}`].join(' '));
function writeDebugMsg(lines) {
let outputLines = ['*'.repeat(60)];
let tags = lines.map((line) => (Array.isArray(line) ? line[0] : line));
lines.forEach((line) => {
if (Array.isArray(line)) {
// need to require lpad-align
// outputLines.push(`${lpadAlign(line[0], tags)}: ${line[1]}`);
} else {
outputLines.push(line);
}
});
outputLines.push('*'.repeat(60));
console.debug(outputLines.join('\n'));
}
function roundNumber(num, places = 2) {
return parseFloat(parseFloat(num.toString()).toFixed(places));
// Math.round(num * 100) / 100
}
prevX = ev.clientX;
prevY = ev.clientY;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {JQuery.MouseMoveEvent<Document, undefined, Document, Document>} ev
*/
function stopResize(ev) {
prevX = -1;
prevY = -1;
$(document).off('mousemove', resize);
$(document).off('mouseup', stopResize);
}
}
function traceMethodCalls(obj) {
/** @type {ProxyHandler} */
let handler = {
get(target, propKey, receiver) {
if (propKey == 'isProxy') return true;
const prop = target[propKey];
if (typeof prop == 'undefined') return;
if (typeof prop === 'object' && target[propKey] !== null) {
if (!prop.isProxy) {
target[propKey] = new Proxy(prop, handler);
return target[propKey];
} else {
return target[propKey];
}
}
if (typeof target[propKey] == 'function') {
const origMethod = target[propKey];
return function (...args) {
let result = origMethod.apply(this, args);
console.log(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result));
return result;
};
} else {
return target[propKey];
}
},
};
return new Proxy(obj, handler);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} elem
* @param {number} [topOffset=0]
* @returns {boolean}
*/
function isVisible(elem, topOffset = 0) {
/**
* Checks if a DOM element is visible. Takes into
* consideration its parents and overflow.
*
* @param {HTMLElement} el the DOM element to check if is visible
*
* These params are optional that are sent in recursively,
* you typically won't use these:
*
* @param {number} top Top corner position number
* @param {number} right Right corner position number
* @param {number} bottom Bottom corner position number
* @param {number} left Left corner position number
* @param {number} width Element width number
* @param {number} height Element height number
* @returns {boolean}
*/
function _isVisible(el, top, right, bottom, left, width, height) {
let parent = el.parentNode;
let VISIBLE_PADDING = 2;
if (!_elementInDocument(el)) {
return false;
}
// Return true for document node
if (9 === parent.nodeType) {
return true;
}
// Return false if our element is invisible
if ('0' === _getStyle(el, 'opacity') || 'none' === _getStyle(el, 'display') || 'hidden' === _getStyle(el, 'visibility')) {
return false;
}
if ('undefined' === typeof top || 'undefined' === typeof right || 'undefined' === typeof bottom || 'undefined' === typeof left || 'undefined' === typeof width || 'undefined' === typeof height) {
top = el.offsetTop + topOffset;
left = el.offsetLeft;
bottom = top + el.offsetHeight;
right = left + el.offsetWidth;
width = el.offsetWidth;
height = el.offsetHeight;
}
// If we have a parent, let's continue:
if (parent) {
// Check if the parent can hide its children.
if ('hidden' === _getStyle(parent, 'overflow') || 'scroll' === _getStyle(parent, 'overflow')) {
// Only check if the offset is different for the parent
if (
// If the target element is to the right of the parent elm
left + VISIBLE_PADDING > parent.offsetWidth + parent.scrollLeft ||
// If the target element is to the left of the parent elm
left + width - VISIBLE_PADDING < parent.scrollLeft ||
// If the target element is under the parent elm
top + VISIBLE_PADDING > parent.offsetHeight + parent.scrollTop ||
// If the target element is above the parent elm
top + height - VISIBLE_PADDING < parent.scrollTop
) {
// Our target element is out of bounds:
return false;
}
}
// Add the offset parent's left/top coords to our element's offset:
if (el.offsetParent === parent) {
left += parent.offsetLeft;
top += parent.offsetTop;
}
// Let's recursively check upwards:
return _isVisible(parent, top, right, bottom, left, width, height);
}
return true;
}
// Cross browser method to get style properties:
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} el
* @param {string} property
* @returns
*/
function _getStyle(el, property) {
let value;
if (window.getComputedStyle) {
value = document.defaultView.getComputedStyle(el, null)[property];
}
if (el.currentStyle) {
value = el.currentStyle[property];
}
return value;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} element
* @returns {boolean}
*/
function _elementInDocument(element) {
while ((element = element.parentNode)) {
if (element == document) {
return true;
}
}
return false;
}
return _isVisible(elem);
}
/**
* @summary
* High-order function that memoizes a function, by creating a scope
* to store the result of each function call, returning the cached
* result when the same inputs is given.
*
* @description
* Memoization is an optimization technique used primarily to speed up
* functions by storing the results of expensive function calls, and returning
* the cached result when the same inputs occur again.
*
* Each time a memoized function is called, its parameters are used as keys to index the cache.
* If the index (key) is present, then it can be returned, without executing the entire function.
* If the index is not cached, then all the body of the function is executed, and the result is
* added to the cache.
*
* @see https://www.sitepoint.com/implementing-memoization-in-javascript/
*
* @export
* @param {Function} func: function to memoize
* @returns {Function}
*/
function memoize(func) {
const cache = {};
function memoized(...args) {
const key = JSON.stringify(args);
if (key in cache) return cache[key];
if (globalThis instanceof this.constructor) {
return (cache[key] = func.apply(null, args));
} else {
return (cache[key] = func.apply(this, args));
}
}
memoized.toString = () => func.toString();
return memoized;
}
function memoizeClass(clazz, options = { toIgnore: [] }) {
let funcs = getObjProps(clazz, { namesOnly: true }).funcs;
for (let i = 0; i < funcs.length; i++) {
let funcName = funcs[i];
if (options.toIgnore.includes(funcName)) continue;
let func = Object.getOwnPropertyDescriptor(clazz.prototype, funcName);
let memFunc = memoize(func.value);
Object.defineProperty(clazz.prototype, funcName, {
get: function () {
return memFunc;
},
});
}
let props = getObjProps(clazz, { namesOnly: true }).props;
for (let i = 0; i < props.length; i++) {
let propName = props[i];
if (options.toIgnore.includes(propName)) continue;
let prop = Object.getOwnPropertyDescriptor(clazz.prototype, propName);
let cacheKey = `_${propName}-cache_`;
Object.defineProperty(clazz.prototype, propName, {
get: function () {
return (this[cacheKey] = this[cacheKey] || prop.get.call(this));
},
});
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {any[]} objects
* @returns {object}
*/
function merge(...objects) {
const isObject = (obj) => Object.prototype.toString.call(obj) == '[object Object]' && obj.constructor && obj.constructor.name == 'Object';
let _merge = (_target, _source, _isMergingArrays) => {
if (!isObject(_target) || !isObject(_source)) {
return _source;
}
Object.keys(_source).forEach((key) => {
const targetValue = _target[key];
const sourceValue = _source[key];
if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
if (_isMergingArrays) {
_target[key] = targetValue.map((x, i) => (sourceValue.length <= i ? x : _merge(x, sourceValue[i], _isMergingArrays)));
if (sourceValue.length > targetValue.length) {
_target[key] = _target[key].concat(sourceValue.slice(targetValue.length));
}
} else {
_target[key] = targetValue.concat(sourceValue);
}
} else if (isObject(targetValue) && isObject(sourceValue)) {
_target[key] = _merge(Object.assign({}, targetValue), sourceValue, _isMergingArrays);
} else {
_target[key] = sourceValue;
}
});
return _target;
};
const isMergingArrays = typeof objects[objects.length - 1] == 'boolean' ? objects.pop() : false;
if (objects.length < 2) throw new Error('mergeEx: this function expects at least 2 objects to be provided');
if (objects.some((object) => !isObject(object))) throw new Error('mergeEx: all values should be of type "object"');
const target = objects.shift();
let source;
while ((source = objects.shift())) {
_merge(target, source, isMergingArrays);
}
return target;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {Window} [windowOrFrame=getTopWindow()]
* @param {Window[]} [allFrameArray=[]]
* @returns {Window[]}
*/
function getAllFrames(windowOrFrame = getTopWindow(), allFrameArray = []) {
allFrameArray.push(windowOrFrame.frames);
for (var i = 0; i < windowOrFrame.frames.length; i++) {
getAllFrames(windowOrFrame.frames[i], allFrameArray);
}
return allFrameArray;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {boolean}
*/
function windowIsFocused() {
let frames = getAllFrames();
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
try {
if (frame.document.hasFocus()) {
return true;
}
} catch (error) {}
}
return false;
}
function setupWindowHasFocused() {
let frames = getAllFrames();
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
try {
frame.hasFocus = windowIsFocused;
} catch (error) {}
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {[]} array
* @param {any} element
* @param {number} index
*/
function moveArrayElement(array, filter, index) {
let item = array.filter((item) => item === filter)[0];
if (item) {
array = array.filter((item) => item !== filter);
array.unshift(item);
}
return array;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} elem
* @param {(elem: HTMLElement, level: number) => void} callback
* @param {number} [level=0]
*/
function walkDom(elem, callback, level = 0) {
let children = elem.children;
callback(elem, level);
for (let i = 0; i < children.length; i++) {
/** @type {HTMLElement} */
let child = children[i];
walkDom(child, callback, level + 1);
if (child.shadowRoot) {
walkDom(child.shadowRoot, callback, level + 2);
}
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {T} obj
* @param {(key: string | number, value: any, keyPath: string, callbackRes: { doBreak: boolean, returnValue: any | null }, obj: T) => boolean} callback
* @template T
* @returns {{ dottedObj: T, returnValue: any }}
*/
function walkObj(obj, callback) {
let callbackRes = {
doBreak: false,
returnValue: null,
};
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{}} _obj
* @param {string[]} keyPath
* @param {{}} newObj
*/
function _walk(_obj, keyPath, newObj) {
keyPath = typeof keyPath === 'undefined' ? [] : keyPath;
newObj = typeof newObj === 'undefined' ? {} : newObj;
for (let key in _obj) {
if (_obj.hasOwnProperty(key)) {
let value = _obj[key];
keyPath.push(key);
callback.apply(this, [key, value, keyPath.join('.'), callbackRes, obj]);
if (typeof value === 'object' && value !== null) {
newObj = _walk(value, keyPath, newObj);
} else {
let newKey = keyPath.join('.');
newObj[newKey] = value;
}
keyPath.pop();
if (callbackRes.doBreak) {
break;
}
}
}
return newObj;
}
let newObj = _walk(obj);
return {
dottedObj: newObj,
returnValue: callbackRes.returnValue,
};
}
/**
* A function to take a string written in dot notation style, and use it to
* find a nested object property inside of an object.
*
* Useful in a plugin or module that accepts a JSON array of objects, but
* you want to let the user specify where to find various bits of data
* inside of each custom object instead of forcing a standardized
* property list.
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{}} obj
* @param {string} dotPath
* @returns {*}
*/
function getNestedDot(obj, dotPath) {
let parts = dotPath.split('.');
let length = parts.length;
let property = obj || this;
for (let i = 0; i < length; i++) {
property = property[parts[i]];
}
return property;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{}} obj
* @param {number} maxLevel
*/
function getDottedObj(obj, maxLevel = 50) {
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {{}} _obj
* @param {string[]} keyPath
* @param {{}} newObj
* @param {number} level
*/
function _worker(_obj, keyPath, newObj, level = 0) {
keyPath = typeof keyPath === 'undefined' ? [] : keyPath;
newObj = typeof newObj === 'undefined' ? {} : newObj;
for (let key in _obj) {
if (_obj.hasOwnProperty(key)) {
let value = _obj[key];
keyPath.push(key);
if (typeof value === 'object' && value !== null) {
newObj = _worker(value, keyPath, newObj, level++);
} else {
let newKey = keyPath.join('.');
newObj[newKey] = value;
}
keyPath.pop();
if (maxLevel > 0 && level >= maxLevel) {
break;
}
}
}
return newObj;
}
let dottedObj = _worker(obj);
return dottedObj;
}
function isNode() {
return !(typeof window !== 'undefined' && typeof window.document !== 'undefined');
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {number}
*/
function getCurrentTimeMs() {
if (isNode()) {
const NS_PER_MS = 1e6;
let time = process.hrtime();
return time[0] * 1000 + time[1] / NS_PER_MS;
} else {
return performance.now();
}
}
const intervalIDsMap = new Map();
const timeoutIDsMap = new Map();
/**
* Schedules the repeated execution of a function (callback) with a fixed time delay between each call.
* @param {TimerHandler} handler - A function to be executed repeatedly.
* @param {number} [timeout] - The time, in milliseconds, between each function call. Default is 0.
* @param {...any} [args] - Additional arguments to be passed to the function.
* @returns {number} - An identifier representing the interval. This value can be used with clearInterval to cancel the interval.
*/
function setIntervalEx(handler, timeout, ...args) {
if (isNode()) {
return setInterval(handler, timeout, ...args);
} else {
let startTime = getCurrentTimeMs();
let elapsedTime = 0;
/** @type {number} */
let intervalId;
let intervalIdTemp;
function loop(currentTime) {
if (intervalIDsMap.get(intervalId)) {
const deltaTime = currentTime - startTime;
elapsedTime += deltaTime;
if (elapsedTime >= timeout) {
handler(...args);
elapsedTime = 0;
}
startTime = currentTime;
intervalIdTemp = window.requestAnimationFrame(loop);
} else {
window.cancelAnimationFrame(intervalIdTemp);
intervalIDsMap.delete(intervalId);
}
}
intervalId = window.requestAnimationFrame(loop);
intervalIDsMap.set(intervalId, true);
return intervalId;
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} intervalId
*/
function clearIntervalEx(intervalId) {
if (isNode()) {
clearInterval(intervalId);
} else {
intervalIDsMap.set(intervalId, false);
window.cancelAnimationFrame(intervalId);
}
}
/**
* Schedules the execution of a function (callback) after a specified time delay.
* @param {TimerHandler} handler - A function to be executed.
* @param {number} [timeout] - The time, in milliseconds, to wait before executing the function. Default is 0.
* @param {...any} [args] - Additional arguments to be passed to the function.
* @returns {number} - An identifier representing the timeout. This value can be used with clearTimeout to cancel the timeout.
*/
function setTimeoutEx(handler, timeout, ...args) {
if (isNode()) {
return setTimeout(handler, timeout, ...args);
} else {
let startTime = getCurrentTimeMs();
/** @type {number} */
let timeoutId;
let timeoutIdTemp;
function loop(currentTime) {
if (timeoutIDsMap.get(timeoutId)) {
const deltaTime = currentTime - startTime;
if (deltaTime >= timeout) {
handler(...args);
} else {
timeoutIdTemp = window.requestAnimationFrame(loop);
}
} else {
window.cancelAnimationFrame(timeoutIdTemp);
timeoutIDsMap.delete(timeoutId);
}
}
timeoutId = window.requestAnimationFrame(loop);
timeoutIDsMap.set(timeoutId, true);
return timeoutId;
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} timeoutId
*/
function clearTimeoutEx(timeoutId) {
if (isNode()) {
clearTimeout(timeoutId);
} else {
timeoutIDsMap.set(timeoutId, false);
window.cancelAnimationFrame(timeoutId);
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} attribute
* @param {string} value
* @param {string} elementType
* @returns {HTMLElement[]}
*/
function findByAttributeValue(attribute, value, elementType) {
elementType = elementType || '*';
let all = document.getElementsByTagName(elementType);
let foundElements = [];
for (let i = 0; i < all.length; i++) {
if (all[i].getAttribute(attribute).includes(value)) {
foundElements.push(all[i]);
}
}
return foundElements;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {number}
*/
function getLocalStorageSize() {
let total = 0;
for (let x in localStorage) {
// Value is multiplied by 2 due to data being stored in `utf-16` format, which requires twice the space.
let amount = localStorage[x].length * 2;
if (!isNaN(amount) && localStorage.hasOwnProperty(x)) {
total += amount;
}
}
return total;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} bytes
* @param {boolean} [si=false]
* @returns {string}
*/
function bytes2HumanReadable(bytes, si = false) {
let thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
let units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
while (true) {
bytes = bytes / thresh;
u++;
if (!(Math.abs(bytes) >= thresh && u < units.length - 1)) {
break;
}
}
return bytes.toFixed(1) + ' ' + units[u];
}
async function GM_fetch(url, fetchInit = {}) {
if (!window.GM_xmlhttpRequest) {
console.warn('GM_xmlhttpRequest not required. Using native fetch.');
return await fetch(url, fetchInit);
}
let parseHeaders = function (headersString) {
const headers = new Headers();
for (const line of headersString.trim().split('\n')) {
const [key, ...valueParts] = line.split(':');
if (key) {
headers.set(key.trim().toLowerCase(), valueParts.join(':').trim());
}
}
return headers;
};
const defaultFetchInit = { method: 'get' };
const { headers, method } = { ...defaultFetchInit, ...fetchInit };
const isStreamSupported = GM_xmlhttpRequest?.RESPONSE_TYPE_STREAM;
const HEADERS_RECEIVED = 2;
if (!isStreamSupported) {
return new Promise((resolve, _reject) => {
const blobPromise = new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
method,
headers,
responseType: 'blob',
onload: async (response) => resolve(response.response),
onerror: reject,
onreadystatechange: onHeadersReceived,
});
});
blobPromise.catch(_reject);
function onHeadersReceived(gmResponse) {
const { readyState, responseHeaders, status, statusText } = gmResponse;
if (readyState === HEADERS_RECEIVED) {
const headers = parseHeaders(responseHeaders);
resolve({
headers,
status,
statusText,
arrayBuffer: () => blobPromise.then((blob) => blob.arrayBuffer()),
blob: () => blobPromise,
json: () => blobPromise.then((blob) => blob.text()).then((text) => JSON.parse(text)),
text: () => blobPromise.then((blob) => blob.text()),
});
}
}
});
} else {
return new Promise((resolve, _reject) => {
const responsePromise = new Promise((resolve, reject) => {
void GM_xmlhttpRequest({
url,
method,
headers,
responseType: 'stream',
onerror: reject,
onreadystatechange: onHeadersReceived,
// onloadstart: (gmResponse) => logDebug('[onloadstart]', gmResponse), // debug
});
});
responsePromise.catch(_reject);
function onHeadersReceived(gmResponse) {
const { readyState, responseHeaders, status, statusText, response: readableStream } = gmResponse;
if (readyState === HEADERS_RECEIVED) {
const headers = parseHeaders(responseHeaders);
let newResp;
if (status === 0) {
newResp = new Response(readableStream, { headers /*status, statusText*/ });
} else {
newResp = new Response(readableStream, { headers, status, statusText });
}
resolve(newResp);
}
}
});
}
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {T} items
* @param {{name: string, desc: boolean, case_sensitive: boolean}[]} columns
* @param {{cmpFunc: any}} [{ cmpFunc = cmp }={}]
* @returns {T}
* @template T
*/
function multiKeySort(items, columns, { cmpFunc = null } = {}) {
function cmp(a, b) {
if (a < b) {
return -1;
} else {
if (a > b) {
return 1;
} else {
return 0;
}
}
}
cmpFunc = cmpFunc != null ? cmpFunc : cmp;
let comparers = [];
columns.forEach((col) => {
let column = col.name;
let desc = 'desc' in col ? col.desc : false;
let case_sensitive = 'case_sensitive' in col ? col.case_sensitive : true;
comparers.push([column, desc ? -1 : 1, case_sensitive]);
});
function comparer(left, right) {
for (let i = 0; i < comparers.length; i++) {
const column = comparers[i][0];
const polarity = comparers[i][1];
const case_sensitive = comparers[i][2];
let result = 0;
if (case_sensitive) {
result = cmpFunc(left[column], right[column]);
} else {
result = cmpFunc(left[column].toLowerCase(), right[column].toLowerCase());
}
if (result) {
return polarity * result;
}
}
return 0;
}
return items.sort(comparer);
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} text
* @param {string} [nodeType='div']
* @returns {HTMLElement}
*/
function getElementByTextContent(text, nodeType = 'div') {
let xpath = `//${nodeType}[text()='${text}']`;
/** @type {HTMLElement} */
let elem = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
return elem;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} text
* @param {string} [nodeType='div']
* @returns {HTMLElement}
*/
function getElementByTextContentContains(text, nodeType = 'div') {
let xpath = `//${nodeType}[contains(text(),'${text}')]`;
/** @type {HTMLElement} */
let elem = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
return elem;
}
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {HTMLElement} element
* @param {(elem: HTMLElement) => void} callback
*/
function onRemove(element, callback) {
const parent = element.parentNode;
if (!parent) {
throw new Error('The node must already be attached');
}
const observer = new MutationObserver((mutations) => {
let removed = false;
for (const mutation of mutations) {
for (const node of mutation.removedNodes) {
if (node === element) {
observer.disconnect();
callback(element);
removed = true;
break;
}
}
if (removed) {
break;
}
}
});
observer.observe(parent, {
childList: true,
});
}
// #endregion Helper Functions
// #region Prototype Functions
// #region Array
/**
* Function to setup custom prototype functions for the Array class
* (For no conflicts, Object.defineProperty must be used)
*
*/
function setupArrayPrototypes() {
let funcs = [
function pushUnique(item) {
let index = -1;
for (let i = 0; i < this.length; i++) {
if (equals(this[i], item)) index = i;
}
if (index === -1) this.push(item);
},
];
for (let i = 0; i < funcs.length; i++) {
let func = funcs[i];
Object.defineProperty(Array.prototype, func.name, {
enumerable: false,
configurable: true,
writable: true,
value: func,
});
}
}
setupArrayPrototypes();
// #endregion Array
// #endregion Prototype Functions
// #region jQuery
function setupJqueryExtendedFuncs() {
if ('jQuery' in getWindow() || 'jQuery' in window) {
jQuery.fn.extend({
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {boolean}
* @this {JQuery<HTMLElement>}
*/
exists: function exists() {
return this.length !== 0;
},
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {() => void} callback
* @returns {JQuery<HTMLElement>}
* @this {JQuery<HTMLElement>}
*/
ready: function ready(callback) {
let cb = function cb() {
return setTimeout(callback, 0, jQuery);
};
if (document.readyState !== 'loading') {
cb();
} else {
document.addEventListener('DOMContentLoaded', cb);
}
return this;
},
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {string} method
* @param {{}} options
* @returns {number | string | null}
* @this {JQuery<HTMLElement>}
*/
actual: function actual(method, options) {
// check if the jQuery method exist
if (!this[method]) {
throw '$.actual => The jQuery method "' + method + '" you called does not exist';
}
let defaults = {
absolute: false,
clone: false,
includeMargin: false,
display: 'block',
};
let configs = jQuery.extend(defaults, options);
let $target = this.eq(0);
let fix;
let restore;
if (configs.clone === true) {
fix = function () {
let style = 'position: absolute !important; top: -1000 !important; ';
// this is useful with css3pie
$target = $target.clone().attr('style', style).appendTo('body');
};
restore = function () {
// remove DOM element after getting the width
$target.remove();
};
} else {
let tmp = [];
let style = '';
let $hidden;
fix = function () {
// get all hidden parents
$hidden = $target.parents().addBack().filter(':hidden');
style += 'visibility: hidden !important; display: ' + configs.display + ' !important; ';
if (configs.absolute === true) {
style += 'position: absolute !important; ';
}
// save the origin style props
// set the hidden el css to be got the actual value later
$hidden.each(function () {
// Save original style. If no style was set, attr() returns undefined
let $this = jQuery(this);
let thisStyle = $this.attr('style');
tmp.push(thisStyle);
// Retain as much of the original style as possible, if there is one
$this.attr('style', thisStyle ? thisStyle + ';' + style : style);
});
};
restore = function () {
// restore origin style values
$hidden.each(function (i) {
let $this = jQuery(this);
let _tmp = tmp[i];
if (_tmp === undefined) {
$this.removeAttr('style');
} else {
$this.attr('style', _tmp);
}
});
};
}
fix();
// get the actual value with user specific methed
// it can be 'width', 'height', 'outerWidth', 'innerWidth'... etc
// configs.includeMargin only works for 'outerWidth' and 'outerHeight'
let actual = /(outer)/.test(method) ? $target[method](configs.includeMargin) : $target[method]();
restore();
// IMPORTANT, this plugin only return the value of the first element
return actual;
},
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @returns {DOMRect}
* @this {JQuery<HTMLElement>}
*/
rect: function rect() {
return this[0].getBoundingClientRect();
},
/**
*
*
* @author Michael Barros <michaelcbarros@gmail.com>
* @param {number} levels
* @returns {JQuery<HTMLElement>}
* @this {JQuery<HTMLElement>}
*/
parentEx: function parentEx(levels = 1) {
let parent = this;
for (let i = 0; i < levels; i++) {
if (parent.parent().length == 0) {
break;
}
parent = parent.parent();
}
return parent;
},
});
}
}
setupJqueryExtendedFuncs();
// #endregion jQuery