You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

133 lines
3.1 KiB

import React from "react";
import useLocation from "wouter/use-location";
export const sleep = (t: number) => new Promise(r => setTimeout(r, t));
type Ref<T> =
| React.MutableRefObject<T | null>
| React.RefCallback<T | null>
| React.ForwardedRef<T>;
export const composeRefs = <T>(...refs: Ref<T | null>[]) => {
return (el: T) => {
refs.forEach(ref => {
if (typeof ref === "function") ref(el);
else if (ref) ref.current = el;
});
};
};
export function* intersperse<T, U>(
xs: T[],
delim: (index: number) => U,
): Generator<T | U> {
let first = true;
let i = 0;
for (const x of xs) {
if (!first) {
yield delim(i);
i++;
}
first = false;
yield x;
i++;
}
}
export const getTimeout = () => {
const clearables = new Set<ReturnType<typeof setTimeout>>();
const timeout = (f: (...attr: any[]) => any, t: number) => {
const self = setTimeout(() => (f(), clearables.delete(self)), t);
clearables.add(self);
};
const clearTimers = () => {
clearables.forEach(timer => clearTimeout(timer));
clearables.clear();
};
return [timeout, clearTimers] as const;
};
export function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number,
): T {
let timeoutId: number | null = null;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (timeoutId !== null) clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, wait);
} as T;
}
export const ellipses = (text: string, length: number = 100) =>
text.length > length ? text.slice(0, length - 3) + "..." : text;
export const useNav = () => {
const [location, navigate] = useLocation();
return [
location,
(link: string) => (e: React.MouseEvent | KeyboardEvent) => {
e?.preventDefault();
if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener");
navigate(link);
},
] as const;
};
export function rewriteExtn(filename: string, extn: string) {
const split = filename.split(".");
split[split.length - 1] = extn;
return split.join(".");
}
export function normalise(path: string) {
return (
(path.startsWith("/") ? "/" : "") +
path.trim().split("/").filter(Boolean).join("/")
);
}
export function comparePaths(p1: string, p2: string) {
return normalise(p1) === normalise(p2);
}
export const get = {
next<X>(xs: X[], i: number) {
return xs.at((i + 1) % xs.length)!;
},
prev<X>(xs: X[], i: number) {
return xs.at((i - 1) % xs.length)!;
},
};
// required css is inlined in index.html
export function setupCursorTracking(el: HTMLElement | null) {
if (!el) return;
el.addEventListener("mousemove", e => {
const rect = el.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
el.style.setProperty("--x", x + "px");
el.style.setProperty("--y", y + "px");
});
}
export function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
export function normaliseAngleDifference(delta: number): number {
// Bring into range (-2PI, 2PI)
delta = delta % (2 * Math.PI);
if (delta > Math.PI) delta -= 2 * Math.PI;
else if (delta <= -Math.PI) delta += 2 * Math.PI;
return delta;
}