mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 02:52:04 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue