tgui-core 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/common/assets.ts +38 -0
- package/lib/common/collections.ts +27 -0
- package/lib/common/color.ts +88 -0
- package/lib/common/constants.ts +349 -0
- package/lib/common/events.ts +262 -0
- package/{dist/common/exhaustive.d.ts → lib/common/exhaustive.ts} +3 -1
- package/lib/common/format.ts +167 -0
- package/{dist/common/fp.d.ts → lib/common/fp.ts} +16 -2
- package/lib/common/hotkeys.ts +207 -0
- package/lib/common/http.ts +16 -0
- package/lib/common/keycodes.ts +86 -0
- package/{dist/common/keys.d.ts → lib/common/keys.ts} +24 -21
- package/lib/common/math.ts +76 -0
- package/lib/common/perf.ts +72 -0
- package/lib/common/random.ts +32 -0
- package/lib/common/react.ts +59 -0
- package/lib/common/redux.ts +187 -0
- package/lib/common/storage.ts +207 -0
- package/lib/common/string.ts +169 -0
- package/lib/common/timer.ts +63 -0
- package/lib/common/type-utils.ts +41 -0
- package/lib/common/types.d.ts +12 -0
- package/lib/common/uuid.ts +18 -0
- package/lib/components/AnimatedNumber.tsx +180 -0
- package/lib/components/Autofocus.tsx +23 -0
- package/lib/components/Blink.tsx +91 -0
- package/lib/components/BlockQuote.tsx +9 -0
- package/lib/components/BodyZoneSelector.tsx +149 -0
- package/lib/components/Box.tsx +252 -0
- package/lib/components/Button.tsx +425 -0
- package/lib/components/ByondUi.jsx +110 -0
- package/lib/components/Chart.tsx +155 -0
- package/lib/components/Collapsible.tsx +43 -0
- package/lib/components/ColorBox.tsx +29 -0
- package/lib/components/Dialog.tsx +81 -0
- package/lib/components/Dimmer.tsx +13 -0
- package/lib/components/Divider.tsx +20 -0
- package/lib/components/DmIcon.tsx +86 -0
- package/lib/components/DraggableControl.jsx +276 -0
- package/lib/components/Dropdown.tsx +246 -0
- package/lib/components/FakeTerminal.jsx +52 -0
- package/lib/components/FitText.tsx +99 -0
- package/lib/components/Flex.tsx +159 -0
- package/lib/components/Icon.tsx +95 -0
- package/lib/components/Image.tsx +54 -0
- package/lib/components/InfinitePlane.jsx +192 -0
- package/lib/components/Input.tsx +176 -0
- package/lib/components/KeyListener.tsx +40 -0
- package/lib/components/Knob.tsx +178 -0
- package/lib/components/LabeledControls.tsx +44 -0
- package/lib/components/LabeledList.tsx +154 -0
- package/lib/components/MenuBar.tsx +228 -0
- package/lib/components/Modal.tsx +23 -0
- package/lib/components/NoticeBox.tsx +45 -0
- package/lib/components/NumberInput.tsx +328 -0
- package/lib/components/Popper.tsx +100 -0
- package/lib/components/ProgressBar.tsx +105 -0
- package/lib/components/RestrictedInput.jsx +301 -0
- package/lib/components/RoundGauge.tsx +180 -0
- package/lib/components/Section.tsx +120 -0
- package/lib/components/Slider.tsx +169 -0
- package/lib/components/Stack.tsx +96 -0
- package/lib/components/StyleableSection.tsx +33 -0
- package/lib/components/Table.tsx +84 -0
- package/lib/components/Tabs.tsx +89 -0
- package/lib/components/TextArea.tsx +182 -0
- package/lib/components/TimeDisplay.jsx +64 -0
- package/lib/components/Tooltip.tsx +152 -0
- package/lib/components/TrackOutsideClicks.tsx +35 -0
- package/lib/components/VirtualList.tsx +69 -0
- package/lib/styles/atomic/candystripe.scss +8 -0
- package/lib/styles/atomic/centered-image.scss +7 -0
- package/lib/styles/atomic/color.scss +21 -0
- package/lib/styles/atomic/debug-layout.scss +17 -0
- package/lib/styles/atomic/fit-text.scss +14 -0
- package/lib/styles/atomic/links.scss +12 -0
- package/lib/styles/atomic/outline.scss +47 -0
- package/lib/styles/atomic/text.scss +44 -0
- package/lib/styles/base.scss +32 -0
- package/lib/styles/colors.scss +92 -0
- package/lib/styles/components/BlockQuote.module.scss +20 -0
- package/lib/styles/components/BlockQuote.module.scss.d.ts +4 -0
- package/lib/styles/components/Button.module.scss +157 -0
- package/lib/styles/components/Button.module.scss.d.ts +46 -0
- package/lib/styles/components/ColorBox.module.scss +12 -0
- package/lib/styles/components/ColorBox.module.scss.d.ts +4 -0
- package/lib/styles/components/Dialog.module.scss +60 -0
- package/lib/styles/components/Dialog.module.scss.d.ts +10 -0
- package/lib/styles/components/Dimmer.module.scss +22 -0
- package/lib/styles/components/Dimmer.module.scss.d.ts +4 -0
- package/lib/styles/components/Divider.module.scss +27 -0
- package/lib/styles/components/Divider.module.scss.d.ts +6 -0
- package/lib/styles/components/Dropdown.scss +72 -0
- package/lib/styles/components/Flex.module.scss +13 -0
- package/lib/styles/components/Flex.module.scss.d.ts +5 -0
- package/lib/styles/components/Icon.module.scss +25 -0
- package/lib/styles/components/Icon.module.scss.d.ts +5 -0
- package/lib/styles/components/Input.module.scss +64 -0
- package/lib/styles/components/Input.module.scss.d.ts +8 -0
- package/lib/styles/components/Knob.module.scss +131 -0
- package/lib/styles/components/Knob.module.scss.d.ts +33 -0
- package/lib/styles/components/LabeledList.module.scss +49 -0
- package/lib/styles/components/LabeledList.module.scss.d.ts +8 -0
- package/lib/styles/components/MenuBar.module.scss +75 -0
- package/lib/styles/components/MenuBar.module.scss.d.ts +14 -0
- package/lib/styles/components/Modal.module.scss +14 -0
- package/lib/styles/components/Modal.module.scss.d.ts +4 -0
- package/lib/styles/components/NoticeBox.module.scss +65 -0
- package/lib/styles/components/NoticeBox.module.scss.d.ts +27 -0
- package/lib/styles/components/NumberInput.module.scss +71 -0
- package/lib/styles/components/NumberInput.module.scss.d.ts +9 -0
- package/lib/styles/components/ProgressBar.module.scss +63 -0
- package/lib/styles/components/ProgressBar.module.scss.d.ts +27 -0
- package/lib/styles/components/RoundGauge.module.scss +85 -0
- package/lib/styles/components/RoundGauge.module.scss.d.ts +49 -0
- package/lib/styles/components/Section.module.scss +130 -0
- package/lib/styles/components/Section.module.scss.d.ts +13 -0
- package/lib/styles/components/Slider.module.scss +54 -0
- package/lib/styles/components/Slider.module.scss.d.ts +8 -0
- package/lib/styles/components/Stack.module.scss +60 -0
- package/lib/styles/components/Stack.module.scss.d.ts +12 -0
- package/lib/styles/components/Table.module.scss +44 -0
- package/lib/styles/components/Table.module.scss.d.ts +10 -0
- package/lib/styles/components/Tabs.module.scss +144 -0
- package/lib/styles/components/Tabs.module.scss.d.ts +35 -0
- package/lib/styles/components/TextArea.module.scss +86 -0
- package/lib/styles/components/TextArea.module.scss.d.ts +11 -0
- package/lib/styles/components/Tooltip.module.scss +24 -0
- package/lib/styles/components/Tooltip.module.scss.d.ts +4 -0
- package/lib/styles/functions.scss +79 -0
- package/lib/styles/input.scss +9 -0
- package/lib/styles/main.scss +20 -0
- package/lib/styles/reset.scss +68 -0
- package/package.json +6 -6
- package/dist/ProgressBar.module-BkAFfFy0.js +0 -29
- package/dist/Section.module-CLVHJ4yA.js +0 -15
- package/dist/assets/BlockQuote.css +0 -1
- package/dist/assets/Button.css +0 -1
- package/dist/assets/ColorBox.css +0 -1
- package/dist/assets/Dialog.css +0 -1
- package/dist/assets/Dimmer.css +0 -1
- package/dist/assets/Divider.css +0 -1
- package/dist/assets/Flex.css +0 -1
- package/dist/assets/Icon.css +0 -6
- package/dist/assets/Input.css +0 -1
- package/dist/assets/Knob.css +0 -1
- package/dist/assets/LabeledList.css +0 -1
- package/dist/assets/MenuBar.css +0 -1
- package/dist/assets/Modal.css +0 -1
- package/dist/assets/NoticeBox.css +0 -1
- package/dist/assets/NumberInput.css +0 -1
- package/dist/assets/ProgressBar.css +0 -1
- package/dist/assets/RoundGauge.css +0 -1
- package/dist/assets/Section.css +0 -1
- package/dist/assets/Slider.css +0 -1
- package/dist/assets/Stack.css +0 -1
- package/dist/assets/Table.css +0 -1
- package/dist/assets/Tabs.css +0 -1
- package/dist/assets/TextArea.css +0 -1
- package/dist/assets/Tooltip.css +0 -1
- package/dist/common/assets.d.ts +0 -4
- package/dist/common/assets.js +0 -21
- package/dist/common/collections.d.ts +0 -10
- package/dist/common/collections.js +0 -15
- package/dist/common/color.d.ts +0 -25
- package/dist/common/color.js +0 -69
- package/dist/common/constants.d.ts +0 -102
- package/dist/common/constants.js +0 -312
- package/dist/common/events.d.ts +0 -33
- package/dist/common/events.js +0 -147
- package/dist/common/exhaustive.js +0 -6
- package/dist/common/format.d.ts +0 -11
- package/dist/common/format.js +0 -114
- package/dist/common/fp.js +0 -9
- package/dist/common/hotkeys.d.ts +0 -25
- package/dist/common/hotkeys.js +0 -112
- package/dist/common/http.d.ts +0 -4
- package/dist/common/http.js +0 -10
- package/dist/common/keycodes.d.ts +0 -85
- package/dist/common/keycodes.js +0 -88
- package/dist/common/keys.js +0 -8
- package/dist/common/math.d.ts +0 -39
- package/dist/common/math.js +0 -41
- package/dist/common/perf.d.ts +0 -24
- package/dist/common/perf.js +0 -33
- package/dist/common/random.d.ts +0 -16
- package/dist/common/random.js +0 -18
- package/dist/common/react.d.ts +0 -23
- package/dist/common/react.js +0 -30
- package/dist/common/redux.d.ts +0 -64
- package/dist/common/redux.js +0 -72
- package/dist/common/storage.js +0 -124
- package/dist/common/string.d.ts +0 -65
- package/dist/common/string.js +0 -83
- package/dist/common/timer.d.ts +0 -18
- package/dist/common/timer.js +0 -28
- package/dist/common/type-utils.d.ts +0 -9
- package/dist/common/type-utils.js +0 -25
- package/dist/common/uuid.d.ts +0 -9
- package/dist/common/uuid.js +0 -10
- package/dist/components/AnimatedNumber.d.ts +0 -60
- package/dist/components/AnimatedNumber.js +0 -76
- package/dist/components/Autofocus.d.ts +0 -4
- package/dist/components/Autofocus.js +0 -17
- package/dist/components/Blink.d.ts +0 -26
- package/dist/components/Blink.js +0 -56
- package/dist/components/BlockQuote.d.ts +0 -3
- package/dist/components/BlockQuote.js +0 -13
- package/dist/components/BodyZoneSelector.d.ts +0 -28
- package/dist/components/BodyZoneSelector.js +0 -115
- package/dist/components/Box.d.ts +0 -91
- package/dist/components/Box.js +0 -133
- package/dist/components/Button.d.ts +0 -93
- package/dist/components/Button.js +0 -298
- package/dist/components/ByondUi.js +0 -73
- package/dist/components/Chart.d.ts +0 -28
- package/dist/components/Chart.js +0 -95
- package/dist/components/Collapsible.d.ts +0 -15
- package/dist/components/Collapsible.js +0 -27
- package/dist/components/ColorBox.d.ts +0 -8
- package/dist/components/ColorBox.js +0 -24
- package/dist/components/Dialog.d.ts +0 -24
- package/dist/components/Dialog.js +0 -67
- package/dist/components/Dimmer.d.ts +0 -3
- package/dist/components/Dimmer.js +0 -13
- package/dist/components/Divider.d.ts +0 -6
- package/dist/components/Divider.js +0 -22
- package/dist/components/DmIcon.d.ts +0 -31
- package/dist/components/DmIcon.js +0 -31
- package/dist/components/DraggableControl.js +0 -176
- package/dist/components/Dropdown.d.ts +0 -48
- package/dist/components/Dropdown.js +0 -152
- package/dist/components/FakeTerminal.js +0 -38
- package/dist/components/FitText.d.ts +0 -22
- package/dist/components/FitText.js +0 -63
- package/dist/components/Flex.d.ts +0 -93
- package/dist/components/Flex.js +0 -72
- package/dist/components/Icon.d.ts +0 -30
- package/dist/components/Icon.js +0 -51
- package/dist/components/Image.d.ts +0 -14
- package/dist/components/Image.js +0 -35
- package/dist/components/InfinitePlane.js +0 -139
- package/dist/components/Input.d.ts +0 -61
- package/dist/components/Input.js +0 -89
- package/dist/components/KeyListener.d.ts +0 -15
- package/dist/components/KeyListener.js +0 -23
- package/dist/components/Knob.d.ts +0 -49
- package/dist/components/Knob.js +0 -162
- package/dist/components/LabeledControls.d.ts +0 -11
- package/dist/components/LabeledControls.js +0 -39
- package/dist/components/LabeledList.d.ts +0 -57
- package/dist/components/LabeledList.js +0 -94
- package/dist/components/MenuBar.d.ts +0 -28
- package/dist/components/MenuBar.js +0 -174
- package/dist/components/Modal.d.ts +0 -3
- package/dist/components/Modal.js +0 -25
- package/dist/components/NoticeBox.d.ts +0 -20
- package/dist/components/NoticeBox.js +0 -49
- package/dist/components/NumberInput.d.ts +0 -45
- package/dist/components/NumberInput.js +0 -221
- package/dist/components/Popper.d.ts +0 -27
- package/dist/components/Popper.js +0 -177
- package/dist/components/ProgressBar.d.ts +0 -46
- package/dist/components/ProgressBar.js +0 -37
- package/dist/components/RestrictedInput.js +0 -155
- package/dist/components/RoundGauge.d.ts +0 -53
- package/dist/components/RoundGauge.js +0 -147
- package/dist/components/Section.d.ts +0 -63
- package/dist/components/Section.js +0 -62
- package/dist/components/Slider.d.ts +0 -46
- package/dist/components/Slider.js +0 -124
- package/dist/components/Stack.d.ts +0 -27
- package/dist/components/Stack.js +0 -67
- package/dist/components/StyleableSection.d.ts +0 -11
- package/dist/components/StyleableSection.js +0 -16
- package/dist/components/Table.d.ts +0 -29
- package/dist/components/Table.js +0 -67
- package/dist/components/Tabs.d.ts +0 -23
- package/dist/components/Tabs.js +0 -89
- package/dist/components/TextArea.d.ts +0 -39
- package/dist/components/TextArea.js +0 -118
- package/dist/components/TimeDisplay.js +0 -34
- package/dist/components/Tooltip.d.ts +0 -29
- package/dist/components/Tooltip.js +0 -83
- package/dist/components/TrackOutsideClicks.d.ts +0 -13
- package/dist/components/TrackOutsideClicks.js +0 -24
- package/dist/components/VirtualList.d.ts +0 -8
- package/dist/components/VirtualList.js +0 -34
- package/dist/components/index.js +0 -92
- package/dist/popper-CiqSDJTE.js +0 -906
- /package/{dist/components/index.d.ts → lib/components/index.ts} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { KEY_ALT, KEY_CTRL, KEY_F1, KEY_F12, KEY_SHIFT } from './keycodes';
|
|
2
|
+
|
|
3
|
+
type Fn = (...args: any[]) => void;
|
|
4
|
+
|
|
5
|
+
export class EventEmitter {
|
|
6
|
+
private listeners: Record<string, Fn[]>;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.listeners = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
on(name: string, listener: Fn): void {
|
|
13
|
+
this.listeners[name] = this.listeners[name] || [];
|
|
14
|
+
this.listeners[name].push(listener);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
off(name: string, listener: Fn): void {
|
|
18
|
+
const listeners = this.listeners[name];
|
|
19
|
+
if (!listeners) {
|
|
20
|
+
throw new Error(`There is no listeners for "${name}"`);
|
|
21
|
+
}
|
|
22
|
+
this.listeners[name] = listeners.filter((existingListener) => {
|
|
23
|
+
return existingListener !== listener;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
emit(name: string, ...params: any[]): void {
|
|
28
|
+
const listeners = this.listeners[name];
|
|
29
|
+
if (!listeners) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (let i = 0, len = listeners.length; i < len; i += 1) {
|
|
33
|
+
const listener = listeners[i];
|
|
34
|
+
listener(...params);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clear(): void {
|
|
39
|
+
this.listeners = {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const globalEvents = new EventEmitter();
|
|
44
|
+
let ignoreWindowFocus = false;
|
|
45
|
+
|
|
46
|
+
export const setupGlobalEvents = (
|
|
47
|
+
options: { ignoreWindowFocus?: boolean } = {},
|
|
48
|
+
): void => {
|
|
49
|
+
ignoreWindowFocus = !!options.ignoreWindowFocus;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Window focus
|
|
53
|
+
// --------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
let windowFocusTimeout: ReturnType<typeof setTimeout> | null;
|
|
56
|
+
let windowFocused = true;
|
|
57
|
+
|
|
58
|
+
// Pretend to always be in focus.
|
|
59
|
+
function setWindowFocus(value: boolean, delayed?: boolean) {
|
|
60
|
+
if (ignoreWindowFocus) {
|
|
61
|
+
windowFocused = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (windowFocusTimeout) {
|
|
65
|
+
clearTimeout(windowFocusTimeout);
|
|
66
|
+
windowFocusTimeout = null;
|
|
67
|
+
}
|
|
68
|
+
if (delayed) {
|
|
69
|
+
windowFocusTimeout = setTimeout(() => setWindowFocus(value));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (windowFocused !== value) {
|
|
73
|
+
windowFocused = value;
|
|
74
|
+
globalEvents.emit(value ? 'window-focus' : 'window-blur');
|
|
75
|
+
globalEvents.emit('window-focus-change', value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Focus stealing
|
|
80
|
+
// --------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
let focusStolenBy: HTMLElement | null = null;
|
|
83
|
+
|
|
84
|
+
export function canStealFocus(node: HTMLElement) {
|
|
85
|
+
const tag = String(node.tagName).toLowerCase();
|
|
86
|
+
return tag === 'input' || tag === 'textarea';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stealFocus(node: HTMLElement) {
|
|
90
|
+
releaseStolenFocus();
|
|
91
|
+
focusStolenBy = node;
|
|
92
|
+
focusStolenBy.addEventListener('blur', releaseStolenFocus);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function releaseStolenFocus() {
|
|
96
|
+
if (focusStolenBy) {
|
|
97
|
+
focusStolenBy.removeEventListener('blur', releaseStolenFocus);
|
|
98
|
+
focusStolenBy = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Focus follows the mouse
|
|
103
|
+
// --------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
let focusedNode: HTMLElement | null = null;
|
|
106
|
+
let lastVisitedNode: HTMLElement | null = null;
|
|
107
|
+
const trackedNodes: HTMLElement[] = [];
|
|
108
|
+
|
|
109
|
+
export function addScrollableNode(node: HTMLElement) {
|
|
110
|
+
trackedNodes.push(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function removeScrollableNode(node: HTMLElement) {
|
|
114
|
+
const index = trackedNodes.indexOf(node);
|
|
115
|
+
if (index >= 0) {
|
|
116
|
+
trackedNodes.splice(index, 1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function focusNearestTrackedParent(node: HTMLElement | null) {
|
|
121
|
+
if (focusStolenBy || !windowFocused) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const body = document.body;
|
|
125
|
+
while (node && node !== body) {
|
|
126
|
+
if (trackedNodes.includes(node)) {
|
|
127
|
+
// NOTE: Contains is a DOM4 method
|
|
128
|
+
if (node.contains(focusedNode)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
focusedNode = node;
|
|
132
|
+
node.focus();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
node = node.parentElement;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
window.addEventListener('mousemove', (e) => {
|
|
140
|
+
const node = e.target as HTMLElement;
|
|
141
|
+
if (node !== lastVisitedNode) {
|
|
142
|
+
lastVisitedNode = node;
|
|
143
|
+
focusNearestTrackedParent(node);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Focus event hooks
|
|
148
|
+
// --------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
window.addEventListener('focusin', (e) => {
|
|
151
|
+
lastVisitedNode = null;
|
|
152
|
+
focusedNode = e.target as HTMLElement;
|
|
153
|
+
setWindowFocus(true);
|
|
154
|
+
if (canStealFocus(e.target as HTMLElement)) {
|
|
155
|
+
stealFocus(e.target as HTMLElement);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
window.addEventListener('focusout', () => {
|
|
160
|
+
lastVisitedNode = null;
|
|
161
|
+
setWindowFocus(false, true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
window.addEventListener('blur', () => {
|
|
165
|
+
lastVisitedNode = null;
|
|
166
|
+
setWindowFocus(false, true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
window.addEventListener('beforeunload', () => {
|
|
170
|
+
setWindowFocus(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Key events
|
|
174
|
+
// --------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
const keyHeldByCode: Record<number, boolean> = {};
|
|
177
|
+
|
|
178
|
+
export class KeyEvent {
|
|
179
|
+
event: KeyboardEvent;
|
|
180
|
+
type: 'keydown' | 'keyup';
|
|
181
|
+
code: number;
|
|
182
|
+
ctrl: boolean;
|
|
183
|
+
shift: boolean;
|
|
184
|
+
alt: boolean;
|
|
185
|
+
repeat: boolean;
|
|
186
|
+
_str?: string;
|
|
187
|
+
|
|
188
|
+
constructor(e: KeyboardEvent, type: 'keydown' | 'keyup', repeat?: boolean) {
|
|
189
|
+
this.event = e;
|
|
190
|
+
this.type = type;
|
|
191
|
+
this.code = e.keyCode;
|
|
192
|
+
this.ctrl = e.ctrlKey;
|
|
193
|
+
this.shift = e.shiftKey;
|
|
194
|
+
this.alt = e.altKey;
|
|
195
|
+
this.repeat = !!repeat;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
hasModifierKeys() {
|
|
199
|
+
return this.ctrl || this.alt || this.shift;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
isModifierKey() {
|
|
203
|
+
return (
|
|
204
|
+
this.code === KEY_CTRL || this.code === KEY_SHIFT || this.code === KEY_ALT
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
isDown() {
|
|
209
|
+
return this.type === 'keydown';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
isUp() {
|
|
213
|
+
return this.type === 'keyup';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
toString() {
|
|
217
|
+
if (this._str) {
|
|
218
|
+
return this._str;
|
|
219
|
+
}
|
|
220
|
+
this._str = '';
|
|
221
|
+
if (this.ctrl) {
|
|
222
|
+
this._str += 'Ctrl+';
|
|
223
|
+
}
|
|
224
|
+
if (this.alt) {
|
|
225
|
+
this._str += 'Alt+';
|
|
226
|
+
}
|
|
227
|
+
if (this.shift) {
|
|
228
|
+
this._str += 'Shift+';
|
|
229
|
+
}
|
|
230
|
+
if (this.code >= 48 && this.code <= 90) {
|
|
231
|
+
this._str += String.fromCharCode(this.code);
|
|
232
|
+
} else if (this.code >= KEY_F1 && this.code <= KEY_F12) {
|
|
233
|
+
this._str += 'F' + (this.code - 111);
|
|
234
|
+
} else {
|
|
235
|
+
this._str += '[' + this.code + ']';
|
|
236
|
+
}
|
|
237
|
+
return this._str;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// IE8: Keydown event is only available on document.
|
|
242
|
+
document.addEventListener('keydown', (e) => {
|
|
243
|
+
if (canStealFocus(e.target as HTMLElement)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const code = e.keyCode;
|
|
247
|
+
const key = new KeyEvent(e, 'keydown', keyHeldByCode[code]);
|
|
248
|
+
globalEvents.emit('keydown', key);
|
|
249
|
+
globalEvents.emit('key', key);
|
|
250
|
+
keyHeldByCode[code] = true;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
document.addEventListener('keyup', (e) => {
|
|
254
|
+
if (canStealFocus(e.target as HTMLElement)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const code = e.keyCode;
|
|
258
|
+
const key = new KeyEvent(e, 'keyup');
|
|
259
|
+
globalEvents.emit('keyup', key);
|
|
260
|
+
globalEvents.emit('key', key);
|
|
261
|
+
keyHeldByCode[code] = false;
|
|
262
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const SI_SYMBOLS = [
|
|
2
|
+
'f', // femto
|
|
3
|
+
'p', // pico
|
|
4
|
+
'n', // nano
|
|
5
|
+
'μ', // micro
|
|
6
|
+
'm', // milli
|
|
7
|
+
// NOTE: This is a space for a reason. When we right align si numbers,
|
|
8
|
+
// in monospace mode, we want to units and numbers stay in their respective
|
|
9
|
+
// columns. If rendering in HTML mode, this space will collapse into
|
|
10
|
+
// a single space anyway.
|
|
11
|
+
' ', // base
|
|
12
|
+
'k', // kilo
|
|
13
|
+
'M', // mega
|
|
14
|
+
'G', // giga
|
|
15
|
+
'T', // tera
|
|
16
|
+
'P', // peta
|
|
17
|
+
'E', // exa
|
|
18
|
+
'Z', // zetta
|
|
19
|
+
'Y', // yotta
|
|
20
|
+
'R', // ronna
|
|
21
|
+
'Q', // quecca
|
|
22
|
+
'F',
|
|
23
|
+
'N',
|
|
24
|
+
'H',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const SI_BASE_INDEX = SI_SYMBOLS.indexOf(' ');
|
|
28
|
+
|
|
29
|
+
// Formats a number to a human readable form, with a custom unit
|
|
30
|
+
export const formatSiUnit = (
|
|
31
|
+
value: number,
|
|
32
|
+
minBase1000 = -SI_BASE_INDEX,
|
|
33
|
+
unit = '',
|
|
34
|
+
): string => {
|
|
35
|
+
if (!isFinite(value)) {
|
|
36
|
+
return value.toString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const realBase10 = Math.floor(Math.log10(Math.abs(value)));
|
|
40
|
+
const base10 = Math.max(minBase1000 * 3, realBase10);
|
|
41
|
+
const base1000 = Math.floor(base10 / 3);
|
|
42
|
+
const symbol =
|
|
43
|
+
SI_SYMBOLS[Math.min(base1000 + SI_BASE_INDEX, SI_SYMBOLS.length - 1)];
|
|
44
|
+
|
|
45
|
+
const scaledValue = value / Math.pow(1000, base1000);
|
|
46
|
+
|
|
47
|
+
let formattedValue = scaledValue.toFixed(2);
|
|
48
|
+
if (formattedValue.endsWith('.00')) {
|
|
49
|
+
formattedValue = formattedValue.slice(0, -3);
|
|
50
|
+
} else if (formattedValue.endsWith('.0')) {
|
|
51
|
+
formattedValue = formattedValue.slice(0, -2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${formattedValue} ${symbol.trim()}${unit}`.trim();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Formats a number to a human readable form, with power (W) as the unit
|
|
58
|
+
export function formatPower(value: number, minBase1000 = 0) {
|
|
59
|
+
return formatSiUnit(value, minBase1000, 'W');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatEnergy(value: number, minBase1000 = 0) {
|
|
63
|
+
return formatSiUnit(value, minBase1000, 'J');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Formats a number as a currency string
|
|
67
|
+
export function formatMoney(value: number, precision = 0) {
|
|
68
|
+
if (!Number.isFinite(value)) {
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Round the number and make it fixed precision
|
|
73
|
+
const roundedValue = Number(value.toFixed(precision));
|
|
74
|
+
|
|
75
|
+
// Handle the negative sign
|
|
76
|
+
const isNegative = roundedValue < 0;
|
|
77
|
+
const absoluteValue = Math.abs(roundedValue);
|
|
78
|
+
|
|
79
|
+
// Convert to string and place thousand separators
|
|
80
|
+
const parts = absoluteValue.toString().split('.');
|
|
81
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '\u2009'); // Thin space
|
|
82
|
+
|
|
83
|
+
const formattedValue = parts.join('.');
|
|
84
|
+
|
|
85
|
+
return isNegative ? `-${formattedValue}` : formattedValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Formats a floating point number as a number on the decibel scale
|
|
89
|
+
export function formatDb(value: number) {
|
|
90
|
+
const db = 20 * Math.log10(value);
|
|
91
|
+
const sign = db >= 0 ? '+' : '-';
|
|
92
|
+
let formatted: string | number = Math.abs(db);
|
|
93
|
+
|
|
94
|
+
if (formatted === Infinity) {
|
|
95
|
+
formatted = 'Inf';
|
|
96
|
+
} else {
|
|
97
|
+
formatted = formatted.toFixed(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `${sign}${formatted} dB`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const SI_BASE_TEN_UNITS = [
|
|
104
|
+
'',
|
|
105
|
+
'· 10³', // kilo
|
|
106
|
+
'· 10⁶', // mega
|
|
107
|
+
'· 10⁹', // giga
|
|
108
|
+
'· 10¹²', // tera
|
|
109
|
+
'· 10¹⁵', // peta
|
|
110
|
+
'· 10¹⁸', // exa
|
|
111
|
+
'· 10²¹', // zetta
|
|
112
|
+
'· 10²⁴', // yotta
|
|
113
|
+
'· 10²⁷', // ronna
|
|
114
|
+
'· 10³⁰', // quecca
|
|
115
|
+
'· 10³³',
|
|
116
|
+
'· 10³⁶',
|
|
117
|
+
'· 10³⁹',
|
|
118
|
+
] as const;
|
|
119
|
+
|
|
120
|
+
// Converts a number to a string with SI base 10 units
|
|
121
|
+
export const formatSiBaseTenUnit = (
|
|
122
|
+
value: number,
|
|
123
|
+
minBase1000 = 0,
|
|
124
|
+
unit = '',
|
|
125
|
+
): string => {
|
|
126
|
+
if (!isFinite(value)) {
|
|
127
|
+
return 'NaN';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const realBase10 = Math.floor(Math.log10(value));
|
|
131
|
+
const base10 = Math.max(minBase1000 * 3, realBase10);
|
|
132
|
+
const base1000 = Math.floor(base10 / 3);
|
|
133
|
+
const symbol = SI_BASE_TEN_UNITS[base1000];
|
|
134
|
+
|
|
135
|
+
const scaledValue = value / Math.pow(1000, base1000);
|
|
136
|
+
const precision = Math.max(0, 2 - (base10 % 3));
|
|
137
|
+
const formattedValue = scaledValue.toFixed(precision);
|
|
138
|
+
|
|
139
|
+
return `${formattedValue} ${symbol} ${unit}`.trim();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Formats decisecond count into HH:MM:SS display by default
|
|
144
|
+
* "short" format does not pad and adds hms suffixes
|
|
145
|
+
*/
|
|
146
|
+
export const formatTime = (
|
|
147
|
+
val: number,
|
|
148
|
+
formatType: 'short' | 'default' = 'default',
|
|
149
|
+
): string => {
|
|
150
|
+
const totalSeconds = Math.floor(val / 10);
|
|
151
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
152
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
153
|
+
const seconds = totalSeconds % 60;
|
|
154
|
+
|
|
155
|
+
if (formatType === 'short') {
|
|
156
|
+
const hoursFormatted = hours > 0 ? `${hours}h` : '';
|
|
157
|
+
const minutesFormatted = minutes > 0 ? `${minutes}m` : '';
|
|
158
|
+
const secondsFormatted = seconds > 0 ? `${seconds}s` : '';
|
|
159
|
+
return `${hoursFormatted}${minutesFormatted}${secondsFormatted}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const hoursPadded = String(hours).padStart(2, '0');
|
|
163
|
+
const minutesPadded = String(minutes).padStart(2, '0');
|
|
164
|
+
const secondsPadded = String(seconds).padStart(2, '0');
|
|
165
|
+
|
|
166
|
+
return `${hoursPadded}:${minutesPadded}:${secondsPadded}`;
|
|
167
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
type Func = (...args: any[]) => any;
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* Creates a function that returns the result of invoking the given
|
|
4
5
|
* functions, where each successive invocation is supplied the return
|
|
@@ -14,5 +15,18 @@ type Func = (...args: any[]) => any;
|
|
|
14
15
|
* const composedFunction2 = flow([add2, multiplyBy3], subtract5); // ((4 + 2) * 3) - 5 = 13
|
|
15
16
|
*
|
|
16
17
|
*/
|
|
17
|
-
export
|
|
18
|
-
|
|
18
|
+
export const flow =
|
|
19
|
+
(...funcs: Array<Func | Func[]>) =>
|
|
20
|
+
(input: any, ...rest: any[]): any => {
|
|
21
|
+
let output = input;
|
|
22
|
+
|
|
23
|
+
for (const func of funcs) {
|
|
24
|
+
// Recurse into the array of functions
|
|
25
|
+
if (Array.isArray(func)) {
|
|
26
|
+
output = flow(...func)(output, ...rest);
|
|
27
|
+
} else if (func) {
|
|
28
|
+
output = func(output, ...rest);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return output;
|
|
32
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { globalEvents, KeyEvent } from './events';
|
|
2
|
+
import * as keycodes from './keycodes';
|
|
3
|
+
|
|
4
|
+
// BYOND macros, in `key: command` format.
|
|
5
|
+
const byondMacros: Record<string, string> = {};
|
|
6
|
+
|
|
7
|
+
// Default set of acquired keys, which will not be sent to BYOND.
|
|
8
|
+
const hotKeysAcquired = [
|
|
9
|
+
keycodes.KEY_ESCAPE,
|
|
10
|
+
keycodes.KEY_ENTER,
|
|
11
|
+
keycodes.KEY_SPACE,
|
|
12
|
+
keycodes.KEY_TAB,
|
|
13
|
+
keycodes.KEY_CTRL,
|
|
14
|
+
keycodes.KEY_SHIFT,
|
|
15
|
+
keycodes.KEY_UP,
|
|
16
|
+
keycodes.KEY_DOWN,
|
|
17
|
+
keycodes.KEY_LEFT,
|
|
18
|
+
keycodes.KEY_RIGHT,
|
|
19
|
+
keycodes.KEY_F5,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// State of passed-through keys.
|
|
23
|
+
const keyState: Record<string, boolean> = {};
|
|
24
|
+
|
|
25
|
+
// Custom listeners for key events
|
|
26
|
+
const keyListeners: ((key: KeyEvent) => void)[] = [];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts a browser keycode to BYOND keycode.
|
|
30
|
+
*/
|
|
31
|
+
function keyCodeToByond(keyCode: number) {
|
|
32
|
+
if (keyCode === 16) return 'Shift';
|
|
33
|
+
if (keyCode === 17) return 'Ctrl';
|
|
34
|
+
if (keyCode === 18) return 'Alt';
|
|
35
|
+
if (keyCode === 33) return 'Northeast';
|
|
36
|
+
if (keyCode === 34) return 'Southeast';
|
|
37
|
+
if (keyCode === 35) return 'Southwest';
|
|
38
|
+
if (keyCode === 36) return 'Northwest';
|
|
39
|
+
if (keyCode === 37) return 'West';
|
|
40
|
+
if (keyCode === 38) return 'North';
|
|
41
|
+
if (keyCode === 39) return 'East';
|
|
42
|
+
if (keyCode === 40) return 'South';
|
|
43
|
+
if (keyCode === 45) return 'Insert';
|
|
44
|
+
if (keyCode === 46) return 'Delete';
|
|
45
|
+
|
|
46
|
+
if ((keyCode >= 48 && keyCode <= 57) || (keyCode >= 65 && keyCode <= 90)) {
|
|
47
|
+
return String.fromCharCode(keyCode);
|
|
48
|
+
}
|
|
49
|
+
if (keyCode >= 96 && keyCode <= 105) {
|
|
50
|
+
return 'Numpad' + (keyCode - 96);
|
|
51
|
+
}
|
|
52
|
+
if (keyCode >= 112 && keyCode <= 123) {
|
|
53
|
+
return 'F' + (keyCode - 111);
|
|
54
|
+
}
|
|
55
|
+
if (keyCode === 188) return ',';
|
|
56
|
+
if (keyCode === 189) return '-';
|
|
57
|
+
if (keyCode === 190) return '.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Keyboard passthrough logic. This allows you to keep doing things
|
|
62
|
+
* in game while the browser window is focused.
|
|
63
|
+
*/
|
|
64
|
+
function handlePassthrough(key: KeyEvent) {
|
|
65
|
+
const keyString = String(key);
|
|
66
|
+
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
|
|
67
|
+
if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') {
|
|
68
|
+
location.reload();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Prevent passthrough on Ctrl+F
|
|
72
|
+
if (keyString === 'Ctrl+F') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// NOTE: Alt modifier is pretty bad and sticky in IE11.
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
key.event.defaultPrevented ||
|
|
79
|
+
key.isModifierKey() ||
|
|
80
|
+
hotKeysAcquired.includes(key.code)
|
|
81
|
+
) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const byondKeyCode = keyCodeToByond(key.code);
|
|
85
|
+
if (!byondKeyCode) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Macro
|
|
89
|
+
const macro = byondMacros[byondKeyCode];
|
|
90
|
+
if (macro) {
|
|
91
|
+
return Byond.command(macro);
|
|
92
|
+
}
|
|
93
|
+
// KeyDown
|
|
94
|
+
if (key.isDown() && !keyState[byondKeyCode]) {
|
|
95
|
+
keyState[byondKeyCode] = true;
|
|
96
|
+
const command = `KeyDown "${byondKeyCode}"`;
|
|
97
|
+
return Byond.command(command);
|
|
98
|
+
}
|
|
99
|
+
// KeyUp
|
|
100
|
+
if (key.isUp() && keyState[byondKeyCode]) {
|
|
101
|
+
keyState[byondKeyCode] = false;
|
|
102
|
+
const command = `KeyUp "${byondKeyCode}"`;
|
|
103
|
+
return Byond.command(command);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Acquires a lock on the hotkey, which prevents it from being
|
|
109
|
+
* passed through to BYOND.
|
|
110
|
+
*/
|
|
111
|
+
export function acquireHotKey(keyCode: number) {
|
|
112
|
+
hotKeysAcquired.push(keyCode);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Makes the hotkey available to BYOND again.
|
|
117
|
+
*/
|
|
118
|
+
export function releaseHotKey(keyCode: number) {
|
|
119
|
+
const index = hotKeysAcquired.indexOf(keyCode);
|
|
120
|
+
if (index >= 0) {
|
|
121
|
+
hotKeysAcquired.splice(index, 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function releaseHeldKeys() {
|
|
126
|
+
for (const byondKeyCode of Object.keys(keyState)) {
|
|
127
|
+
if (keyState[byondKeyCode]) {
|
|
128
|
+
keyState[byondKeyCode] = false;
|
|
129
|
+
Byond.command(`KeyUp "${byondKeyCode}"`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type ByondSkinMacro = {
|
|
135
|
+
command: string;
|
|
136
|
+
name: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export function setupHotKeys() {
|
|
140
|
+
// Read macros
|
|
141
|
+
Byond.winget('default.*').then((data: Record<string, string>) => {
|
|
142
|
+
// Group each macro by ref
|
|
143
|
+
const groupedByRef: Record<string, ByondSkinMacro> = {};
|
|
144
|
+
for (const key of Object.keys(data)) {
|
|
145
|
+
const keyPath = key.split('.');
|
|
146
|
+
const ref = keyPath[1];
|
|
147
|
+
const prop = keyPath[2];
|
|
148
|
+
if (ref && prop) {
|
|
149
|
+
// This piece of code imperatively adds each property to a
|
|
150
|
+
// ByondSkinMacro object in the order we meet it, which is hard
|
|
151
|
+
// to express safely in typescript.
|
|
152
|
+
if (!groupedByRef[ref]) {
|
|
153
|
+
groupedByRef[ref] = {} as any;
|
|
154
|
+
}
|
|
155
|
+
groupedByRef[ref][prop] = data[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Insert macros
|
|
159
|
+
const escapedQuotRegex = /\\"/g;
|
|
160
|
+
|
|
161
|
+
function unescape(str: string) {
|
|
162
|
+
return str.substring(1, str.length - 1).replace(escapedQuotRegex, '"');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const ref of Object.keys(groupedByRef)) {
|
|
166
|
+
const macro = groupedByRef[ref];
|
|
167
|
+
const byondKeyName = unescape(macro.name);
|
|
168
|
+
byondMacros[byondKeyName] = unescape(macro.command);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Setup event handlers
|
|
172
|
+
globalEvents.on('window-blur', () => {
|
|
173
|
+
releaseHeldKeys();
|
|
174
|
+
});
|
|
175
|
+
globalEvents.on('key', (key: KeyEvent) => {
|
|
176
|
+
for (const keyListener of keyListeners) {
|
|
177
|
+
keyListener(key);
|
|
178
|
+
}
|
|
179
|
+
handlePassthrough(key);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Registers for any key events, such as key down or key up.
|
|
185
|
+
* This should be preferred over directly connecting to keydown/keyup
|
|
186
|
+
* as it lets tgui prevent the key from reaching BYOND.
|
|
187
|
+
*
|
|
188
|
+
* If using in a component, prefer KeyListener, which automatically handles
|
|
189
|
+
* stopping listening when unmounting.
|
|
190
|
+
*
|
|
191
|
+
* @param callback The function to call whenever a key event occurs
|
|
192
|
+
* @returns A callback to stop listening
|
|
193
|
+
*/
|
|
194
|
+
export function listenForKeyEvents(callback: (key: KeyEvent) => void) {
|
|
195
|
+
keyListeners.push(callback);
|
|
196
|
+
|
|
197
|
+
let removed = false;
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
if (removed) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
removed = true;
|
|
205
|
+
keyListeners.splice(keyListeners.indexOf(callback), 1);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An equivalent to `fetch`, except will automatically retry.
|
|
3
|
+
*/
|
|
4
|
+
export function fetchRetry(
|
|
5
|
+
url: string,
|
|
6
|
+
options?: RequestInit,
|
|
7
|
+
retryTimer: number = 1000,
|
|
8
|
+
): Promise<Response> {
|
|
9
|
+
return fetch(url, options).catch(() => {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
fetchRetry(url, options, retryTimer).then(resolve);
|
|
13
|
+
}, retryTimer);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|