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.
Files changed (152) hide show
  1. package/.editorconfig +10 -0
  2. package/.eslintrc.cjs +78 -0
  3. package/.gitattributes +4 -0
  4. package/.prettierrc.yml +1 -0
  5. package/.vscode/extensions.json +6 -0
  6. package/.vscode/settings.json +5 -0
  7. package/README.md +1 -0
  8. package/global.d.ts +173 -0
  9. package/package.json +25 -0
  10. package/src/assets.ts +43 -0
  11. package/src/backend.ts +369 -0
  12. package/src/common/collections.ts +349 -0
  13. package/src/common/color.ts +94 -0
  14. package/src/common/events.ts +45 -0
  15. package/src/common/exhaustive.ts +19 -0
  16. package/src/common/fp.ts +38 -0
  17. package/src/common/keycodes.ts +86 -0
  18. package/src/common/keys.ts +39 -0
  19. package/src/common/math.ts +98 -0
  20. package/src/common/perf.ts +72 -0
  21. package/src/common/random.ts +32 -0
  22. package/src/common/react.ts +65 -0
  23. package/src/common/redux.ts +196 -0
  24. package/src/common/storage.js +196 -0
  25. package/src/common/string.ts +173 -0
  26. package/src/common/timer.ts +68 -0
  27. package/src/common/type-utils.ts +41 -0
  28. package/src/common/types.ts +9 -0
  29. package/src/common/uuid.ts +24 -0
  30. package/src/common/vector.ts +51 -0
  31. package/src/components/AnimatedNumber.tsx +185 -0
  32. package/src/components/Autofocus.tsx +23 -0
  33. package/src/components/Blink.jsx +69 -0
  34. package/src/components/BlockQuote.tsx +15 -0
  35. package/src/components/BodyZoneSelector.tsx +149 -0
  36. package/src/components/Box.tsx +255 -0
  37. package/src/components/Button.tsx +415 -0
  38. package/src/components/ByondUi.jsx +121 -0
  39. package/src/components/Chart.tsx +160 -0
  40. package/src/components/Collapsible.tsx +45 -0
  41. package/src/components/ColorBox.tsx +30 -0
  42. package/src/components/Dialog.tsx +85 -0
  43. package/src/components/Dimmer.tsx +19 -0
  44. package/src/components/Divider.tsx +26 -0
  45. package/src/components/DmIcon.tsx +72 -0
  46. package/src/components/DraggableControl.jsx +282 -0
  47. package/src/components/Dropdown.tsx +246 -0
  48. package/src/components/FakeTerminal.jsx +52 -0
  49. package/src/components/FitText.tsx +99 -0
  50. package/src/components/Flex.tsx +105 -0
  51. package/src/components/Grid.tsx +44 -0
  52. package/src/components/Icon.tsx +91 -0
  53. package/src/components/Image.tsx +63 -0
  54. package/src/components/InfinitePlane.jsx +192 -0
  55. package/src/components/Input.tsx +181 -0
  56. package/src/components/KeyListener.tsx +40 -0
  57. package/src/components/Knob.tsx +185 -0
  58. package/src/components/LabeledControls.tsx +50 -0
  59. package/src/components/LabeledList.tsx +130 -0
  60. package/src/components/MenuBar.tsx +238 -0
  61. package/src/components/Modal.tsx +25 -0
  62. package/src/components/NoticeBox.tsx +48 -0
  63. package/src/components/NumberInput.tsx +328 -0
  64. package/src/components/Popper.tsx +100 -0
  65. package/src/components/ProgressBar.tsx +79 -0
  66. package/src/components/RestrictedInput.jsx +301 -0
  67. package/src/components/RoundGauge.tsx +189 -0
  68. package/src/components/Section.tsx +125 -0
  69. package/src/components/Slider.tsx +173 -0
  70. package/src/components/Stack.tsx +101 -0
  71. package/src/components/StyleableSection.tsx +30 -0
  72. package/src/components/Table.tsx +90 -0
  73. package/src/components/Tabs.tsx +90 -0
  74. package/src/components/TextArea.tsx +198 -0
  75. package/src/components/TimeDisplay.jsx +64 -0
  76. package/src/components/Tooltip.tsx +147 -0
  77. package/src/components/TrackOutsideClicks.tsx +35 -0
  78. package/src/components/VirtualList.tsx +69 -0
  79. package/src/constants.ts +355 -0
  80. package/src/debug/KitchenSink.jsx +56 -0
  81. package/src/debug/actions.js +11 -0
  82. package/src/debug/hooks.js +10 -0
  83. package/src/debug/index.ts +10 -0
  84. package/src/debug/middleware.js +86 -0
  85. package/src/debug/reducer.js +22 -0
  86. package/src/debug/selectors.js +7 -0
  87. package/src/drag.ts +280 -0
  88. package/src/events.ts +237 -0
  89. package/src/focus.ts +25 -0
  90. package/src/format.ts +173 -0
  91. package/src/hotkeys.ts +212 -0
  92. package/src/http.ts +16 -0
  93. package/src/layouts/Layout.tsx +75 -0
  94. package/src/layouts/NtosWindow.tsx +162 -0
  95. package/src/layouts/Pane.tsx +56 -0
  96. package/src/layouts/Window.tsx +227 -0
  97. package/src/renderer.ts +50 -0
  98. package/styles/base.scss +32 -0
  99. package/styles/colors.scss +92 -0
  100. package/styles/components/BlockQuote.scss +20 -0
  101. package/styles/components/Button.scss +175 -0
  102. package/styles/components/ColorBox.scss +12 -0
  103. package/styles/components/Dialog.scss +105 -0
  104. package/styles/components/Dimmer.scss +22 -0
  105. package/styles/components/Divider.scss +27 -0
  106. package/styles/components/Dropdown.scss +72 -0
  107. package/styles/components/Flex.scss +31 -0
  108. package/styles/components/Icon.scss +25 -0
  109. package/styles/components/Input.scss +68 -0
  110. package/styles/components/Knob.scss +131 -0
  111. package/styles/components/LabeledList.scss +49 -0
  112. package/styles/components/MenuBar.scss +75 -0
  113. package/styles/components/Modal.scss +14 -0
  114. package/styles/components/NoticeBox.scss +65 -0
  115. package/styles/components/NumberInput.scss +76 -0
  116. package/styles/components/ProgressBar.scss +63 -0
  117. package/styles/components/RoundGauge.scss +88 -0
  118. package/styles/components/Section.scss +143 -0
  119. package/styles/components/Slider.scss +54 -0
  120. package/styles/components/Stack.scss +59 -0
  121. package/styles/components/Table.scss +44 -0
  122. package/styles/components/Tabs.scss +144 -0
  123. package/styles/components/TextArea.scss +84 -0
  124. package/styles/components/Tooltip.scss +24 -0
  125. package/styles/functions.scss +79 -0
  126. package/styles/layouts/Layout.scss +57 -0
  127. package/styles/layouts/NtosHeader.scss +20 -0
  128. package/styles/layouts/NtosWindow.scss +26 -0
  129. package/styles/layouts/TitleBar.scss +111 -0
  130. package/styles/layouts/Window.scss +103 -0
  131. package/styles/main.scss +97 -0
  132. package/styles/reset.scss +68 -0
  133. package/styles/themes/abductor.scss +68 -0
  134. package/styles/themes/admin.scss +38 -0
  135. package/styles/themes/cardtable.scss +57 -0
  136. package/styles/themes/hackerman.scss +70 -0
  137. package/styles/themes/malfunction.scss +67 -0
  138. package/styles/themes/neutral.scss +50 -0
  139. package/styles/themes/ntOS95.scss +166 -0
  140. package/styles/themes/ntos.scss +44 -0
  141. package/styles/themes/ntos_cat.scss +148 -0
  142. package/styles/themes/ntos_darkmode.scss +44 -0
  143. package/styles/themes/ntos_lightmode.scss +67 -0
  144. package/styles/themes/ntos_spooky.scss +69 -0
  145. package/styles/themes/ntos_synth.scss +99 -0
  146. package/styles/themes/ntos_terminal.scss +112 -0
  147. package/styles/themes/paper.scss +184 -0
  148. package/styles/themes/retro.scss +72 -0
  149. package/styles/themes/spookyconsole.scss +73 -0
  150. package/styles/themes/syndicate.scss +67 -0
  151. package/styles/themes/wizard.scss +68 -0
  152. package/tsconfig.json +34 -0
package/src/backend.ts ADDED
@@ -0,0 +1,369 @@
1
+ /**
2
+ * This file provides a clear separation layer between backend updates
3
+ * and what state our React app sees.
4
+ *
5
+ * Sometimes backend can response without a "data" field, but our final
6
+ * state will still contain previous "data" because we are merging
7
+ * the response with already existing state.
8
+ *
9
+ * @file
10
+ * @copyright 2020 Aleksej Komarov
11
+ * @license MIT
12
+ */
13
+
14
+ import { perf } from './common/perf';
15
+ import { createAction } from './common/redux';
16
+
17
+ import { setupDrag } from './drag';
18
+ import { globalEvents } from './events';
19
+ import { focusMap } from './focus';
20
+ import { resumeRenderer, suspendRenderer } from './renderer';
21
+
22
+ export let globalStore;
23
+
24
+ export const setGlobalStore = (store) => {
25
+ globalStore = store;
26
+ };
27
+
28
+ export const backendUpdate = createAction('backend/update');
29
+ export const backendSetSharedState = createAction('backend/setSharedState');
30
+ export const backendSuspendStart = createAction('backend/suspendStart');
31
+
32
+ export const backendSuspendSuccess = () => ({
33
+ type: 'backend/suspendSuccess',
34
+ payload: {
35
+ timestamp: Date.now(),
36
+ },
37
+ });
38
+
39
+ const initialState = {
40
+ config: {},
41
+ data: {},
42
+ shared: {},
43
+ // Start as suspended
44
+ suspended: Date.now(),
45
+ suspending: false,
46
+ };
47
+
48
+ export const backendReducer = (state = initialState, action) => {
49
+ const { type, payload } = action;
50
+
51
+ if (type === 'backend/update') {
52
+ // Merge config
53
+ const config = {
54
+ ...state.config,
55
+ ...payload.config,
56
+ };
57
+ // Merge data
58
+ const data = {
59
+ ...state.data,
60
+ ...payload.static_data,
61
+ ...payload.data,
62
+ };
63
+ // Merge shared states
64
+ const shared = { ...state.shared };
65
+ if (payload.shared) {
66
+ for (let key of Object.keys(payload.shared)) {
67
+ const value = payload.shared[key];
68
+ if (value === '') {
69
+ shared[key] = undefined;
70
+ } else {
71
+ shared[key] = JSON.parse(value);
72
+ }
73
+ }
74
+ }
75
+ // Return new state
76
+ return {
77
+ ...state,
78
+ config,
79
+ data,
80
+ shared,
81
+ suspended: false,
82
+ };
83
+ }
84
+
85
+ if (type === 'backend/setSharedState') {
86
+ const { key, nextState } = payload;
87
+ return {
88
+ ...state,
89
+ shared: {
90
+ ...state.shared,
91
+ [key]: nextState,
92
+ },
93
+ };
94
+ }
95
+
96
+ if (type === 'backend/suspendStart') {
97
+ return {
98
+ ...state,
99
+ suspending: true,
100
+ };
101
+ }
102
+
103
+ if (type === 'backend/suspendSuccess') {
104
+ const { timestamp } = payload;
105
+ return {
106
+ ...state,
107
+ data: {},
108
+ shared: {},
109
+ config: {
110
+ ...state.config,
111
+ title: '',
112
+ status: 1,
113
+ },
114
+ suspending: false,
115
+ suspended: timestamp,
116
+ };
117
+ }
118
+
119
+ return state;
120
+ };
121
+
122
+ export const backendMiddleware = (store) => {
123
+ let fancyState;
124
+ let suspendInterval;
125
+
126
+ return (next) => (action) => {
127
+ const { suspended } = selectBackend(store.getState());
128
+ const { type, payload } = action;
129
+
130
+ if (type === 'update') {
131
+ store.dispatch(backendUpdate(payload));
132
+ return;
133
+ }
134
+
135
+ if (type === 'suspend') {
136
+ store.dispatch(backendSuspendSuccess());
137
+ return;
138
+ }
139
+
140
+ if (type === 'ping') {
141
+ Byond.sendMessage('ping/reply');
142
+ return;
143
+ }
144
+
145
+ if (type === 'byond/mousedown') {
146
+ globalEvents.emit('byond/mousedown');
147
+ }
148
+
149
+ if (type === 'byond/mouseup') {
150
+ globalEvents.emit('byond/mouseup');
151
+ }
152
+
153
+ if (type === 'byond/ctrldown') {
154
+ globalEvents.emit('byond/ctrldown');
155
+ }
156
+
157
+ if (type === 'byond/ctrlup') {
158
+ globalEvents.emit('byond/ctrlup');
159
+ }
160
+
161
+ if (type === 'backend/suspendStart' && !suspendInterval) {
162
+ // Keep sending suspend messages until it succeeds.
163
+ // It may fail multiple times due to topic rate limiting.
164
+ const suspendFn = () => Byond.sendMessage('suspend');
165
+ suspendFn();
166
+ suspendInterval = setInterval(suspendFn, 2000);
167
+ }
168
+
169
+ if (type === 'backend/suspendSuccess') {
170
+ suspendRenderer();
171
+ clearInterval(suspendInterval);
172
+ suspendInterval = undefined;
173
+ Byond.winset(Byond.windowId, {
174
+ 'is-visible': false,
175
+ });
176
+ setTimeout(() => focusMap());
177
+ }
178
+
179
+ if (type === 'backend/update') {
180
+ const fancy = payload.config?.window?.fancy;
181
+ // Initialize fancy state
182
+ if (fancyState === undefined) {
183
+ fancyState = fancy;
184
+ }
185
+ // React to changes in fancy
186
+ else if (fancyState !== fancy) {
187
+ fancyState = fancy;
188
+ Byond.winset(Byond.windowId, {
189
+ titlebar: !fancy,
190
+ 'can-resize': !fancy,
191
+ });
192
+ }
193
+ }
194
+
195
+ // Resume on incoming update
196
+ if (type === 'backend/update' && suspended) {
197
+ // Signal renderer that we have resumed
198
+ resumeRenderer();
199
+ // Setup drag
200
+ setupDrag();
201
+ // We schedule this for the next tick here because resizing and unhiding
202
+ // during the same tick will flash with a white background.
203
+ setTimeout(() => {
204
+ perf.mark('resume/start');
205
+ // Doublecheck if we are not re-suspended.
206
+ const { suspended } = selectBackend(store.getState());
207
+ if (suspended) {
208
+ return;
209
+ }
210
+ Byond.winset(Byond.windowId, {
211
+ 'is-visible': true,
212
+ });
213
+ perf.mark('resume/finish');
214
+ });
215
+ }
216
+
217
+ return next(action);
218
+ };
219
+ };
220
+
221
+ /**
222
+ * Sends an action to `ui_act` on `src_object` that this tgui window
223
+ * is associated with.
224
+ */
225
+ export const sendAct = (action: string, payload: object = {}) => {
226
+ // Validate that payload is an object
227
+ // prettier-ignore
228
+ const isObject = typeof payload === 'object'
229
+ && payload !== null
230
+ && !Array.isArray(payload);
231
+ if (!isObject) {
232
+ return;
233
+ }
234
+ Byond.sendMessage('act/' + action, payload);
235
+ };
236
+
237
+ type BackendState<TData> = {
238
+ config: {
239
+ title: string;
240
+ status: number;
241
+ interface: string;
242
+ refreshing: boolean;
243
+ window: {
244
+ key: string;
245
+ size: [number, number];
246
+ fancy: boolean;
247
+ locked: boolean;
248
+ };
249
+ client: {
250
+ ckey: string;
251
+ address: string;
252
+ computer_id: string;
253
+ };
254
+ user: {
255
+ name: string;
256
+ observer: number;
257
+ };
258
+ };
259
+ data: TData;
260
+ shared: Record<string, any>;
261
+ suspending: boolean;
262
+ suspended: boolean;
263
+ };
264
+
265
+ /**
266
+ * Selects a backend-related slice of Redux state
267
+ */
268
+ export const selectBackend = <TData>(state: any): BackendState<TData> =>
269
+ state.backend || {};
270
+
271
+ /**
272
+ * Get data from tgui backend.
273
+ *
274
+ * Includes the `act` function for performing DM actions.
275
+ */
276
+ export const useBackend = <TData>() => {
277
+ const state: BackendState<TData> = globalStore?.getState()?.backend;
278
+
279
+ return {
280
+ ...state,
281
+ act: sendAct,
282
+ };
283
+ };
284
+
285
+ /**
286
+ * A tuple that contains the state and a setter function for it.
287
+ */
288
+ type StateWithSetter<T> = [T, (nextState: T) => void];
289
+
290
+ /**
291
+ * Allocates state on Redux store without sharing it with other clients.
292
+ *
293
+ * Use it when you want to have a stateful variable in your component
294
+ * that persists between renders, but will be forgotten after you close
295
+ * the UI.
296
+ *
297
+ * It is a lot more performant than `setSharedState`.
298
+ *
299
+ * @param context React context.
300
+ * @param key Key which uniquely identifies this state in Redux store.
301
+ * @param initialState Initializes your global variable with this value.
302
+ * @deprecated Use useState and useEffect when you can. Pass the state as a prop.
303
+ */
304
+ export const useLocalState = <T>(
305
+ key: string,
306
+ initialState: T
307
+ ): StateWithSetter<T> => {
308
+ const state = globalStore?.getState()?.backend;
309
+ const sharedStates = state?.shared ?? {};
310
+ const sharedState = key in sharedStates ? sharedStates[key] : initialState;
311
+ return [
312
+ sharedState,
313
+ (nextState) => {
314
+ globalStore.dispatch(
315
+ backendSetSharedState({
316
+ key,
317
+ nextState:
318
+ typeof nextState === 'function'
319
+ ? nextState(sharedState)
320
+ : nextState,
321
+ })
322
+ );
323
+ },
324
+ ];
325
+ };
326
+
327
+ /**
328
+ * Allocates state on Redux store, and **shares** it with other clients
329
+ * in the game.
330
+ *
331
+ * Use it when you want to have a stateful variable in your component
332
+ * that persists not only between renders, but also gets pushed to other
333
+ * clients that observe this UI.
334
+ *
335
+ * This makes creation of observable s
336
+ *
337
+ * @param context React context.
338
+ * @param key Key which uniquely identifies this state in Redux store.
339
+ * @param initialState Initializes your global variable with this value.
340
+ */
341
+ export const useSharedState = <T>(
342
+ key: string,
343
+ initialState: T
344
+ ): StateWithSetter<T> => {
345
+ const state = globalStore?.getState()?.backend;
346
+ const sharedStates = state?.shared ?? {};
347
+ const sharedState = key in sharedStates ? sharedStates[key] : initialState;
348
+ return [
349
+ sharedState,
350
+ (nextState) => {
351
+ Byond.sendMessage({
352
+ type: 'setSharedState',
353
+ key,
354
+ value:
355
+ JSON.stringify(
356
+ typeof nextState === 'function' ? nextState(sharedState) : nextState
357
+ ) || '',
358
+ });
359
+ },
360
+ ];
361
+ };
362
+
363
+ export const useDispatch = () => {
364
+ return globalStore.dispatch;
365
+ };
366
+
367
+ export const useSelector = (selector: (state: any) => any) => {
368
+ return selector(globalStore?.getState());
369
+ };
@@ -0,0 +1,349 @@
1
+ /**
2
+ * @file
3
+ * @copyright 2020 Aleksej Komarov
4
+ * @license MIT
5
+ */
6
+
7
+ /**
8
+ * Iterates over elements of collection, returning an array of all elements
9
+ * iteratee returns truthy for. The predicate is invoked with three
10
+ * arguments: (value, index|key, collection).
11
+ *
12
+ * If collection is 'null' or 'undefined', it will be returned "as is"
13
+ * without emitting any errors (which can be useful in some cases).
14
+ */
15
+ export const filter = <T>(
16
+ collection: T[],
17
+ iterateeFn: (input: T, index: number, collection: T[]) => boolean
18
+ ): T[] => {
19
+ if (collection === null || collection === undefined) {
20
+ return collection;
21
+ }
22
+ if (Array.isArray(collection)) {
23
+ const result: T[] = [];
24
+ for (let i = 0; i < collection.length; i++) {
25
+ const item = collection[i];
26
+ if (iterateeFn(item, i, collection)) {
27
+ result.push(item);
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+ throw new Error(`filter() can't iterate on type ${typeof collection}`);
33
+ };
34
+
35
+ type MapFunction = {
36
+ <T, U>(
37
+ collection: T[],
38
+ iterateeFn: (value: T, index: number, collection: T[]) => U
39
+ ): U[];
40
+
41
+ <T, U, K extends string | number>(
42
+ collection: Record<K, T>,
43
+ iterateeFn: (value: T, index: K, collection: Record<K, T>) => U
44
+ ): U[];
45
+ };
46
+
47
+ /**
48
+ * Creates an array of values by running each element in collection
49
+ * thru an iteratee function. The iteratee is invoked with three
50
+ * arguments: (value, index|key, collection).
51
+ *
52
+ * If collection is 'null' or 'undefined', it will be returned "as is"
53
+ * without emitting any errors (which can be useful in some cases).
54
+ */
55
+ export const map: MapFunction = (collection, iterateeFn) => {
56
+ if (collection === null || collection === undefined) {
57
+ return collection;
58
+ }
59
+
60
+ if (Array.isArray(collection)) {
61
+ const result: unknown[] = [];
62
+ for (let i = 0; i < collection.length; i++) {
63
+ result.push(iterateeFn(collection[i], i, collection));
64
+ }
65
+ return result;
66
+ }
67
+
68
+ if (typeof collection === 'object') {
69
+ const result: unknown[] = [];
70
+ for (let i in collection) {
71
+ if (Object.prototype.hasOwnProperty.call(collection, i)) {
72
+ result.push(iterateeFn(collection[i], i, collection));
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ throw new Error(`map() can't iterate on type ${typeof collection}`);
79
+ };
80
+
81
+ const COMPARATOR = (objA, objB) => {
82
+ const criteriaA = objA.criteria;
83
+ const criteriaB = objB.criteria;
84
+ const length = criteriaA.length;
85
+ for (let i = 0; i < length; i++) {
86
+ const a = criteriaA[i];
87
+ const b = criteriaB[i];
88
+ if (a < b) {
89
+ return -1;
90
+ }
91
+ if (a > b) {
92
+ return 1;
93
+ }
94
+ }
95
+ return 0;
96
+ };
97
+
98
+ /**
99
+ * Creates an array of elements, sorted in ascending order by the results
100
+ * of running each element in a collection thru each iteratee.
101
+ *
102
+ * Iteratees are called with one argument (value).
103
+ */
104
+ export const sortBy = <T>(
105
+ array: T[],
106
+ ...iterateeFns: ((input: T) => unknown)[]
107
+ ): T[] => {
108
+ if (!Array.isArray(array)) {
109
+ return array;
110
+ }
111
+ let length = array.length;
112
+ // Iterate over the array to collect criteria to sort it by
113
+ let mappedArray: {
114
+ criteria: unknown[];
115
+ value: T;
116
+ }[] = [];
117
+ for (let i = 0; i < length; i++) {
118
+ const value = array[i];
119
+ mappedArray.push({
120
+ criteria: iterateeFns.map((fn) => fn(value)),
121
+ value,
122
+ });
123
+ }
124
+ // Sort criteria using the base comparator
125
+ mappedArray.sort(COMPARATOR);
126
+
127
+ // Unwrap values
128
+ const values: T[] = [];
129
+ while (length--) {
130
+ values[length] = mappedArray[length].value;
131
+ }
132
+ return values;
133
+ };
134
+
135
+ export const sort = <T>(array: T[]): T[] => sortBy(array);
136
+
137
+ /**
138
+ * Returns a range of numbers from start to end, exclusively.
139
+ * For example, range(0, 5) will return [0, 1, 2, 3, 4].
140
+ */
141
+ export const range = (start: number, end: number): number[] =>
142
+ new Array(end - start).fill(null).map((_, index) => index + start);
143
+
144
+ type ReduceFunction = {
145
+ <T, U>(
146
+ array: T[],
147
+ reducerFn: (
148
+ accumulator: U,
149
+ currentValue: T,
150
+ currentIndex: number,
151
+ array: T[]
152
+ ) => U,
153
+ initialValue: U
154
+ ): U;
155
+ <T>(
156
+ array: T[],
157
+ reducerFn: (
158
+ accumulator: T,
159
+ currentValue: T,
160
+ currentIndex: number,
161
+ array: T[]
162
+ ) => T
163
+ ): T;
164
+ };
165
+
166
+ /**
167
+ * A fast implementation of reduce.
168
+ */
169
+ export const reduce: ReduceFunction = (array, reducerFn, initialValue?) => {
170
+ const length = array.length;
171
+ let i: number;
172
+ let result;
173
+ if (initialValue === undefined) {
174
+ i = 1;
175
+ result = array[0];
176
+ } else {
177
+ i = 0;
178
+ result = initialValue;
179
+ }
180
+ for (; i < length; i++) {
181
+ result = reducerFn(result, array[i], i, array);
182
+ }
183
+ return result;
184
+ };
185
+
186
+ /**
187
+ * Creates a duplicate-free version of an array, using SameValueZero for
188
+ * equality comparisons, in which only the first occurrence of each element
189
+ * is kept. The order of result values is determined by the order they occur
190
+ * in the array.
191
+ *
192
+ * It accepts iteratee which is invoked for each element in array to generate
193
+ * the criterion by which uniqueness is computed. The order of result values
194
+ * is determined by the order they occur in the array. The iteratee is
195
+ * invoked with one argument: value.
196
+ */
197
+ export const uniqBy = <T extends unknown>(
198
+ array: T[],
199
+ iterateeFn?: (value: T) => unknown
200
+ ): T[] => {
201
+ const { length } = array;
202
+ const result: T[] = [];
203
+ const seen: unknown[] = iterateeFn ? [] : result;
204
+ let index = -1;
205
+
206
+ outer: while (++index < length) {
207
+ let value: T | 0 = array[index];
208
+ const computed = iterateeFn ? iterateeFn(value) : value;
209
+ if (computed === computed) {
210
+ let seenIndex = seen.length;
211
+ while (seenIndex--) {
212
+ if (seen[seenIndex] === computed) {
213
+ continue outer;
214
+ }
215
+ }
216
+ if (iterateeFn) {
217
+ seen.push(computed);
218
+ }
219
+ result.push(value);
220
+ } else if (!seen.includes(computed)) {
221
+ if (seen !== result) {
222
+ seen.push(computed);
223
+ }
224
+ result.push(value);
225
+ }
226
+ }
227
+ return result;
228
+ };
229
+
230
+ export const uniq = <T>(array: T[]): T[] => uniqBy(array);
231
+
232
+ type Zip<T extends unknown[][]> = {
233
+ [I in keyof T]: T[I] extends (infer U)[] ? U : never;
234
+ }[];
235
+
236
+ /**
237
+ * Creates an array of grouped elements, the first of which contains
238
+ * the first elements of the given arrays, the second of which contains
239
+ * the second elements of the given arrays, and so on.
240
+ */
241
+ export const zip = <T extends unknown[][]>(...arrays: T): Zip<T> => {
242
+ if (arrays.length === 0) {
243
+ return [];
244
+ }
245
+ const numArrays = arrays.length;
246
+ const numValues = arrays[0].length;
247
+ const result: Zip<T> = [];
248
+ for (let valueIndex = 0; valueIndex < numValues; valueIndex++) {
249
+ const entry: unknown[] = [];
250
+ for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) {
251
+ entry.push(arrays[arrayIndex][valueIndex]);
252
+ }
253
+
254
+ // I tried everything to remove this any, and have no idea how to do it.
255
+ result.push(entry as any);
256
+ }
257
+ return result;
258
+ };
259
+
260
+ const binarySearch = <T, U = unknown>(
261
+ getKey: (value: T) => U,
262
+ collection: readonly T[],
263
+ inserting: T
264
+ ): number => {
265
+ if (collection.length === 0) {
266
+ return 0;
267
+ }
268
+
269
+ const insertingKey = getKey(inserting);
270
+
271
+ let [low, high] = [0, collection.length];
272
+
273
+ // Because we have checked if the collection is empty, it's impossible
274
+ // for this to be used before assignment.
275
+ let compare: U = undefined as unknown as U;
276
+ let middle = 0;
277
+
278
+ while (low < high) {
279
+ middle = (low + high) >> 1;
280
+
281
+ compare = getKey(collection[middle]);
282
+
283
+ if (compare < insertingKey) {
284
+ low = middle + 1;
285
+ } else if (compare === insertingKey) {
286
+ return middle;
287
+ } else {
288
+ high = middle;
289
+ }
290
+ }
291
+
292
+ return compare > insertingKey ? middle : middle + 1;
293
+ };
294
+
295
+ export const binaryInsertWith = <T, U = unknown>(
296
+ collection: readonly T[],
297
+ value: T,
298
+ getKey: (value: T) => U
299
+ ): T[] => {
300
+ const copy = [...collection];
301
+ copy.splice(binarySearch(getKey, collection, value), 0, value);
302
+ return copy;
303
+ };
304
+
305
+ /**
306
+ * This method takes a collection of items and a number, returning a collection
307
+ * of collections, where the maximum amount of items in each is that second arg
308
+ */
309
+ export const paginate = <T>(collection: T[], maxPerPage: number): T[][] => {
310
+ const pages: T[][] = [];
311
+ let page: T[] = [];
312
+ let itemsToAdd = maxPerPage;
313
+
314
+ for (const item of collection) {
315
+ page.push(item);
316
+ itemsToAdd--;
317
+ if (!itemsToAdd) {
318
+ itemsToAdd = maxPerPage;
319
+ pages.push(page);
320
+ page = [];
321
+ }
322
+ }
323
+ if (page.length) {
324
+ pages.push(page);
325
+ }
326
+ return pages;
327
+ };
328
+
329
+ const isObject = (obj: unknown): obj is object =>
330
+ typeof obj === 'object' && obj !== null;
331
+
332
+ // Does a deep merge of two objects. DO NOT FEED CIRCULAR OBJECTS!!
333
+ export const deepMerge = (...objects: any[]): any => {
334
+ const target = {};
335
+ for (const object of objects) {
336
+ for (const key of Object.keys(object)) {
337
+ const targetValue = target[key];
338
+ const objectValue = object[key];
339
+ if (Array.isArray(targetValue) && Array.isArray(objectValue)) {
340
+ target[key] = [...targetValue, ...objectValue];
341
+ } else if (isObject(targetValue) && isObject(objectValue)) {
342
+ target[key] = deepMerge(targetValue, objectValue);
343
+ } else {
344
+ target[key] = objectValue;
345
+ }
346
+ }
347
+ }
348
+ return target;
349
+ };