429 lines
11 KiB
JavaScript
429 lines
11 KiB
JavaScript
/** @odoo-module */
|
|
|
|
/**
|
|
* @typedef {ArgumentPrimitive | `${ArgumentPrimitive}[]` | null} ArgumentType
|
|
*
|
|
* @typedef {"any"
|
|
* | "bigint"
|
|
* | "boolean"
|
|
* | "error"
|
|
* | "function"
|
|
* | "integer"
|
|
* | "node"
|
|
* | "number"
|
|
* | "object"
|
|
* | "regex"
|
|
* | "string"
|
|
* | "symbol"
|
|
* | "undefined"} ArgumentPrimitive
|
|
*
|
|
* @typedef {[string, string | undefined, any[], any]} InteractionDetails
|
|
*
|
|
* @typedef {"interaction" | "query" | "server" | "time"} InteractionType
|
|
*/
|
|
|
|
/**
|
|
* @template T
|
|
* @typedef {T | Iterable<T>} MaybeIterable
|
|
*/
|
|
|
|
/**
|
|
* @template T
|
|
* @typedef {T | PromiseLike<T>} MaybePromise
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Global
|
|
//-----------------------------------------------------------------------------
|
|
|
|
const {
|
|
Array: { isArray: $isArray },
|
|
matchMedia,
|
|
navigator: { userAgent: $userAgent },
|
|
Object: { assign: $assign, getPrototypeOf: $getPrototypeOf },
|
|
RegExp,
|
|
SyntaxError,
|
|
} = globalThis;
|
|
const $toString = Object.prototype.toString;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Internal
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @template {(...args: any[]) => any} T
|
|
* @param {InteractionType} type
|
|
* @param {T} fn
|
|
* @param {string} name
|
|
* @param {string} [alias]
|
|
* @returns {T}
|
|
*/
|
|
function makeInteractorFn(type, fn, name, alias) {
|
|
return {
|
|
[alias || name](...args) {
|
|
const result = fn(...args);
|
|
if (isInstanceOf(result, Promise)) {
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (isInstanceOf(args[i], Promise)) {
|
|
// Get promise result for async arguments if possible
|
|
args[i].then((result) => (args[i] = result));
|
|
}
|
|
}
|
|
return result.then((promiseResult) =>
|
|
dispatchInteraction(type, name, alias, args, promiseResult)
|
|
);
|
|
} else {
|
|
return dispatchInteraction(type, name, alias, args, result);
|
|
}
|
|
},
|
|
}[alias || name];
|
|
}
|
|
|
|
function polyfillIsError(value) {
|
|
return $toString.call(value) === "[object Error]";
|
|
}
|
|
|
|
const GRAYS = {
|
|
100: "#f1f5f9",
|
|
200: "#e2e8f0",
|
|
300: "#cbd5e1",
|
|
400: "#94a3b8",
|
|
500: "#64748b",
|
|
600: "#475569",
|
|
700: "#334155",
|
|
800: "#1e293b",
|
|
900: "#0f172a",
|
|
};
|
|
|
|
const COLORS = {
|
|
default: {
|
|
// Generic colors
|
|
black: "#000000",
|
|
white: "#ffffff",
|
|
|
|
// Grays
|
|
"gray-100": GRAYS[100],
|
|
"gray-200": GRAYS[200],
|
|
"gray-300": GRAYS[300],
|
|
"gray-400": GRAYS[400],
|
|
"gray-500": GRAYS[500],
|
|
"gray-600": GRAYS[600],
|
|
"gray-700": GRAYS[700],
|
|
"gray-800": GRAYS[800],
|
|
"gray-900": GRAYS[900],
|
|
},
|
|
light: {
|
|
// Generic colors
|
|
primary: "#714b67",
|
|
secondary: "#74b4b9",
|
|
amber: "#f59e0b",
|
|
"amber-900": "#fef3c7",
|
|
blue: "#3b82f6",
|
|
"blue-900": "#dbeafe",
|
|
cyan: "#0891b2",
|
|
"cyan-900": "#e0f2fe",
|
|
emerald: "#047857",
|
|
"emerald-900": "#ecfdf5",
|
|
gray: GRAYS[400],
|
|
lime: "#84cc16",
|
|
"lime-900": "#f7fee7",
|
|
orange: "#ea580c",
|
|
"orange-900": "#ffedd5",
|
|
purple: "#581c87",
|
|
"purple-900": "#f3e8ff",
|
|
rose: "#9f1239",
|
|
"rose-900": "#fecdd3",
|
|
|
|
// App colors
|
|
bg: GRAYS[100],
|
|
text: GRAYS[900],
|
|
"status-bg": GRAYS[300],
|
|
"link-text-hover": "var(--primary)",
|
|
"btn-bg": "#714b67",
|
|
"btn-bg-hover": "#624159",
|
|
"btn-text": "#ffffff",
|
|
"bg-result": "rgba(255, 255, 255, 0.6)",
|
|
"border-result": GRAYS[300],
|
|
"border-search": "#d8dadd",
|
|
"shadow-opacity": 0.1,
|
|
|
|
// HootReporting colors
|
|
"bg-report": "#ffffff",
|
|
"text-report": "#202124",
|
|
"border-report": "#f0f0f0",
|
|
"bg-report-error": "#fff0f0",
|
|
"text-report-error": "#ff0000",
|
|
"border-report-error": "#ffd6d6",
|
|
"text-report-number": "#1a1aa6",
|
|
"text-report-string": "#c80000",
|
|
"text-report-key": "#881280",
|
|
"text-report-html-tag": "#881280",
|
|
"text-report-html-id": "#1a1aa8",
|
|
"text-report-html-class": "#994500",
|
|
},
|
|
dark: {
|
|
// Generic colors
|
|
primary: "#14b8a6",
|
|
amber: "#fbbf24",
|
|
"amber-900": "#422006",
|
|
blue: "#60a5fa",
|
|
"blue-900": "#172554",
|
|
cyan: "#22d3ee",
|
|
"cyan-900": "#083344",
|
|
emerald: "#34d399",
|
|
"emerald-900": "#064e3b",
|
|
gray: GRAYS[500],
|
|
lime: "#bef264",
|
|
"lime-900": "#365314",
|
|
orange: "#fb923c",
|
|
"orange-900": "#431407",
|
|
purple: "#a855f7",
|
|
"purple-900": "#3b0764",
|
|
rose: "#fb7185",
|
|
"rose-900": "#4c0519",
|
|
|
|
// App colors
|
|
bg: GRAYS[900],
|
|
text: GRAYS[100],
|
|
"status-bg": GRAYS[700],
|
|
"btn-bg": "#00dac5",
|
|
"btn-bg-hover": "#00c1ae",
|
|
"btn-text": "#000000",
|
|
"bg-result": "rgba(0, 0, 0, 0.5)",
|
|
"border-result": GRAYS[600],
|
|
"border-search": "#3c3f4c",
|
|
"shadow-opacity": 0.4,
|
|
|
|
// HootReporting colors
|
|
"bg-report": "#202124",
|
|
"text-report": "#e8eaed",
|
|
"border-report": "#3a3a3a",
|
|
"bg-report-error": "#290000",
|
|
"text-report-error": "#ff8080",
|
|
"border-report-error": "#5c0000",
|
|
"text-report-number": "#9980ff",
|
|
"text-report-string": "#f28b54",
|
|
"text-report-key": "#5db0d7",
|
|
"text-report-html-tag": "#5db0d7",
|
|
"text-report-html-id": "#f29364",
|
|
"text-report-html-class": "#9bbbdc",
|
|
},
|
|
};
|
|
const DEBUG_NAMESPACE = "hoot";
|
|
|
|
const isError = typeof Error.isError === "function" ? Error.isError : polyfillIsError;
|
|
const interactionBus = new EventTarget();
|
|
const preferredColorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @param {Iterable<InteractionType>} types
|
|
* @param {(event: CustomEvent<InteractionDetails>) => any} callback
|
|
*/
|
|
export function addInteractionListener(types, callback) {
|
|
for (const type of types) {
|
|
interactionBus.addEventListener(type, callback);
|
|
}
|
|
|
|
return function removeInteractionListener() {
|
|
for (const type of types) {
|
|
interactionBus.removeEventListener(type, callback);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {InteractionType} type
|
|
* @param {string} name
|
|
* @param {string | undefined} alias
|
|
* @param {any[]} args
|
|
* @param {any} returnValue
|
|
*/
|
|
export function dispatchInteraction(type, name, alias, args, returnValue) {
|
|
interactionBus.dispatchEvent(
|
|
new CustomEvent(type, {
|
|
detail: [name, alias, args, returnValue],
|
|
})
|
|
);
|
|
return returnValue;
|
|
}
|
|
|
|
/**
|
|
* @param {...any} helpers
|
|
*/
|
|
export function exposeHelpers(...helpers) {
|
|
let nameSpaceIndex = 1;
|
|
let nameSpace = DEBUG_NAMESPACE;
|
|
while (nameSpace in globalThis) {
|
|
nameSpace = `${DEBUG_NAMESPACE}${nameSpaceIndex++}`;
|
|
}
|
|
globalThis[nameSpace] = new HootDebugHelpers(...helpers);
|
|
return nameSpace;
|
|
}
|
|
|
|
/**
|
|
* @param {keyof typeof COLORS} [scheme]
|
|
*/
|
|
export function getAllColors(scheme) {
|
|
return scheme ? COLORS[scheme] : COLORS;
|
|
}
|
|
|
|
/**
|
|
* @param {keyof typeof COLORS["light"]} varName
|
|
*/
|
|
export function getColorHex(varName) {
|
|
return COLORS[preferredColorScheme][varName];
|
|
}
|
|
|
|
export function getPreferredColorScheme() {
|
|
return preferredColorScheme;
|
|
}
|
|
|
|
/**
|
|
* @param {Node} node
|
|
*/
|
|
export function getTag(node) {
|
|
return node?.nodeName?.toLowerCase() || "";
|
|
}
|
|
|
|
/**
|
|
* @template {(...args: any[]) => any} T
|
|
* @param {InteractionType} type
|
|
* @param {T} fn
|
|
* @returns {T & {
|
|
* as: (name: string) => T;
|
|
* readonly silent: T;
|
|
* }}
|
|
*/
|
|
export function interactor(type, fn) {
|
|
return $assign(makeInteractorFn(type, fn, fn.name), {
|
|
as(alias) {
|
|
return makeInteractorFn(type, fn, fn.name, alias);
|
|
},
|
|
get silent() {
|
|
return fn;
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
export function isFirefox() {
|
|
return /firefox/i.test($userAgent);
|
|
}
|
|
|
|
/**
|
|
* Cross-realm equivalent to 'instanceof'.
|
|
* Can be called with multiple constructors, and will return true if the given object
|
|
* is an instance of any of them.
|
|
*
|
|
* @param {unknown} instance
|
|
* @param {...{ name: string }} classes
|
|
*/
|
|
export function isInstanceOf(instance, ...classes) {
|
|
if (!classes.length) {
|
|
return instance instanceof classes[0];
|
|
}
|
|
if (!instance || Object(instance) !== instance) {
|
|
// Object is falsy or a primitive (null, undefined and primitives cannot be the instance of anything)
|
|
return false;
|
|
}
|
|
for (const cls of classes) {
|
|
if (instance instanceof cls) {
|
|
return true;
|
|
}
|
|
const targetName = cls.name;
|
|
if (!targetName) {
|
|
return false;
|
|
}
|
|
if (targetName === "Array") {
|
|
return $isArray(instance);
|
|
}
|
|
if (targetName === "Error") {
|
|
return isError(instance);
|
|
}
|
|
if ($toString.call(instance) === `[object ${targetName}]`) {
|
|
return true;
|
|
}
|
|
let { constructor } = instance;
|
|
while (constructor) {
|
|
if (constructor.name === targetName) {
|
|
return true;
|
|
}
|
|
constructor = $getPrototypeOf(constructor);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given object is iterable (*excluding strings*).
|
|
*
|
|
* @template T
|
|
* @template {T | Iterable<T>} V
|
|
* @param {V} object
|
|
* @returns {V extends Iterable<T> ? true : false}
|
|
*/
|
|
export function isIterable(object) {
|
|
return !!(object && typeof object === "object" && object[Symbol.iterator]);
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @param {{ safe?: boolean }} [options]
|
|
* @returns {string | RegExp}
|
|
*/
|
|
export function parseRegExp(value, options) {
|
|
const regexParams = value.match(R_REGEX);
|
|
if (regexParams) {
|
|
const unified = regexParams[1].replace(R_WHITE_SPACE, "\\s+");
|
|
const flag = regexParams[2];
|
|
try {
|
|
return new RegExp(unified, flag);
|
|
} catch (error) {
|
|
if (isInstanceOf(error, SyntaxError) && options?.safe) {
|
|
return value;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* @param {Node} node
|
|
* @param {{ raw?: boolean }} [options]
|
|
*/
|
|
export function toSelector(node, options) {
|
|
const tagName = getTag(node);
|
|
const id = node.id ? `#${node.id}` : "";
|
|
const classNames = node.classList
|
|
? [...node.classList].map((className) => `.${className}`)
|
|
: [];
|
|
if (options?.raw) {
|
|
return { tagName, id, classNames };
|
|
} else {
|
|
return [tagName, id, ...classNames].join("");
|
|
}
|
|
}
|
|
|
|
export class HootDebugHelpers {
|
|
/**
|
|
* @param {...any} helpers
|
|
*/
|
|
constructor(...helpers) {
|
|
$assign(this, ...helpers);
|
|
}
|
|
}
|
|
|
|
export const REGEX_MARKER = "/";
|
|
|
|
// Common regular expressions
|
|
export const R_REGEX = new RegExp(`^${REGEX_MARKER}(.*)${REGEX_MARKER}([dgimsuvy]+)?$`);
|
|
export const R_WHITE_SPACE = /\s+/g;
|