tgui-core 1.0.0
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/.editorconfig +10 -0
- package/.eslintrc.cjs +78 -0
- package/.gitattributes +4 -0
- package/.prettierrc.yml +1 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +5 -0
- package/README.md +1 -0
- package/global.d.ts +173 -0
- package/package.json +25 -0
- package/src/assets.ts +43 -0
- package/src/backend.ts +369 -0
- package/src/common/collections.ts +349 -0
- package/src/common/color.ts +94 -0
- package/src/common/events.ts +45 -0
- package/src/common/exhaustive.ts +19 -0
- package/src/common/fp.ts +38 -0
- package/src/common/keycodes.ts +86 -0
- package/src/common/keys.ts +39 -0
- package/src/common/math.ts +98 -0
- package/src/common/perf.ts +72 -0
- package/src/common/random.ts +32 -0
- package/src/common/react.ts +65 -0
- package/src/common/redux.ts +196 -0
- package/src/common/storage.js +196 -0
- package/src/common/string.ts +173 -0
- package/src/common/timer.ts +68 -0
- package/src/common/type-utils.ts +41 -0
- package/src/common/types.ts +9 -0
- package/src/common/uuid.ts +24 -0
- package/src/common/vector.ts +51 -0
- package/src/components/AnimatedNumber.tsx +185 -0
- package/src/components/Autofocus.tsx +23 -0
- package/src/components/Blink.jsx +69 -0
- package/src/components/BlockQuote.tsx +15 -0
- package/src/components/BodyZoneSelector.tsx +149 -0
- package/src/components/Box.tsx +255 -0
- package/src/components/Button.tsx +415 -0
- package/src/components/ByondUi.jsx +121 -0
- package/src/components/Chart.tsx +160 -0
- package/src/components/Collapsible.tsx +45 -0
- package/src/components/ColorBox.tsx +30 -0
- package/src/components/Dialog.tsx +85 -0
- package/src/components/Dimmer.tsx +19 -0
- package/src/components/Divider.tsx +26 -0
- package/src/components/DmIcon.tsx +72 -0
- package/src/components/DraggableControl.jsx +282 -0
- package/src/components/Dropdown.tsx +246 -0
- package/src/components/FakeTerminal.jsx +52 -0
- package/src/components/FitText.tsx +99 -0
- package/src/components/Flex.tsx +105 -0
- package/src/components/Grid.tsx +44 -0
- package/src/components/Icon.tsx +91 -0
- package/src/components/Image.tsx +63 -0
- package/src/components/InfinitePlane.jsx +192 -0
- package/src/components/Input.tsx +181 -0
- package/src/components/KeyListener.tsx +40 -0
- package/src/components/Knob.tsx +185 -0
- package/src/components/LabeledControls.tsx +50 -0
- package/src/components/LabeledList.tsx +130 -0
- package/src/components/MenuBar.tsx +238 -0
- package/src/components/Modal.tsx +25 -0
- package/src/components/NoticeBox.tsx +48 -0
- package/src/components/NumberInput.tsx +328 -0
- package/src/components/Popper.tsx +100 -0
- package/src/components/ProgressBar.tsx +79 -0
- package/src/components/RestrictedInput.jsx +301 -0
- package/src/components/RoundGauge.tsx +189 -0
- package/src/components/Section.tsx +125 -0
- package/src/components/Slider.tsx +173 -0
- package/src/components/Stack.tsx +101 -0
- package/src/components/StyleableSection.tsx +30 -0
- package/src/components/Table.tsx +90 -0
- package/src/components/Tabs.tsx +90 -0
- package/src/components/TextArea.tsx +198 -0
- package/src/components/TimeDisplay.jsx +64 -0
- package/src/components/Tooltip.tsx +147 -0
- package/src/components/TrackOutsideClicks.tsx +35 -0
- package/src/components/VirtualList.tsx +69 -0
- package/src/constants.ts +355 -0
- package/src/debug/KitchenSink.jsx +56 -0
- package/src/debug/actions.js +11 -0
- package/src/debug/hooks.js +10 -0
- package/src/debug/index.ts +10 -0
- package/src/debug/middleware.js +86 -0
- package/src/debug/reducer.js +22 -0
- package/src/debug/selectors.js +7 -0
- package/src/drag.ts +280 -0
- package/src/events.ts +237 -0
- package/src/focus.ts +25 -0
- package/src/format.ts +173 -0
- package/src/hotkeys.ts +212 -0
- package/src/http.ts +16 -0
- package/src/layouts/Layout.tsx +75 -0
- package/src/layouts/NtosWindow.tsx +162 -0
- package/src/layouts/Pane.tsx +56 -0
- package/src/layouts/Window.tsx +227 -0
- package/src/renderer.ts +50 -0
- package/styles/base.scss +32 -0
- package/styles/colors.scss +92 -0
- package/styles/components/BlockQuote.scss +20 -0
- package/styles/components/Button.scss +175 -0
- package/styles/components/ColorBox.scss +12 -0
- package/styles/components/Dialog.scss +105 -0
- package/styles/components/Dimmer.scss +22 -0
- package/styles/components/Divider.scss +27 -0
- package/styles/components/Dropdown.scss +72 -0
- package/styles/components/Flex.scss +31 -0
- package/styles/components/Icon.scss +25 -0
- package/styles/components/Input.scss +68 -0
- package/styles/components/Knob.scss +131 -0
- package/styles/components/LabeledList.scss +49 -0
- package/styles/components/MenuBar.scss +75 -0
- package/styles/components/Modal.scss +14 -0
- package/styles/components/NoticeBox.scss +65 -0
- package/styles/components/NumberInput.scss +76 -0
- package/styles/components/ProgressBar.scss +63 -0
- package/styles/components/RoundGauge.scss +88 -0
- package/styles/components/Section.scss +143 -0
- package/styles/components/Slider.scss +54 -0
- package/styles/components/Stack.scss +59 -0
- package/styles/components/Table.scss +44 -0
- package/styles/components/Tabs.scss +144 -0
- package/styles/components/TextArea.scss +84 -0
- package/styles/components/Tooltip.scss +24 -0
- package/styles/functions.scss +79 -0
- package/styles/layouts/Layout.scss +57 -0
- package/styles/layouts/NtosHeader.scss +20 -0
- package/styles/layouts/NtosWindow.scss +26 -0
- package/styles/layouts/TitleBar.scss +111 -0
- package/styles/layouts/Window.scss +103 -0
- package/styles/main.scss +97 -0
- package/styles/reset.scss +68 -0
- package/styles/themes/abductor.scss +68 -0
- package/styles/themes/admin.scss +38 -0
- package/styles/themes/cardtable.scss +57 -0
- package/styles/themes/hackerman.scss +70 -0
- package/styles/themes/malfunction.scss +67 -0
- package/styles/themes/neutral.scss +50 -0
- package/styles/themes/ntOS95.scss +166 -0
- package/styles/themes/ntos.scss +44 -0
- package/styles/themes/ntos_cat.scss +148 -0
- package/styles/themes/ntos_darkmode.scss +44 -0
- package/styles/themes/ntos_lightmode.scss +67 -0
- package/styles/themes/ntos_spooky.scss +69 -0
- package/styles/themes/ntos_synth.scss +99 -0
- package/styles/themes/ntos_terminal.scss +112 -0
- package/styles/themes/paper.scss +184 -0
- package/styles/themes/retro.scss +72 -0
- package/styles/themes/spookyconsole.scss +73 -0
- package/styles/themes/syndicate.scss +67 -0
- package/styles/themes/wizard.scss +68 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type Reducer<State = any, ActionType extends Action = AnyAction> = (
|
|
8
|
+
state: State | undefined,
|
|
9
|
+
action: ActionType,
|
|
10
|
+
) => State;
|
|
11
|
+
|
|
12
|
+
export type Store<State = any, ActionType extends Action = AnyAction> = {
|
|
13
|
+
dispatch: Dispatch<ActionType>;
|
|
14
|
+
subscribe: (listener: () => void) => void;
|
|
15
|
+
getState: () => State;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type MiddlewareAPI<State = any, ActionType extends Action = AnyAction> = {
|
|
19
|
+
getState: () => State;
|
|
20
|
+
dispatch: Dispatch<ActionType>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type Middleware = <State = any, ActionType extends Action = AnyAction>(
|
|
24
|
+
storeApi: MiddlewareAPI<State, ActionType>,
|
|
25
|
+
) => (next: Dispatch<ActionType>) => Dispatch<ActionType>;
|
|
26
|
+
|
|
27
|
+
export type Action<TType = any> = {
|
|
28
|
+
type: TType;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type AnyAction = Action & {
|
|
32
|
+
[extraProps: string]: any;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type Dispatch<ActionType extends Action = AnyAction> = (
|
|
36
|
+
action: ActionType,
|
|
37
|
+
) => void;
|
|
38
|
+
|
|
39
|
+
type StoreEnhancer = (createStoreFunction: Function) => Function;
|
|
40
|
+
|
|
41
|
+
type PreparedAction = {
|
|
42
|
+
payload?: any;
|
|
43
|
+
meta?: any;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a Redux store.
|
|
48
|
+
*/
|
|
49
|
+
export const createStore = <State, ActionType extends Action = AnyAction>(
|
|
50
|
+
reducer: Reducer<State, ActionType>,
|
|
51
|
+
enhancer?: StoreEnhancer,
|
|
52
|
+
): Store<State, ActionType> => {
|
|
53
|
+
// Apply a store enhancer (applyMiddleware is one of them).
|
|
54
|
+
if (enhancer) {
|
|
55
|
+
return enhancer(createStore)(reducer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let currentState: State;
|
|
59
|
+
let listeners: Array<() => void> = [];
|
|
60
|
+
|
|
61
|
+
const getState = (): State => currentState;
|
|
62
|
+
|
|
63
|
+
const subscribe = (listener: () => void): void => {
|
|
64
|
+
listeners.push(listener);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const dispatch = (action: ActionType): void => {
|
|
68
|
+
currentState = reducer(currentState, action);
|
|
69
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
70
|
+
listeners[i]();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// This creates the initial store by causing each reducer to be called
|
|
75
|
+
// with an undefined state
|
|
76
|
+
dispatch({ type: '@@INIT' } as ActionType);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
dispatch,
|
|
80
|
+
subscribe,
|
|
81
|
+
getState,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a store enhancer which applies middleware to all dispatched
|
|
87
|
+
* actions.
|
|
88
|
+
*/
|
|
89
|
+
export const applyMiddleware = (
|
|
90
|
+
...middlewares: Middleware[]
|
|
91
|
+
): StoreEnhancer => {
|
|
92
|
+
return (
|
|
93
|
+
createStoreFunction: (reducer: Reducer, enhancer?: StoreEnhancer) => Store,
|
|
94
|
+
) => {
|
|
95
|
+
return (reducer, ...args): Store => {
|
|
96
|
+
const store = createStoreFunction(reducer, ...args);
|
|
97
|
+
|
|
98
|
+
let dispatch: Dispatch = (action, ...args) => {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'Dispatching while constructing your middleware is not allowed.',
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const storeApi: MiddlewareAPI = {
|
|
105
|
+
getState: store.getState,
|
|
106
|
+
dispatch: (action, ...args) => dispatch(action, ...args),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const chain = middlewares.map((middleware) => middleware(storeApi));
|
|
110
|
+
dispatch = chain.reduceRight(
|
|
111
|
+
(next, middleware) => middleware(next),
|
|
112
|
+
store.dispatch,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...store,
|
|
117
|
+
dispatch,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Combines reducers by running them in their own object namespaces as
|
|
125
|
+
* defined in reducersObj paramter.
|
|
126
|
+
*
|
|
127
|
+
* Main difference from redux/combineReducers is that it preserves keys
|
|
128
|
+
* in the state that are not present in the reducers object. This function
|
|
129
|
+
* is also more flexible than the redux counterpart.
|
|
130
|
+
*/
|
|
131
|
+
export const combineReducers = (
|
|
132
|
+
reducersObj: Record<string, Reducer>,
|
|
133
|
+
): Reducer => {
|
|
134
|
+
const keys = Object.keys(reducersObj);
|
|
135
|
+
|
|
136
|
+
return (prevState = {}, action) => {
|
|
137
|
+
const nextState = { ...prevState };
|
|
138
|
+
let hasChanged = false;
|
|
139
|
+
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
const reducer = reducersObj[key];
|
|
142
|
+
const prevDomainState = prevState[key];
|
|
143
|
+
const nextDomainState = reducer(prevDomainState, action);
|
|
144
|
+
|
|
145
|
+
if (prevDomainState !== nextDomainState) {
|
|
146
|
+
hasChanged = true;
|
|
147
|
+
nextState[key] = nextDomainState;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return hasChanged ? nextState : prevState;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A utility function to create an action creator for the given action
|
|
157
|
+
* type string. The action creator accepts a single argument, which will
|
|
158
|
+
* be included in the action object as a field called payload. The action
|
|
159
|
+
* creator function will also have its toString() overriden so that it
|
|
160
|
+
* returns the action type, allowing it to be used in reducer logic that
|
|
161
|
+
* is looking for that action type.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} type The action type to use for created actions.
|
|
164
|
+
* @param {any} prepare (optional) a method that takes any number of arguments
|
|
165
|
+
* and returns { payload } or { payload, meta }. If this is given, the
|
|
166
|
+
* resulting action creator will pass it's arguments to this method to
|
|
167
|
+
* calculate payload & meta.
|
|
168
|
+
*
|
|
169
|
+
* @public
|
|
170
|
+
*/
|
|
171
|
+
export const createAction = <TAction extends string>(
|
|
172
|
+
type: TAction,
|
|
173
|
+
prepare?: (...args: any[]) => PreparedAction,
|
|
174
|
+
) => {
|
|
175
|
+
const actionCreator = (...args: any[]) => {
|
|
176
|
+
let action: Action<TAction> & PreparedAction = { type };
|
|
177
|
+
|
|
178
|
+
if (prepare) {
|
|
179
|
+
const prepared = prepare(...args);
|
|
180
|
+
if (!prepared) {
|
|
181
|
+
throw new Error('prepare function did not return an object');
|
|
182
|
+
}
|
|
183
|
+
action = { ...action, ...prepared };
|
|
184
|
+
} else {
|
|
185
|
+
action.payload = args[0];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return action;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
actionCreator.toString = () => type;
|
|
192
|
+
actionCreator.type = type;
|
|
193
|
+
actionCreator.match = (action) => action.type === type;
|
|
194
|
+
|
|
195
|
+
return actionCreator;
|
|
196
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-agnostic abstraction of key-value web storage.
|
|
3
|
+
*
|
|
4
|
+
* @file
|
|
5
|
+
* @copyright 2020 Aleksej Komarov
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const IMPL_MEMORY = 0;
|
|
10
|
+
export const IMPL_LOCAL_STORAGE = 1;
|
|
11
|
+
export const IMPL_INDEXED_DB = 2;
|
|
12
|
+
|
|
13
|
+
const INDEXED_DB_VERSION = 1;
|
|
14
|
+
const INDEXED_DB_NAME = 'tgui';
|
|
15
|
+
const INDEXED_DB_STORE_NAME = 'storage-v1';
|
|
16
|
+
|
|
17
|
+
const READ_ONLY = 'readonly';
|
|
18
|
+
const READ_WRITE = 'readwrite';
|
|
19
|
+
|
|
20
|
+
const testGeneric = (testFn) => () => {
|
|
21
|
+
try {
|
|
22
|
+
return Boolean(testFn());
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Localstorage can sometimes throw an error, even if DOM storage is not
|
|
29
|
+
// disabled in IE11 settings.
|
|
30
|
+
// See: https://superuser.com/questions/1080011
|
|
31
|
+
|
|
32
|
+
const testLocalStorage = testGeneric(
|
|
33
|
+
() => window.localStorage && window.localStorage.getItem
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const testIndexedDb = testGeneric(
|
|
37
|
+
() =>
|
|
38
|
+
(window.indexedDB || window.msIndexedDB) &&
|
|
39
|
+
(window.IDBTransaction || window.msIDBTransaction)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
class MemoryBackend {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.impl = IMPL_MEMORY;
|
|
45
|
+
this.store = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get(key) {
|
|
49
|
+
return this.store[key];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set(key, value) {
|
|
53
|
+
this.store[key] = value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
remove(key) {
|
|
57
|
+
this.store[key] = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clear() {
|
|
61
|
+
this.store = {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class LocalStorageBackend {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.impl = IMPL_LOCAL_STORAGE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get(key) {
|
|
71
|
+
const value = localStorage.getItem(key);
|
|
72
|
+
if (typeof value === 'string') {
|
|
73
|
+
return JSON.parse(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
set(key, value) {
|
|
78
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
remove(key) {
|
|
82
|
+
localStorage.removeItem(key);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
clear() {
|
|
86
|
+
localStorage.clear();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class IndexedDbBackend {
|
|
91
|
+
constructor() {
|
|
92
|
+
this.impl = IMPL_INDEXED_DB;
|
|
93
|
+
/** @type {Promise<IDBDatabase>} */
|
|
94
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
95
|
+
const indexedDB = window.indexedDB || window.msIndexedDB;
|
|
96
|
+
const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
|
|
97
|
+
req.onupgradeneeded = () => {
|
|
98
|
+
try {
|
|
99
|
+
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
reject(new Error('Failed to upgrade IDB: ' + req.error));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
req.onsuccess = () => resolve(req.result);
|
|
105
|
+
req.onerror = () => {
|
|
106
|
+
reject(new Error('Failed to open IDB: ' + req.error));
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getStore(mode) {
|
|
112
|
+
return this.dbPromise.then((db) =>
|
|
113
|
+
db
|
|
114
|
+
.transaction(INDEXED_DB_STORE_NAME, mode)
|
|
115
|
+
.objectStore(INDEXED_DB_STORE_NAME)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async get(key) {
|
|
120
|
+
const store = await this.getStore(READ_ONLY);
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const req = store.get(key);
|
|
123
|
+
req.onsuccess = () => resolve(req.result);
|
|
124
|
+
req.onerror = () => reject(req.error);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async set(key, value) {
|
|
129
|
+
// The reason we don't _save_ null is because IE 10 does
|
|
130
|
+
// not support saving the `null` type in IndexedDB. How
|
|
131
|
+
// ironic, given the bug below!
|
|
132
|
+
// See: https://github.com/mozilla/localForage/issues/161
|
|
133
|
+
if (value === null) {
|
|
134
|
+
value = undefined;
|
|
135
|
+
}
|
|
136
|
+
// NOTE: We deliberately make this operation transactionless
|
|
137
|
+
const store = await this.getStore(READ_WRITE);
|
|
138
|
+
store.put(value, key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async remove(key) {
|
|
142
|
+
// NOTE: We deliberately make this operation transactionless
|
|
143
|
+
const store = await this.getStore(READ_WRITE);
|
|
144
|
+
store.delete(key);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async clear() {
|
|
148
|
+
// NOTE: We deliberately make this operation transactionless
|
|
149
|
+
const store = await this.getStore(READ_WRITE);
|
|
150
|
+
store.clear();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Web Storage Proxy object, which selects the best backend available
|
|
156
|
+
* depending on the environment.
|
|
157
|
+
*/
|
|
158
|
+
class StorageProxy {
|
|
159
|
+
constructor() {
|
|
160
|
+
this.backendPromise = (async () => {
|
|
161
|
+
if (testIndexedDb()) {
|
|
162
|
+
try {
|
|
163
|
+
const backend = new IndexedDbBackend();
|
|
164
|
+
await backend.dbPromise;
|
|
165
|
+
return backend;
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
if (testLocalStorage()) {
|
|
169
|
+
return new LocalStorageBackend();
|
|
170
|
+
}
|
|
171
|
+
return new MemoryBackend();
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async get(key) {
|
|
176
|
+
const backend = await this.backendPromise;
|
|
177
|
+
return backend.get(key);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async set(key, value) {
|
|
181
|
+
const backend = await this.backendPromise;
|
|
182
|
+
return backend.set(key, value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async remove(key) {
|
|
186
|
+
const backend = await this.backendPromise;
|
|
187
|
+
return backend.remove(key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async clear() {
|
|
191
|
+
const backend = await this.backendPromise;
|
|
192
|
+
return backend.clear();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const storage = new StorageProxy();
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a search terms matcher. Returns true if given string matches the search text.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* type Thing = { id: string; name: string };
|
|
13
|
+
*
|
|
14
|
+
* const objects = [
|
|
15
|
+
* { id: '123', name: 'Test' },
|
|
16
|
+
* { id: '456', name: 'Test' },
|
|
17
|
+
* ];
|
|
18
|
+
*
|
|
19
|
+
* const search = createSearch('123', (obj: Thing) => obj.id);
|
|
20
|
+
*
|
|
21
|
+
* objects.filter(search); // returns [{ id: '123', name: 'Test' }]
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function createSearch<TObj>(
|
|
25
|
+
searchText: string,
|
|
26
|
+
stringifier = (obj: TObj) => JSON.stringify(obj),
|
|
27
|
+
): (obj: TObj) => boolean {
|
|
28
|
+
const preparedSearchText = searchText.toLowerCase().trim();
|
|
29
|
+
|
|
30
|
+
return (obj) => {
|
|
31
|
+
if (!preparedSearchText) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const str = stringifier(obj);
|
|
35
|
+
if (!str) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return str.toLowerCase().includes(preparedSearchText);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Capitalizes a word and lowercases the rest.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* capitalize('heLLo') // Hello
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function capitalize(str: string): string {
|
|
51
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Similar to capitalize, this takes a string and replaces all first letters
|
|
56
|
+
* of any words.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* capitalizeAll('heLLo woRLd') // 'HeLLo WoRLd'
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function capitalizeAll(str: string): string {
|
|
64
|
+
return str.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Capitalizes only the first letter of the str, leaving others untouched.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* capitalizeFirst('heLLo woRLd') // 'HeLLo woRLd'
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function capitalizeFirst(str: string): string {
|
|
76
|
+
return str.replace(/^\w/, (letter) => letter.toUpperCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const WORDS_UPPER = ['Id', 'Tv'] as const;
|
|
80
|
+
|
|
81
|
+
const WORDS_LOWER = [
|
|
82
|
+
'A',
|
|
83
|
+
'An',
|
|
84
|
+
'And',
|
|
85
|
+
'As',
|
|
86
|
+
'At',
|
|
87
|
+
'But',
|
|
88
|
+
'By',
|
|
89
|
+
'For',
|
|
90
|
+
'For',
|
|
91
|
+
'From',
|
|
92
|
+
'In',
|
|
93
|
+
'Into',
|
|
94
|
+
'Near',
|
|
95
|
+
'Nor',
|
|
96
|
+
'Of',
|
|
97
|
+
'On',
|
|
98
|
+
'Onto',
|
|
99
|
+
'Or',
|
|
100
|
+
'The',
|
|
101
|
+
'To',
|
|
102
|
+
'With',
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Converts a string to title case.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```tsx
|
|
110
|
+
* toTitleCase('a tale of two cities') // 'A Tale of Two Cities'
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function toTitleCase(str: string): string {
|
|
114
|
+
if (!str) return str;
|
|
115
|
+
|
|
116
|
+
let currentStr = str.replace(/([^\W_]+[^\s-]*) */g, (str) => {
|
|
117
|
+
return capitalize(str);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (let word of WORDS_LOWER) {
|
|
121
|
+
const regex = new RegExp('\\s' + word + '\\s', 'g');
|
|
122
|
+
currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let word of WORDS_UPPER) {
|
|
126
|
+
const regex = new RegExp('\\b' + word + '\\b', 'g');
|
|
127
|
+
currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return currentStr;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const TRANSLATE_REGEX = /&(nbsp|amp|quot|lt|gt|apos);/g;
|
|
134
|
+
const TRANSLATIONS = {
|
|
135
|
+
amp: '&',
|
|
136
|
+
apos: "'",
|
|
137
|
+
gt: '>',
|
|
138
|
+
lt: '<',
|
|
139
|
+
nbsp: ' ',
|
|
140
|
+
quot: '"',
|
|
141
|
+
} as const;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Decodes HTML entities and removes unnecessary HTML tags.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```tsx
|
|
148
|
+
* decodeHtmlEntities('&') // returns '&'
|
|
149
|
+
* decodeHtmlEntities('<') // returns '<'
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function decodeHtmlEntities(str: string): string {
|
|
153
|
+
if (!str) return str;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
str
|
|
157
|
+
// Newline tags
|
|
158
|
+
.replace(/<br>/gi, '\n')
|
|
159
|
+
.replace(/<\/?[a-z0-9-_]+[^>]*>/gi, '')
|
|
160
|
+
// Basic entities
|
|
161
|
+
.replace(TRANSLATE_REGEX, (match, entity) => TRANSLATIONS[entity])
|
|
162
|
+
// Decimal entities
|
|
163
|
+
.replace(/&#?([0-9]+);/gi, (match, numStr) => {
|
|
164
|
+
const num = parseInt(numStr, 10);
|
|
165
|
+
return String.fromCharCode(num);
|
|
166
|
+
})
|
|
167
|
+
// Hex entities
|
|
168
|
+
.replace(/&#x?([0-9a-f]+);/gi, (match, numStr) => {
|
|
169
|
+
const num = parseInt(numStr, 16);
|
|
170
|
+
return String.fromCharCode(num);
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a function, that, as long as it continues to be invoked, will
|
|
9
|
+
* not be triggered. The function will be called after it stops being
|
|
10
|
+
* called for N milliseconds. If `immediate` is passed, trigger the
|
|
11
|
+
* function on the leading edge, instead of the trailing.
|
|
12
|
+
*/
|
|
13
|
+
export const debounce = <F extends (...args: any[]) => any>(
|
|
14
|
+
fn: F,
|
|
15
|
+
time: number,
|
|
16
|
+
immediate = false,
|
|
17
|
+
): ((...args: Parameters<F>) => void) => {
|
|
18
|
+
let timeout: ReturnType<typeof setTimeout> | null;
|
|
19
|
+
return (...args: Parameters<F>) => {
|
|
20
|
+
const later = () => {
|
|
21
|
+
timeout = null;
|
|
22
|
+
if (!immediate) {
|
|
23
|
+
fn(...args);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const callNow = immediate && !timeout;
|
|
27
|
+
clearTimeout(timeout!);
|
|
28
|
+
timeout = setTimeout(later, time);
|
|
29
|
+
if (callNow) {
|
|
30
|
+
fn(...args);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns a function, that, when invoked, will only be triggered at most once
|
|
37
|
+
* during a given window of time.
|
|
38
|
+
*/
|
|
39
|
+
export const throttle = <F extends (...args: any[]) => any>(
|
|
40
|
+
fn: F,
|
|
41
|
+
time: number,
|
|
42
|
+
): ((...args: Parameters<F>) => void) => {
|
|
43
|
+
let previouslyRun: number | null,
|
|
44
|
+
queuedToRun: ReturnType<typeof setTimeout> | null;
|
|
45
|
+
return function invokeFn(...args: Parameters<F>) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (queuedToRun) {
|
|
48
|
+
clearTimeout(queuedToRun);
|
|
49
|
+
}
|
|
50
|
+
if (!previouslyRun || now - previouslyRun >= time) {
|
|
51
|
+
fn.apply(null, args);
|
|
52
|
+
previouslyRun = now;
|
|
53
|
+
} else {
|
|
54
|
+
queuedToRun = setTimeout(
|
|
55
|
+
() => invokeFn(...args),
|
|
56
|
+
time - (now - (previouslyRun ?? 0)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Suspends an asynchronous function for N milliseconds.
|
|
64
|
+
*
|
|
65
|
+
* @param {number} time
|
|
66
|
+
*/
|
|
67
|
+
export const sleep = (time: number): Promise<void> =>
|
|
68
|
+
new Promise((resolve) => setTimeout(resolve, time));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helps visualize highly complex ui data on the fly.
|
|
3
|
+
* @example
|
|
4
|
+
* ```tsx
|
|
5
|
+
* const { data } = useBackend<CargoData>();
|
|
6
|
+
* logger.log(getShallowTypes(data));
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export function getShallowTypes(
|
|
10
|
+
data: Record<string, any>,
|
|
11
|
+
): Record<string, any> {
|
|
12
|
+
const output = {};
|
|
13
|
+
|
|
14
|
+
for (const key in data) {
|
|
15
|
+
if (Array.isArray(data[key])) {
|
|
16
|
+
const arr: any[] = data[key];
|
|
17
|
+
|
|
18
|
+
// Return the first array item if it exists
|
|
19
|
+
if (data[key].length > 0) {
|
|
20
|
+
output[key] = arr[0];
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
output[key] = 'emptyarray';
|
|
25
|
+
} else if (typeof data[key] === 'object' && data[key] !== null) {
|
|
26
|
+
// Please inspect it further and make a new type for it
|
|
27
|
+
output[key] = 'object (inspect) || Record<string, any>';
|
|
28
|
+
} else if (typeof data[key] === 'number') {
|
|
29
|
+
const num = Number(data[key]);
|
|
30
|
+
|
|
31
|
+
// 0 and 1 could be booleans from byond
|
|
32
|
+
if (num === 1 || num === 0) {
|
|
33
|
+
output[key] = `${num}, BooleanLike?`;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
output[key] = data[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return output;
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a UUID v4 string
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* createUuid(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function createUuid(): string {
|
|
16
|
+
let d = new Date().getTime();
|
|
17
|
+
|
|
18
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
19
|
+
const r = (d + Math.random() * 16) % 16 | 0;
|
|
20
|
+
d = Math.floor(d / 16);
|
|
21
|
+
|
|
22
|
+
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
23
|
+
});
|
|
24
|
+
}
|