mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 11:32:00 +02:00
vanilla 18.0
This commit is contained in:
parent
5454004ff9
commit
d7f6d2725e
979 changed files with 428093 additions and 0 deletions
2144
odoo-bringout-oca-ocb-web/web/static/lib/hoot-dom/helpers/dom.js
Normal file
2144
odoo-bringout-oca-ocb-web/web/static/lib/hoot-dom/helpers/dom.js
Normal file
File diff suppressed because it is too large
Load diff
2937
odoo-bringout-oca-ocb-web/web/static/lib/hoot-dom/helpers/events.js
Normal file
2937
odoo-bringout-oca-ocb-web/web/static/lib/hoot-dom/helpers/events.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,495 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { isInstanceOf } from "../hoot_dom_utils";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* animationFrame?: boolean;
|
||||
* blockTimers?: boolean;
|
||||
* }} AdvanceTimeOptions
|
||||
*
|
||||
* @typedef {{
|
||||
* message?: string | () => string;
|
||||
* timeout?: number;
|
||||
* }} WaitOptions
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Global
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
cancelAnimationFrame,
|
||||
clearInterval,
|
||||
clearTimeout,
|
||||
Error,
|
||||
Math: { ceil: $ceil, floor: $floor, max: $max, min: $min },
|
||||
Number,
|
||||
performance,
|
||||
Promise,
|
||||
requestAnimationFrame,
|
||||
setInterval,
|
||||
setTimeout,
|
||||
} = globalThis;
|
||||
/** @type {Performance["now"]} */
|
||||
const $performanceNow = performance.now.bind(performance);
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Internal
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
function animationToId(id) {
|
||||
return ID_PREFIX.animation + String(id);
|
||||
}
|
||||
|
||||
function getNextTimerValues() {
|
||||
/** @type {[number, () => any, string] | null} */
|
||||
let timerValues = null;
|
||||
for (const [internalId, [callback, init, delay]] of timers.entries()) {
|
||||
const timeout = init + delay;
|
||||
if (!timerValues || timeout < timerValues[0]) {
|
||||
timerValues = [timeout, callback, internalId];
|
||||
}
|
||||
}
|
||||
return timerValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function idToAnimation(id) {
|
||||
return Number(id.slice(ID_PREFIX.animation.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function idToInterval(id) {
|
||||
return Number(id.slice(ID_PREFIX.interval.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function idToTimeout(id) {
|
||||
return Number(id.slice(ID_PREFIX.timeout.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
function intervalToId(id) {
|
||||
return ID_PREFIX.interval + String(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given value to a **natural number** (or 0 if failing to do so).
|
||||
*
|
||||
* @param {unknown} value
|
||||
*/
|
||||
function parseNat(value) {
|
||||
return $max($floor(Number(value)), 0) || 0;
|
||||
}
|
||||
|
||||
function now() {
|
||||
return (frozen ? 0 : $performanceNow()) + timeOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} id
|
||||
*/
|
||||
function timeoutToId(id) {
|
||||
return ID_PREFIX.timeout + String(id);
|
||||
}
|
||||
|
||||
class HootTimingError extends Error {
|
||||
name = "HootTimingError";
|
||||
}
|
||||
|
||||
const ID_PREFIX = {
|
||||
animation: "a_",
|
||||
interval: "i_",
|
||||
timeout: "t_",
|
||||
};
|
||||
|
||||
/** @type {Map<string, [() => any, number, number]>} */
|
||||
const timers = new Map();
|
||||
|
||||
let allowTimers = false;
|
||||
let frozen = false;
|
||||
let frameDelay = 1000 / 60;
|
||||
let nextDummyId = 1;
|
||||
let timeOffset = 0;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {number} [frameCount]
|
||||
* @param {AdvanceTimeOptions} [options]
|
||||
*/
|
||||
export function advanceFrame(frameCount, options) {
|
||||
return advanceTime(frameDelay * parseNat(frameCount), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the current time by the given amount of milliseconds. This will
|
||||
* affect all timeouts, intervals, animations and date objects.
|
||||
*
|
||||
* It returns a promise resolved after all related callbacks have been executed.
|
||||
*
|
||||
* @param {number} ms
|
||||
* @param {AdvanceTimeOptions} [options]
|
||||
* @returns {Promise<number>} time consumed by timers (in ms).
|
||||
*/
|
||||
export async function advanceTime(ms, options) {
|
||||
ms = parseNat(ms);
|
||||
|
||||
if (options?.blockTimers) {
|
||||
allowTimers = false;
|
||||
}
|
||||
|
||||
const targetTime = now() + ms;
|
||||
let remaining = ms;
|
||||
/** @type {ReturnType<typeof getNextTimerValues>} */
|
||||
let timerValues;
|
||||
while ((timerValues = getNextTimerValues()) && timerValues[0] <= targetTime) {
|
||||
const [timeout, handler, id] = timerValues;
|
||||
const diff = timeout - now();
|
||||
if (diff > 0) {
|
||||
timeOffset += $min(remaining, diff);
|
||||
remaining = $max(remaining - diff, 0);
|
||||
}
|
||||
if (timers.has(id)) {
|
||||
handler(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining > 0) {
|
||||
timeOffset += remaining;
|
||||
}
|
||||
|
||||
if (options?.animationFrame ?? true) {
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
allowTimers = true;
|
||||
|
||||
return ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise resolved after the next animation frame, typically allowing
|
||||
* Owl components to render.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function animationFrame() {
|
||||
return new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all current timeouts, intervals and animations.
|
||||
*/
|
||||
export function cancelAllTimers() {
|
||||
for (const id of timers.keys()) {
|
||||
if (id.startsWith(ID_PREFIX.animation)) {
|
||||
globalThis.cancelAnimationFrame(idToAnimation(id));
|
||||
} else if (id.startsWith(ID_PREFIX.interval)) {
|
||||
globalThis.clearInterval(idToInterval(id));
|
||||
} else if (id.startsWith(ID_PREFIX.timeout)) {
|
||||
globalThis.clearTimeout(idToTimeout(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupTime() {
|
||||
allowTimers = false;
|
||||
frozen = false;
|
||||
|
||||
cancelAllTimers();
|
||||
|
||||
// Wait for remaining async code to run
|
||||
return delay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise resolved after a given amount of milliseconds (default to 0).
|
||||
*
|
||||
* @param {number} [duration]
|
||||
* @returns {Promise<void>}
|
||||
* @example
|
||||
* await delay(1000); // waits for 1 second
|
||||
*/
|
||||
export function delay(duration) {
|
||||
return new Promise((resolve) => setTimeout(resolve, duration));
|
||||
}
|
||||
|
||||
export function freezeTime() {
|
||||
frozen = true;
|
||||
}
|
||||
|
||||
export function unfreezeTime() {
|
||||
frozen = false;
|
||||
}
|
||||
|
||||
export function getTimeOffset() {
|
||||
return timeOffset;
|
||||
}
|
||||
|
||||
export function isTimeFrozen() {
|
||||
return frozen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise resolved after the next microtask tick.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function microTick() {
|
||||
return new Promise(queueMicrotask);
|
||||
}
|
||||
|
||||
/** @type {typeof cancelAnimationFrame} */
|
||||
export function mockedCancelAnimationFrame(handle) {
|
||||
if (!frozen) {
|
||||
cancelAnimationFrame(handle);
|
||||
}
|
||||
timers.delete(animationToId(handle));
|
||||
}
|
||||
|
||||
/** @type {typeof clearInterval} */
|
||||
export function mockedClearInterval(intervalId) {
|
||||
if (!frozen) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
timers.delete(intervalToId(intervalId));
|
||||
}
|
||||
|
||||
/** @type {typeof clearTimeout} */
|
||||
export function mockedClearTimeout(timeoutId) {
|
||||
if (!frozen) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timers.delete(timeoutToId(timeoutId));
|
||||
}
|
||||
|
||||
/** @type {typeof requestAnimationFrame} */
|
||||
export function mockedRequestAnimationFrame(callback) {
|
||||
if (!allowTimers) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function handler() {
|
||||
mockedCancelAnimationFrame(handle);
|
||||
return callback(now());
|
||||
}
|
||||
|
||||
const animationValues = [handler, now(), frameDelay];
|
||||
const handle = frozen ? nextDummyId++ : requestAnimationFrame(handler);
|
||||
const internalId = animationToId(handle);
|
||||
timers.set(internalId, animationValues);
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/** @type {typeof setInterval} */
|
||||
export function mockedSetInterval(callback, ms, ...args) {
|
||||
if (!allowTimers) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ms = parseNat(ms);
|
||||
|
||||
function handler() {
|
||||
if (allowTimers) {
|
||||
intervalValues[1] = $max(now(), intervalValues[1] + ms);
|
||||
} else {
|
||||
mockedClearInterval(intervalId);
|
||||
}
|
||||
return callback(...args);
|
||||
}
|
||||
|
||||
const intervalValues = [handler, now(), ms];
|
||||
const intervalId = frozen ? nextDummyId++ : setInterval(handler, ms);
|
||||
const internalId = intervalToId(intervalId);
|
||||
timers.set(internalId, intervalValues);
|
||||
|
||||
return intervalId;
|
||||
}
|
||||
|
||||
/** @type {typeof setTimeout} */
|
||||
export function mockedSetTimeout(callback, ms, ...args) {
|
||||
if (!allowTimers) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ms = parseNat(ms);
|
||||
|
||||
function handler() {
|
||||
mockedClearTimeout(timeoutId);
|
||||
return callback(...args);
|
||||
}
|
||||
|
||||
const timeoutValues = [handler, now(), ms];
|
||||
const timeoutId = frozen ? nextDummyId++ : setTimeout(handler, ms);
|
||||
const internalId = timeoutToId(timeoutId);
|
||||
timers.set(internalId, timeoutValues);
|
||||
|
||||
return timeoutId;
|
||||
}
|
||||
|
||||
export function resetTimeOffset() {
|
||||
timeOffset = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the amount of time needed to run all current timeouts, intervals and
|
||||
* animations, and then advances the current time by that amount.
|
||||
*
|
||||
* @see {@link advanceTime}
|
||||
* @param {AdvanceTimeOptions} [options]
|
||||
* @returns {Promise<number>} time consumed by timers (in ms).
|
||||
*/
|
||||
export function runAllTimers(options) {
|
||||
if (!timers.size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const endts = $max(...[...timers.values()].map(([, init, delay]) => init + delay));
|
||||
return advanceTime($ceil(endts - now()), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current frame rate (in fps) used by animation frames (default to 60fps).
|
||||
*
|
||||
* @param {number} frameRate
|
||||
*/
|
||||
export function setFrameRate(frameRate) {
|
||||
frameRate = parseNat(frameRate);
|
||||
if (frameRate < 1 || frameRate > 1000) {
|
||||
throw new HootTimingError("frame rate must be an number between 1 and 1000");
|
||||
}
|
||||
frameDelay = 1000 / frameRate;
|
||||
}
|
||||
|
||||
export function setupTime() {
|
||||
allowTimers = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise resolved after the next task tick.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function tick() {
|
||||
return delay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise fulfilled when the given `predicate` returns a truthy value,
|
||||
* with the value of the promise being the return value of the `predicate`.
|
||||
*
|
||||
* The `predicate` is run once initially, and then on each animation frame until
|
||||
* it succeeds or fail.
|
||||
*
|
||||
* The promise automatically rejects after a given `timeout` (defaults to 5 seconds).
|
||||
*
|
||||
* @template T
|
||||
* @param {(last: boolean) => T} predicate
|
||||
* @param {WaitOptions} [options]
|
||||
* @returns {Promise<T>}
|
||||
* @example
|
||||
* await waitUntil(() => []); // -> []
|
||||
* @example
|
||||
* const button = await waitUntil(() => queryOne("button:visible"));
|
||||
* button.click();
|
||||
*/
|
||||
export async function waitUntil(predicate, options) {
|
||||
await Promise.resolve();
|
||||
|
||||
// Early check before running the loop
|
||||
const result = predicate(false);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const timeout = $floor(options?.timeout ?? 200);
|
||||
const maxFrameCount = $ceil(timeout / frameDelay);
|
||||
let frameCount = 0;
|
||||
let handle;
|
||||
return new Promise((resolve, reject) => {
|
||||
function runCheck() {
|
||||
const isLast = ++frameCount >= maxFrameCount;
|
||||
const result = predicate(isLast);
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else if (!isLast) {
|
||||
handle = requestAnimationFrame(runCheck);
|
||||
} else {
|
||||
let message =
|
||||
options?.message || `'waitUntil' timed out after %timeout% milliseconds`;
|
||||
if (typeof message === "function") {
|
||||
message = message();
|
||||
}
|
||||
if (isInstanceOf(message, Error)) {
|
||||
reject(message);
|
||||
} else {
|
||||
reject(new HootTimingError(message.replace("%timeout%", String(timeout))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle = requestAnimationFrame(runCheck);
|
||||
}).finally(() => {
|
||||
cancelAnimationFrame(handle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually resolvable and rejectable promise. It introduces 2 new methods:
|
||||
* - {@link reject} rejects the deferred with the given reason;
|
||||
* - {@link resolve} resolves the deferred with the given value.
|
||||
*
|
||||
* @template [T=unknown]
|
||||
*/
|
||||
export class Deferred extends Promise {
|
||||
/** @type {typeof Promise.resolve<T>} */
|
||||
_resolve;
|
||||
/** @type {typeof Promise.reject<T>} */
|
||||
_reject;
|
||||
|
||||
/**
|
||||
* @param {(resolve: (value?: T) => any, reject: (reason?: any) => any) => any} [executor]
|
||||
*/
|
||||
constructor(executor) {
|
||||
let _resolve, _reject;
|
||||
|
||||
super(function deferredResolver(resolve, reject) {
|
||||
_resolve = resolve;
|
||||
_reject = reject;
|
||||
executor?.(_resolve, _reject);
|
||||
});
|
||||
|
||||
this._resolve = _resolve;
|
||||
this._reject = _reject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} [reason]
|
||||
*/
|
||||
async reject(reason) {
|
||||
return this._reject(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} [value]
|
||||
*/
|
||||
async resolve(value) {
|
||||
return this._resolve(value);
|
||||
}
|
||||
}
|
||||
110
odoo-bringout-oca-ocb-web/web/static/lib/hoot-dom/hoot-dom.js
Normal file
110
odoo-bringout-oca-ocb-web/web/static/lib/hoot-dom/hoot-dom.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/** @odoo-module alias=@odoo/hoot-dom default=false */
|
||||
|
||||
import * as dom from "./helpers/dom";
|
||||
import * as events from "./helpers/events";
|
||||
import * as time from "./helpers/time";
|
||||
import { interactor } from "./hoot_dom_utils";
|
||||
|
||||
/**
|
||||
* @typedef {import("./helpers/dom").Dimensions} Dimensions
|
||||
* @typedef {import("./helpers/dom").FormatXmlOptions} FormatXmlOptions
|
||||
* @typedef {import("./helpers/dom").Position} Position
|
||||
* @typedef {import("./helpers/dom").QueryOptions} QueryOptions
|
||||
* @typedef {import("./helpers/dom").QueryRectOptions} QueryRectOptions
|
||||
* @typedef {import("./helpers/dom").QueryTextOptions} QueryTextOptions
|
||||
* @typedef {import("./helpers/dom").Target} Target
|
||||
*
|
||||
* @typedef {import("./helpers/events").DragHelpers} DragHelpers
|
||||
* @typedef {import("./helpers/events").DragOptions} DragOptions
|
||||
* @typedef {import("./helpers/events").EventType} EventType
|
||||
* @typedef {import("./helpers/events").FillOptions} FillOptions
|
||||
* @typedef {import("./helpers/events").InputValue} InputValue
|
||||
* @typedef {import("./helpers/events").KeyStrokes} KeyStrokes
|
||||
* @typedef {import("./helpers/events").PointerOptions} PointerOptions
|
||||
*/
|
||||
|
||||
export {
|
||||
formatXml,
|
||||
getActiveElement,
|
||||
getFocusableElements,
|
||||
getNextFocusableElement,
|
||||
getParentFrame,
|
||||
getPreviousFocusableElement,
|
||||
isDisplayed,
|
||||
isEditable,
|
||||
isFocusable,
|
||||
isInDOM,
|
||||
isInViewPort,
|
||||
isScrollable,
|
||||
isVisible,
|
||||
matches,
|
||||
queryAll,
|
||||
queryAllAttributes,
|
||||
queryAllProperties,
|
||||
queryAllRects,
|
||||
queryAllTexts,
|
||||
queryAllValues,
|
||||
queryAny,
|
||||
queryAttribute,
|
||||
queryFirst,
|
||||
queryOne,
|
||||
queryRect,
|
||||
queryText,
|
||||
queryValue,
|
||||
} from "./helpers/dom";
|
||||
export { on } from "./helpers/events";
|
||||
export {
|
||||
animationFrame,
|
||||
cancelAllTimers,
|
||||
Deferred,
|
||||
delay,
|
||||
freezeTime,
|
||||
unfreezeTime,
|
||||
microTick,
|
||||
setFrameRate,
|
||||
tick,
|
||||
waitUntil,
|
||||
} from "./helpers/time";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Interactors
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// DOM
|
||||
export const observe = interactor("query", dom.observe);
|
||||
export const waitFor = interactor("query", dom.waitFor);
|
||||
export const waitForNone = interactor("query", dom.waitForNone);
|
||||
|
||||
// Events
|
||||
export const check = interactor("interaction", events.check);
|
||||
export const clear = interactor("interaction", events.clear);
|
||||
export const click = interactor("interaction", events.click);
|
||||
export const dblclick = interactor("interaction", events.dblclick);
|
||||
export const drag = interactor("interaction", events.drag);
|
||||
export const edit = interactor("interaction", events.edit);
|
||||
export const fill = interactor("interaction", events.fill);
|
||||
export const hover = interactor("interaction", events.hover);
|
||||
export const keyDown = interactor("interaction", events.keyDown);
|
||||
export const keyUp = interactor("interaction", events.keyUp);
|
||||
export const leave = interactor("interaction", events.leave);
|
||||
export const manuallyDispatchProgrammaticEvent = interactor("interaction", events.dispatch);
|
||||
export const middleClick = interactor("interaction", events.middleClick);
|
||||
export const pointerDown = interactor("interaction", events.pointerDown);
|
||||
export const pointerUp = interactor("interaction", events.pointerUp);
|
||||
export const press = interactor("interaction", events.press);
|
||||
export const resize = interactor("interaction", events.resize);
|
||||
export const rightClick = interactor("interaction", events.rightClick);
|
||||
export const scroll = interactor("interaction", events.scroll);
|
||||
export const select = interactor("interaction", events.select);
|
||||
export const setInputFiles = interactor("interaction", events.setInputFiles);
|
||||
export const setInputRange = interactor("interaction", events.setInputRange);
|
||||
export const uncheck = interactor("interaction", events.uncheck);
|
||||
export const unload = interactor("interaction", events.unload);
|
||||
|
||||
// Time
|
||||
export const advanceFrame = interactor("time", time.advanceFrame);
|
||||
export const advanceTime = interactor("time", time.advanceTime);
|
||||
export const runAllTimers = interactor("time", time.runAllTimers);
|
||||
|
||||
// Debug
|
||||
export { exposeHelpers } from "./hoot_dom_utils";
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* @typedef {ArgumentPrimitive | `${ArgumentPrimitive}[]` | null} ArgumentType
|
||||
*
|
||||
* @typedef {"any"
|
||||
* | "bigint"
|
||||
* | "boolean"
|
||||
* | "error"
|
||||
* | "function"
|
||||
* | "integer"
|
||||
* | "node"
|
||||
* | "number"
|
||||
* | "object"
|
||||
* | "regex"
|
||||
* | "string"
|
||||
* | "symbol"
|
||||
* | "undefined"} ArgumentPrimitive
|
||||
*
|
||||
* @typedef {[string, 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
|
||||
* @returns {T}
|
||||
*/
|
||||
function makeInteractorFn(type, fn, name) {
|
||||
return {
|
||||
[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, args, promiseResult)
|
||||
);
|
||||
} else {
|
||||
return dispatchInteraction(type, name, args, result);
|
||||
}
|
||||
},
|
||||
}[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 {any[]} args
|
||||
* @param {any} returnValue
|
||||
*/
|
||||
export function dispatchInteraction(type, name, args, returnValue) {
|
||||
interactionBus.dispatchEvent(
|
||||
new CustomEvent(type, {
|
||||
detail: [name, 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, 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue