-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommon.ts
More file actions
187 lines (169 loc) · 4.9 KB
/
Copy pathcommon.ts
File metadata and controls
187 lines (169 loc) · 4.9 KB
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/*
* Common JS utilities.
*/
/**
* A Webpack variable.
* true iff webpack mode is production, not development.
*/
export declare const PRODUCTION : boolean;
/**
* Log args to console unless in production mode.
*
* @param args the arguments for console.log
*/
export const debug = (
PRODUCTION
? (...args: any[]): void => {}
: function debug(...args: any[]): void {
console.log('%c[Debug]', 'font-weight: bold;', ...args);
}
);
/**
* Always throws an Error with given message.
*
* Use like so:
* const x: number = foo() ?? error('foo() is null');
*
* @throws {Error} always
*/
export function error(message: string): never {
throw new Error(message);
}
/**
* Always throws an Error.
*
* Use like so:
* const x: number = foo() ?? die(); // foo() should never return null
*
* @throws {Error} always
*/
export function die(): never {
throw new Error('Assertion failed');
}
/**
* Get an HTMLElement by its ID or throw an Error.
*
* @param id the ID to find
*
* @returns the element
* @throws {Error} when element is not found
*/
export function getElementByIdOrDie(id: string): HTMLElement {
return document.getElementById(id) ?? error(`#${id} not found`);
}
/**
* Get an HTMLElement child of within by its ID or throw an Error.
* Works on detached elements.
*
* @param within the element to look in
* @param id the ID to find
*
* @returns the element
* @throws {Error} when element is not found
*/
export function getChildById(
within: HTMLElement,
id: string
): HTMLElement {
return (within.querySelector('#' + id)
?? error(`#${id} not found in ${within}`));
}
/**
* Cache of loaded injections by former element ID.
*/
const loadedInjections = new Map<string, any>();
/**
* Get an injected value by its ID.
*
* Injected values are usually created within Django templates using
* {{ somevar|json_script:"injection-id" }}.
*
* Injected values are loaded once from DOM, after which the element containing
* the injection is removed to reduce RAM usage. By default the value is
* instead cached in a Map, but function parameter cache can be used to disable
* caching and destroy the value forever.
*
* @param id the ID of the injection element
* @param cache when set to false, injection data is deleted permanently
*
* @return the value loaded from DOM or cache
*/
export function getInjection<T>(id: string, cache: boolean = true): T {
if (loadedInjections.has(id)) {
return loadedInjections.get(id) as T;
}
const element = getElementByIdOrDie(id);
if (!(element instanceof HTMLScriptElement)) {
throw new Error('Injection must be a script element');
}
if (element.type !== "application/json") {
throw new Error('Injection must have type="application/json"');
}
const value = JSON.parse(element.textContent
?? error('Injection has no textContent'));
element.remove();
if (cache) {
loadedInjections.set(id, value);
}
return value as T;
}
/**
* Formats given Date object, e.g. 2023-02-28 11:39. Local timezone is used.
*
* @param ts the date to format
* @returns the formatted date
*/
export function formatTimestamp(ts: Date): string {
const pad = function(obj: any, minLength: number): string {
let str = '' + obj;
while (str.length < minLength) {
str = '0' + str;
}
return str;
};
return pad(ts.getFullYear(), 4) + '-' +
pad(ts.getMonth() + 1, 2) + '-' +
pad(ts.getDate(), 2) + ' ' +
pad(ts.getHours(), 2) + ':' +
pad(ts.getMinutes(), 2);
}
/**
* Ensure that only certain CSS classes out of a family are present.
*
* A family includes all classes that begin with prefix.
*
* Example:
*
* theDiv = <div class="foo-cat bar-egg ham foo-fish">
* setClasses(theDiv, 'foo-', ['cat', 'foo-dog']);
* // theDiv now has class="foo-cat bar-egg ham foo-dog"
*
* @param element the element to modify
* @param prefix the prefix to manage
* @param allowed CSS class or classes to retain or add. prefix is prepended to
* classes do not start with it.
*/
export function setClasses(
element: HTMLElement,
prefix: string,
allowed: string | Iterable<string>,
): void {
const desiredClasses =
((typeof allowed === 'string') ? [allowed] : Array.from(allowed))
.map((s) => s.startsWith(prefix) ? s : prefix + s);
const currentClasses =
Array.from(element.classList)
.filter((s) => s.startsWith(prefix));
// Remove classes that are no longer needed
for (const currentClass of currentClasses) {
if (!desiredClasses.includes(currentClass)) {
element.classList.remove(currentClass);
}
}
// Add missing classes
for (const desiredClass of desiredClasses) {
if (!currentClasses.includes(desiredClass)) {
element.classList.add(desiredClass);
}
}
}