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.
 
 
 
 

164 lines
3.9 KiB

import React, { useState } 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<Fn extends (...args: any[]) => void>(
func: Fn,
wait: number,
): Fn {
let timeoutId: number | null = null;
return function (this: ThisParameterType<Fn>, ...args: Parameters<Fn>) {
if (timeoutId !== null) clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, wait);
} as Fn;
}
export const throttle = <Fn extends (...args: any[]) => void>(
fn: Fn,
wait: number,
): Fn => {
let inThrottle = false;
let lastFn: ReturnType<typeof setTimeout> | undefined = undefined;
let lastTime = 0;
return function (this: ThisParameterType<Fn>, ...args: Parameters<Fn>) {
const context = this;
if (!inThrottle) {
fn.apply(context, args);
lastTime = Date.now();
inThrottle = true;
} else {
clearTimeout(lastFn);
lastFn = setTimeout(function () {
if (Date.now() - lastTime >= wait) {
fn.apply(context, args);
lastTime = Date.now();
}
}, Math.max(wait - (Date.now() - lastTime), 0));
}
} as Fn;
};
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);
}
// 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;
}
export interface Toggle {
on: boolean;
toggle: () => void;
set: (value: boolean) => void;
}
export function useToggle(initial: boolean): Toggle {
const [state, setState] = useState(initial);
return {
on: state,
toggle: () => setState(s => !s),
set: (value: boolean) => setState(value),
};
}