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;
}
|