aboutsummaryrefslogtreecommitdiff
path: root/src/util.ts
blob: 587ad660a3318be47a0d902cffde8f6498deac12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/*
 * General utility functions
 */

// ========== For device detection ==========

export type Breakpoint = 'sm' | 'md' | 'lg';

export function getBreakpoint(): Breakpoint {
	const w = window.innerWidth;
	if (w < 768){
		return 'sm';
	} else if (w < 1024){
		return 'md';
	} else {
		return 'lg';
	}
}

// Returns true for a touch device
export function onTouchDevice(){
	return window.matchMedia('(pointer: coarse)').matches;
}

// ========== For handler throttling ==========

// For creating throttled version of handler function
export function makeThrottled(hdlr: (...args: any[]) => void, delay: number){
	let timeout = 0;
	return (...args: any[]) => {
		clearTimeout(timeout);
		timeout = window.setTimeout(async () => hdlr(...args), delay);
	};
}

// Like makeThrottled(), but accepts an async function
export function makeThrottledAsync(hdlr: (...args: any[]) => Promise<void>, delay: number){
	let timeout = 0;
	return async (...args: any[]) => {
		clearTimeout(timeout);
		timeout = window.setTimeout(async () => await hdlr(...args), delay);
	};
}

// Like makeThrottled(), but, for runs of fast handler calls, calls it at spaced intervals, and at the start/end
export function makeThrottledSpaced(hdlr: (...args: any[]) => void, delay: number){
	let lastHdlrTime = 0; // Used for throttling
	let endHdlr = 0; // Used to call handler after ending a run of calls
	return (...args: any[]) => {
		clearTimeout(endHdlr);
		const currentTime = new Date().getTime();
		if (currentTime - lastHdlrTime > delay){
			lastHdlrTime = currentTime;
			hdlr(...args);
			lastHdlrTime = new Date().getTime();
		} else {
			endHdlr = window.setTimeout(() => {
				endHdlr = 0;
				hdlr(...args);
				lastHdlrTime = new Date().getTime();
			}, delay);
		}
	};
}

// ========== Other ==========

// Similar to %, but for negative LHS, return a positive offset from a lower RHS multiple
export function moduloPositive(x: number, y: number){
	return x - Math.floor(x / y) * y;
}

// For positive int n, converts 1 to '1st', 2 to '2nd', etc
export function intToOrdinal(n: number){
	if (n == 1 || n > 20 && n % 10 == 1){
		return `${n == 1 ? '' : Math.floor(n / 10)}1st`;
	} else if (n == 2 || n > 20 && n % 10 == 2){
		return `${n == 2 ? '' : Math.floor(n / 10)}2nd`;
	} else if (n == 3 || n > 20 && n % 10 == 3){
		return `${n == 3 ? '' : Math.floor(n / 10)}3rd`;
	} else {
		return String(n) + 'th';
	}
}

// For positive int n, returns number of trailing zeros in decimal representation
export function getNumTrailingZeros(n: number): number {
	let pow10 = 10;
	while (pow10 != Infinity){
		if (n % pow10 != 0){
			return Math.log10(pow10 / 10);
		}
		pow10 *= 10;
	}
	throw new Error('Exceeded floating point precision');
}

// Removes a class from an element, triggers reflow, then adds the class
export function animateWithClass(el: HTMLElement, className: string){
	el.classList.remove(className);
	el.offsetWidth; // Triggers reflow
	el.classList.add(className);
}

// Used to async-await for until after a timeout
export async function timeout(ms: number){
	return new Promise(resolve => setTimeout(resolve, ms))
}

// For estimating text width (via https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript)
const _getTextWidthCanvas = document.createElement('canvas');
export function getTextWidth(text: string, font: string): number {
	const context = _getTextWidthCanvas.getContext('2d')!;
	context.font = font;
	const metrics = context.measureText(text);
	return metrics.width;
}