tgui-core 1.0.2 → 1.0.4

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 (158) hide show
  1. package/{src/components → components}/AnimatedNumber.tsx +185 -185
  2. package/{src/components → components}/BlockQuote.tsx +15 -15
  3. package/{src/components → components}/BodyZoneSelector.tsx +149 -149
  4. package/{src/components → components}/Box.tsx +255 -255
  5. package/{src/components → components}/Button.tsx +415 -415
  6. package/{src/components → components}/ByondUi.jsx +121 -121
  7. package/{src/components → components}/Chart.tsx +160 -160
  8. package/{src/components → components}/ColorBox.tsx +30 -30
  9. package/{src/components → components}/Dimmer.tsx +19 -19
  10. package/{src/components → components}/Divider.tsx +26 -26
  11. package/{src/components → components}/DmIcon.tsx +72 -72
  12. package/{src/components → components}/DraggableControl.jsx +282 -282
  13. package/{src/components → components}/Dropdown.tsx +246 -246
  14. package/{src/components → components}/Flex.tsx +105 -105
  15. package/{src/components → components}/Icon.tsx +91 -91
  16. package/{src/components → components}/Input.tsx +181 -181
  17. package/{src/components → components}/KeyListener.tsx +40 -40
  18. package/{src/components → components}/Knob.tsx +185 -185
  19. package/{src/components → components}/LabeledList.tsx +130 -130
  20. package/{src/components → components}/MenuBar.tsx +233 -238
  21. package/{src/components → components}/Modal.tsx +25 -25
  22. package/{src/components → components}/NoticeBox.tsx +48 -48
  23. package/{src/components → components}/NumberInput.tsx +328 -328
  24. package/{src/components → components}/ProgressBar.tsx +79 -79
  25. package/{src/components → components}/RestrictedInput.jsx +301 -301
  26. package/{src/components → components}/RoundGauge.tsx +189 -189
  27. package/{src/components → components}/Section.tsx +125 -125
  28. package/{src/components → components}/Slider.tsx +173 -173
  29. package/{src/components → components}/Stack.tsx +101 -101
  30. package/{src/components → components}/Table.tsx +90 -90
  31. package/{src/components → components}/Tabs.tsx +90 -90
  32. package/{src/components → components}/TextArea.tsx +198 -198
  33. package/{src/components → components}/TimeDisplay.jsx +64 -64
  34. package/components/index.ts +51 -0
  35. package/{src/debug/KitchenSink.jsx → debug/KitchenSink.tsx} +56 -56
  36. package/{src/debug/actions.js → debug/actions.ts} +11 -11
  37. package/{src/debug/hooks.js → debug/hooks.ts} +10 -10
  38. package/{src/debug/middleware.js → debug/middleware.ts} +67 -86
  39. package/{src/debug/reducer.js → debug/reducer.ts} +27 -22
  40. package/{src/debug/selectors.js → debug/selectors.ts} +7 -7
  41. package/{src/layouts → layouts}/Layout.tsx +75 -75
  42. package/{src/layouts → layouts}/NtosWindow.tsx +162 -162
  43. package/{src/layouts → layouts}/Pane.tsx +56 -56
  44. package/{src/layouts → layouts}/Window.tsx +227 -227
  45. package/layouts/index.ts +10 -0
  46. package/package.json +3 -2
  47. package/src/assets.ts +43 -43
  48. package/src/backend.ts +368 -369
  49. package/src/drag.ts +280 -280
  50. package/src/events.ts +237 -237
  51. package/src/hotkeys.ts +212 -212
  52. package/src/renderer.ts +50 -50
  53. package/stories/Blink.stories.tsx +20 -0
  54. package/stories/BlockQuote.stories.tsx +23 -0
  55. package/stories/Box.stories.tsx +27 -0
  56. package/stories/Button.stories.tsx +68 -0
  57. package/stories/ByondUi.stories.tsx +45 -0
  58. package/stories/Collapsible.stories.tsx +23 -0
  59. package/stories/Flex.stories.tsx +68 -0
  60. package/stories/Input.stories.tsx +124 -0
  61. package/stories/LabeledList.stories.tsx +73 -0
  62. package/stories/Popper.stories.tsx +58 -0
  63. package/stories/ProgressBar.stories.tsx +58 -0
  64. package/stories/Stack.stories.tsx +55 -0
  65. package/stories/Storage.stories.tsx +46 -0
  66. package/stories/Themes.stories.tsx +30 -0
  67. package/stories/Tooltip.stories.tsx +48 -0
  68. package/stories/common.tsx +19 -0
  69. package/tsconfig.json +0 -21
  70. package/src/components/Grid.tsx +0 -44
  71. /package/{src/common → common}/collections.ts +0 -0
  72. /package/{src/common → common}/color.ts +0 -0
  73. /package/{src/common → common}/events.ts +0 -0
  74. /package/{src/common → common}/exhaustive.ts +0 -0
  75. /package/{src/common → common}/fp.ts +0 -0
  76. /package/{src/common → common}/keycodes.ts +0 -0
  77. /package/{src/common → common}/keys.ts +0 -0
  78. /package/{src/common → common}/math.ts +0 -0
  79. /package/{src/common → common}/perf.ts +0 -0
  80. /package/{src/common → common}/random.ts +0 -0
  81. /package/{src/common → common}/react.ts +0 -0
  82. /package/{src/common → common}/redux.ts +0 -0
  83. /package/{src/common → common}/storage.js +0 -0
  84. /package/{src/common → common}/string.ts +0 -0
  85. /package/{src/common → common}/timer.ts +0 -0
  86. /package/{src/common → common}/type-utils.ts +0 -0
  87. /package/{src/common → common}/types.ts +0 -0
  88. /package/{src/common → common}/uuid.ts +0 -0
  89. /package/{src/common → common}/vector.ts +0 -0
  90. /package/{src/components → components}/Autofocus.tsx +0 -0
  91. /package/{src/components → components}/Blink.jsx +0 -0
  92. /package/{src/components → components}/Collapsible.tsx +0 -0
  93. /package/{src/components → components}/Dialog.tsx +0 -0
  94. /package/{src/components → components}/FakeTerminal.jsx +0 -0
  95. /package/{src/components → components}/FitText.tsx +0 -0
  96. /package/{src/components → components}/Image.tsx +0 -0
  97. /package/{src/components → components}/InfinitePlane.jsx +0 -0
  98. /package/{src/components → components}/LabeledControls.tsx +0 -0
  99. /package/{src/components → components}/Popper.tsx +0 -0
  100. /package/{src/components → components}/StyleableSection.tsx +0 -0
  101. /package/{src/components → components}/Tooltip.tsx +0 -0
  102. /package/{src/components → components}/TrackOutsideClicks.tsx +0 -0
  103. /package/{src/components → components}/VirtualList.tsx +0 -0
  104. /package/{src/debug → debug}/index.ts +0 -0
  105. /package/{src/styles → styles}/base.scss +0 -0
  106. /package/{src/styles → styles}/colors.scss +0 -0
  107. /package/{src/styles → styles}/components/BlockQuote.scss +0 -0
  108. /package/{src/styles → styles}/components/Button.scss +0 -0
  109. /package/{src/styles → styles}/components/ColorBox.scss +0 -0
  110. /package/{src/styles → styles}/components/Dialog.scss +0 -0
  111. /package/{src/styles → styles}/components/Dimmer.scss +0 -0
  112. /package/{src/styles → styles}/components/Divider.scss +0 -0
  113. /package/{src/styles → styles}/components/Dropdown.scss +0 -0
  114. /package/{src/styles → styles}/components/Flex.scss +0 -0
  115. /package/{src/styles → styles}/components/Icon.scss +0 -0
  116. /package/{src/styles → styles}/components/Input.scss +0 -0
  117. /package/{src/styles → styles}/components/Knob.scss +0 -0
  118. /package/{src/styles → styles}/components/LabeledList.scss +0 -0
  119. /package/{src/styles → styles}/components/MenuBar.scss +0 -0
  120. /package/{src/styles → styles}/components/Modal.scss +0 -0
  121. /package/{src/styles → styles}/components/NoticeBox.scss +0 -0
  122. /package/{src/styles → styles}/components/NumberInput.scss +0 -0
  123. /package/{src/styles → styles}/components/ProgressBar.scss +0 -0
  124. /package/{src/styles → styles}/components/RoundGauge.scss +0 -0
  125. /package/{src/styles → styles}/components/Section.scss +0 -0
  126. /package/{src/styles → styles}/components/Slider.scss +0 -0
  127. /package/{src/styles → styles}/components/Stack.scss +0 -0
  128. /package/{src/styles → styles}/components/Table.scss +0 -0
  129. /package/{src/styles → styles}/components/Tabs.scss +0 -0
  130. /package/{src/styles → styles}/components/TextArea.scss +0 -0
  131. /package/{src/styles → styles}/components/Tooltip.scss +0 -0
  132. /package/{src/styles → styles}/functions.scss +0 -0
  133. /package/{src/styles → styles}/layouts/Layout.scss +0 -0
  134. /package/{src/styles → styles}/layouts/NtosHeader.scss +0 -0
  135. /package/{src/styles → styles}/layouts/NtosWindow.scss +0 -0
  136. /package/{src/styles → styles}/layouts/TitleBar.scss +0 -0
  137. /package/{src/styles → styles}/layouts/Window.scss +0 -0
  138. /package/{src/styles → styles}/main.scss +0 -0
  139. /package/{src/styles → styles}/reset.scss +0 -0
  140. /package/{src/styles → styles}/themes/abductor.scss +0 -0
  141. /package/{src/styles → styles}/themes/admin.scss +0 -0
  142. /package/{src/styles → styles}/themes/cardtable.scss +0 -0
  143. /package/{src/styles → styles}/themes/hackerman.scss +0 -0
  144. /package/{src/styles → styles}/themes/malfunction.scss +0 -0
  145. /package/{src/styles → styles}/themes/neutral.scss +0 -0
  146. /package/{src/styles → styles}/themes/ntOS95.scss +0 -0
  147. /package/{src/styles → styles}/themes/ntos.scss +0 -0
  148. /package/{src/styles → styles}/themes/ntos_cat.scss +0 -0
  149. /package/{src/styles → styles}/themes/ntos_darkmode.scss +0 -0
  150. /package/{src/styles → styles}/themes/ntos_lightmode.scss +0 -0
  151. /package/{src/styles → styles}/themes/ntos_spooky.scss +0 -0
  152. /package/{src/styles → styles}/themes/ntos_synth.scss +0 -0
  153. /package/{src/styles → styles}/themes/ntos_terminal.scss +0 -0
  154. /package/{src/styles → styles}/themes/paper.scss +0 -0
  155. /package/{src/styles → styles}/themes/retro.scss +0 -0
  156. /package/{src/styles → styles}/themes/spookyconsole.scss +0 -0
  157. /package/{src/styles → styles}/themes/syndicate.scss +0 -0
  158. /package/{src/styles → styles}/themes/wizard.scss +0 -0
@@ -1,301 +1,301 @@
1
- import { KEY_ENTER, KEY_ESCAPE } from '../common/keycodes';
2
- import { clamp } from '../common/math';
3
- import { classes } from '../common/react';
4
- import { Component, createRef } from 'react';
5
-
6
- import { Box } from './Box';
7
-
8
- const DEFAULT_MIN = 0;
9
- const DEFAULT_MAX = 10000;
10
-
11
- /**
12
- * Sanitize a number without interfering with writing negative or floating point numbers.
13
- * Handling dots and minuses in a user friendly way
14
- * @param value {String}
15
- * @param minValue {Number}
16
- * @param maxValue {Number}
17
- * @param allowFloats {Boolean}
18
- * @returns {String}
19
- */
20
- const softSanitizeNumber = (value, minValue, maxValue, allowFloats) => {
21
- const minimum = minValue || DEFAULT_MIN;
22
- const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
23
-
24
- let sanitizedString = allowFloats
25
- ? value.replace(/[^\-\d.]/g, '')
26
- : value.replace(/[^\-\d]/g, '');
27
-
28
- if (allowFloats) {
29
- sanitizedString = maybeLeadWithMin(sanitizedString, minimum);
30
- sanitizedString = keepOnlyFirstOccurrence('.', sanitizedString);
31
- }
32
- if (minValue < 0) {
33
- sanitizedString = maybeMoveMinusSign(sanitizedString);
34
- sanitizedString = keepOnlyFirstOccurrence('-', sanitizedString);
35
- } else {
36
- sanitizedString = sanitizedString.replaceAll('-', '');
37
- }
38
- if (minimum <= 1 && maximum >= 0) {
39
- return clampGuessedNumber(sanitizedString, minimum, maximum, allowFloats);
40
- }
41
- return sanitizedString;
42
- };
43
-
44
- /**
45
- * Clamping the input to the restricted range, making the Input smart for min <= 1 and max >= 0
46
- * @param softSanitizedNumber {String}
47
- * @param allowFloats {Boolean}
48
- * @returns {string}
49
- */
50
- const clampGuessedNumber = (
51
- softSanitizedNumber,
52
- minValue,
53
- maxValue,
54
- allowFloats
55
- ) => {
56
- let parsed = allowFloats
57
- ? parseFloat(softSanitizedNumber)
58
- : parseInt(softSanitizedNumber, 10);
59
- if (
60
- !isNaN(parsed) &&
61
- (softSanitizedNumber.slice(-1) !== '.' || parsed < Math.floor(minValue))
62
- ) {
63
- let clamped = clamp(parsed, minValue, maxValue);
64
- if (parsed !== clamped) {
65
- return String(clamped);
66
- }
67
- }
68
- return softSanitizedNumber;
69
- };
70
-
71
- /**
72
- * Translate x- to -x and -x- to x
73
- * @param string {String}
74
- * @returns {string}
75
- */
76
- const maybeMoveMinusSign = (string) => {
77
- let retString = string;
78
- // if minus sign is present but not first
79
- let minusIdx = string.indexOf('-');
80
- if (minusIdx > 0) {
81
- string = string.replace('-', '');
82
- retString = '-'.concat(string);
83
- } else if (minusIdx === 0) {
84
- if (string.indexOf('-', minusIdx + 1) > 0) {
85
- retString = string.replaceAll('-', '');
86
- }
87
- }
88
- return retString;
89
- };
90
-
91
- /**
92
- * Translate . to min. or .x to mim.x or -. to -min.
93
- * @param string {String}
94
- */
95
- const maybeLeadWithMin = (string, min) => {
96
- let retString = string;
97
- let cuttedVal = Math.sign(min) * Math.floor(Math.abs(min));
98
- if (string.indexOf('.') === 0) {
99
- retString = String(cuttedVal).concat(string);
100
- } else if (string.indexOf('-') === 0 && string.indexOf('.') === 1) {
101
- retString = cuttedVal + '.'.concat(string.slice(2));
102
- }
103
- return retString;
104
- };
105
-
106
- /**
107
- * Keep only the first occurrence of a string in another string.
108
- * @param needle {String}
109
- * @param haystack {String}
110
- * @returns {string}
111
- */
112
- const keepOnlyFirstOccurrence = (needle, haystack) => {
113
- const idx = haystack.indexOf(needle);
114
- const len = haystack.length;
115
- let newHaystack = haystack;
116
- if (idx !== -1 && idx < len - 1) {
117
- let trailingString = haystack.slice(idx + 1, len);
118
- trailingString = trailingString.replaceAll(needle, '');
119
- newHaystack = haystack.slice(0, idx + 1).concat(trailingString);
120
- }
121
- return newHaystack;
122
- };
123
-
124
- /**
125
- * Takes a string input and parses integers or floats from it.
126
- * If none: Minimum is set.
127
- * Else: Clamps it to the given range.
128
- */
129
- const getClampedNumber = (value, minValue, maxValue, allowFloats) => {
130
- const minimum = minValue || DEFAULT_MIN;
131
- const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
132
- if (!value || !value.length) {
133
- return String(minimum);
134
- }
135
- let parsedValue = allowFloats
136
- ? parseFloat(value.replace(/[^\-\d.]/g, ''))
137
- : parseInt(value.replace(/[^\-\d]/g, ''), 10);
138
- if (isNaN(parsedValue)) {
139
- return String(minimum);
140
- } else {
141
- return String(clamp(parsedValue, minimum, maximum));
142
- }
143
- };
144
-
145
- export class RestrictedInput extends Component {
146
- constructor(props) {
147
- super(props);
148
- this.inputRef = createRef();
149
- this.state = {
150
- editing: false,
151
- };
152
- this.handleBlur = (e) => {
153
- const { maxValue, minValue, onBlur, allowFloats } = this.props;
154
- const { editing } = this.state;
155
- if (editing) {
156
- this.setEditing(false);
157
- }
158
- const safeNum = getClampedNumber(
159
- e.target.value,
160
- minValue,
161
- maxValue,
162
- allowFloats
163
- );
164
- if (onBlur) {
165
- onBlur(e, +safeNum);
166
- }
167
- };
168
- this.handleChange = (e) => {
169
- const { maxValue, minValue, onChange, allowFloats } = this.props;
170
- e.target.value = softSanitizeNumber(
171
- e.target.value,
172
- minValue,
173
- maxValue,
174
- allowFloats
175
- );
176
- if (onChange) {
177
- onChange(e, +e.target.value);
178
- }
179
- };
180
- this.handleFocus = (e) => {
181
- const { editing } = this.state;
182
- if (!editing) {
183
- this.setEditing(true);
184
- }
185
- };
186
- this.handleInput = (e) => {
187
- const { editing } = this.state;
188
- const { onInput } = this.props;
189
- if (!editing) {
190
- this.setEditing(true);
191
- }
192
- if (onInput) {
193
- onInput(e, +e.target.value);
194
- }
195
- };
196
- this.handleKeyDown = (e) => {
197
- const { maxValue, minValue, onChange, onEnter, allowFloats } = this.props;
198
- if (e.keyCode === KEY_ENTER) {
199
- const safeNum = getClampedNumber(
200
- e.target.value,
201
- minValue,
202
- maxValue,
203
- allowFloats
204
- );
205
- this.setEditing(false);
206
- if (onChange) {
207
- onChange(e, +safeNum);
208
- }
209
- if (onEnter) {
210
- onEnter(e, +safeNum);
211
- }
212
- e.target.blur();
213
- return;
214
- }
215
- if (e.keyCode === KEY_ESCAPE) {
216
- if (this.props.onEscape) {
217
- this.props.onEscape(e);
218
- return;
219
- }
220
- this.setEditing(false);
221
- e.target.value = this.props.value;
222
- e.target.blur();
223
- return;
224
- }
225
- };
226
- }
227
-
228
- componentDidMount() {
229
- const { maxValue, minValue, allowFloats } = this.props;
230
- const nextValue = this.props.value?.toString();
231
- const input = this.inputRef.current;
232
- if (input) {
233
- input.value = getClampedNumber(
234
- nextValue,
235
- minValue,
236
- maxValue,
237
- allowFloats
238
- );
239
- }
240
- if (this.props.autoFocus || this.props.autoSelect) {
241
- setTimeout(() => {
242
- input.focus();
243
-
244
- if (this.props.autoSelect) {
245
- input.select();
246
- }
247
- }, 1);
248
- }
249
- }
250
-
251
- componentDidUpdate(prevProps, _) {
252
- const { maxValue, minValue, allowFloats } = this.props;
253
- const { editing } = this.state;
254
- const prevValue = prevProps.value?.toString();
255
- const nextValue = this.props.value?.toString();
256
- const input = this.inputRef.current;
257
- if (input && !editing) {
258
- if (nextValue !== prevValue && nextValue !== input.value) {
259
- input.value = getClampedNumber(
260
- nextValue,
261
- minValue,
262
- maxValue,
263
- allowFloats
264
- );
265
- }
266
- }
267
- }
268
-
269
- setEditing(editing) {
270
- this.setState({ editing });
271
- }
272
-
273
- render() {
274
- const { props } = this;
275
- const { onChange, onEnter, onInput, onBlur, value, ...boxProps } = props;
276
- const { className, fluid, monospace, ...rest } = boxProps;
277
- return (
278
- <Box
279
- className={classes([
280
- 'Input',
281
- fluid && 'Input--fluid',
282
- monospace && 'Input--monospace',
283
- className,
284
- ])}
285
- {...rest}
286
- >
287
- <div className="Input__baseline">.</div>
288
- <input
289
- className="Input__input"
290
- onChange={this.handleChange}
291
- onInput={this.handleInput}
292
- onFocus={this.handleFocus}
293
- onBlur={this.handleBlur}
294
- onKeyDown={this.handleKeyDown}
295
- ref={this.inputRef}
296
- type="number | string"
297
- />
298
- </Box>
299
- );
300
- }
301
- }
1
+ import { KEY_ENTER, KEY_ESCAPE } from '../common/keycodes';
2
+ import { clamp } from '../common/math';
3
+ import { classes } from '../common/react';
4
+ import { Component, createRef } from 'react';
5
+
6
+ import { Box } from './Box';
7
+
8
+ const DEFAULT_MIN = 0;
9
+ const DEFAULT_MAX = 10000;
10
+
11
+ /**
12
+ * Sanitize a number without interfering with writing negative or floating point numbers.
13
+ * Handling dots and minuses in a user friendly way
14
+ * @param value {String}
15
+ * @param minValue {Number}
16
+ * @param maxValue {Number}
17
+ * @param allowFloats {Boolean}
18
+ * @returns {String}
19
+ */
20
+ const softSanitizeNumber = (value, minValue, maxValue, allowFloats) => {
21
+ const minimum = minValue || DEFAULT_MIN;
22
+ const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
23
+
24
+ let sanitizedString = allowFloats
25
+ ? value.replace(/[^\-\d.]/g, '')
26
+ : value.replace(/[^\-\d]/g, '');
27
+
28
+ if (allowFloats) {
29
+ sanitizedString = maybeLeadWithMin(sanitizedString, minimum);
30
+ sanitizedString = keepOnlyFirstOccurrence('.', sanitizedString);
31
+ }
32
+ if (minValue < 0) {
33
+ sanitizedString = maybeMoveMinusSign(sanitizedString);
34
+ sanitizedString = keepOnlyFirstOccurrence('-', sanitizedString);
35
+ } else {
36
+ sanitizedString = sanitizedString.replaceAll('-', '');
37
+ }
38
+ if (minimum <= 1 && maximum >= 0) {
39
+ return clampGuessedNumber(sanitizedString, minimum, maximum, allowFloats);
40
+ }
41
+ return sanitizedString;
42
+ };
43
+
44
+ /**
45
+ * Clamping the input to the restricted range, making the Input smart for min <= 1 and max >= 0
46
+ * @param softSanitizedNumber {String}
47
+ * @param allowFloats {Boolean}
48
+ * @returns {string}
49
+ */
50
+ const clampGuessedNumber = (
51
+ softSanitizedNumber,
52
+ minValue,
53
+ maxValue,
54
+ allowFloats
55
+ ) => {
56
+ let parsed = allowFloats
57
+ ? parseFloat(softSanitizedNumber)
58
+ : parseInt(softSanitizedNumber, 10);
59
+ if (
60
+ !isNaN(parsed) &&
61
+ (softSanitizedNumber.slice(-1) !== '.' || parsed < Math.floor(minValue))
62
+ ) {
63
+ let clamped = clamp(parsed, minValue, maxValue);
64
+ if (parsed !== clamped) {
65
+ return String(clamped);
66
+ }
67
+ }
68
+ return softSanitizedNumber;
69
+ };
70
+
71
+ /**
72
+ * Translate x- to -x and -x- to x
73
+ * @param string {String}
74
+ * @returns {string}
75
+ */
76
+ const maybeMoveMinusSign = (string) => {
77
+ let retString = string;
78
+ // if minus sign is present but not first
79
+ let minusIdx = string.indexOf('-');
80
+ if (minusIdx > 0) {
81
+ string = string.replace('-', '');
82
+ retString = '-'.concat(string);
83
+ } else if (minusIdx === 0) {
84
+ if (string.indexOf('-', minusIdx + 1) > 0) {
85
+ retString = string.replaceAll('-', '');
86
+ }
87
+ }
88
+ return retString;
89
+ };
90
+
91
+ /**
92
+ * Translate . to min. or .x to mim.x or -. to -min.
93
+ * @param string {String}
94
+ */
95
+ const maybeLeadWithMin = (string, min) => {
96
+ let retString = string;
97
+ let cuttedVal = Math.sign(min) * Math.floor(Math.abs(min));
98
+ if (string.indexOf('.') === 0) {
99
+ retString = String(cuttedVal).concat(string);
100
+ } else if (string.indexOf('-') === 0 && string.indexOf('.') === 1) {
101
+ retString = cuttedVal + '.'.concat(string.slice(2));
102
+ }
103
+ return retString;
104
+ };
105
+
106
+ /**
107
+ * Keep only the first occurrence of a string in another string.
108
+ * @param needle {String}
109
+ * @param haystack {String}
110
+ * @returns {string}
111
+ */
112
+ const keepOnlyFirstOccurrence = (needle, haystack) => {
113
+ const idx = haystack.indexOf(needle);
114
+ const len = haystack.length;
115
+ let newHaystack = haystack;
116
+ if (idx !== -1 && idx < len - 1) {
117
+ let trailingString = haystack.slice(idx + 1, len);
118
+ trailingString = trailingString.replaceAll(needle, '');
119
+ newHaystack = haystack.slice(0, idx + 1).concat(trailingString);
120
+ }
121
+ return newHaystack;
122
+ };
123
+
124
+ /**
125
+ * Takes a string input and parses integers or floats from it.
126
+ * If none: Minimum is set.
127
+ * Else: Clamps it to the given range.
128
+ */
129
+ const getClampedNumber = (value, minValue, maxValue, allowFloats) => {
130
+ const minimum = minValue || DEFAULT_MIN;
131
+ const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
132
+ if (!value || !value.length) {
133
+ return String(minimum);
134
+ }
135
+ let parsedValue = allowFloats
136
+ ? parseFloat(value.replace(/[^\-\d.]/g, ''))
137
+ : parseInt(value.replace(/[^\-\d]/g, ''), 10);
138
+ if (isNaN(parsedValue)) {
139
+ return String(minimum);
140
+ } else {
141
+ return String(clamp(parsedValue, minimum, maximum));
142
+ }
143
+ };
144
+
145
+ export class RestrictedInput extends Component {
146
+ constructor(props) {
147
+ super(props);
148
+ this.inputRef = createRef();
149
+ this.state = {
150
+ editing: false,
151
+ };
152
+ this.handleBlur = (e) => {
153
+ const { maxValue, minValue, onBlur, allowFloats } = this.props;
154
+ const { editing } = this.state;
155
+ if (editing) {
156
+ this.setEditing(false);
157
+ }
158
+ const safeNum = getClampedNumber(
159
+ e.target.value,
160
+ minValue,
161
+ maxValue,
162
+ allowFloats
163
+ );
164
+ if (onBlur) {
165
+ onBlur(e, +safeNum);
166
+ }
167
+ };
168
+ this.handleChange = (e) => {
169
+ const { maxValue, minValue, onChange, allowFloats } = this.props;
170
+ e.target.value = softSanitizeNumber(
171
+ e.target.value,
172
+ minValue,
173
+ maxValue,
174
+ allowFloats
175
+ );
176
+ if (onChange) {
177
+ onChange(e, +e.target.value);
178
+ }
179
+ };
180
+ this.handleFocus = (e) => {
181
+ const { editing } = this.state;
182
+ if (!editing) {
183
+ this.setEditing(true);
184
+ }
185
+ };
186
+ this.handleInput = (e) => {
187
+ const { editing } = this.state;
188
+ const { onInput } = this.props;
189
+ if (!editing) {
190
+ this.setEditing(true);
191
+ }
192
+ if (onInput) {
193
+ onInput(e, +e.target.value);
194
+ }
195
+ };
196
+ this.handleKeyDown = (e) => {
197
+ const { maxValue, minValue, onChange, onEnter, allowFloats } = this.props;
198
+ if (e.keyCode === KEY_ENTER) {
199
+ const safeNum = getClampedNumber(
200
+ e.target.value,
201
+ minValue,
202
+ maxValue,
203
+ allowFloats
204
+ );
205
+ this.setEditing(false);
206
+ if (onChange) {
207
+ onChange(e, +safeNum);
208
+ }
209
+ if (onEnter) {
210
+ onEnter(e, +safeNum);
211
+ }
212
+ e.target.blur();
213
+ return;
214
+ }
215
+ if (e.keyCode === KEY_ESCAPE) {
216
+ if (this.props.onEscape) {
217
+ this.props.onEscape(e);
218
+ return;
219
+ }
220
+ this.setEditing(false);
221
+ e.target.value = this.props.value;
222
+ e.target.blur();
223
+ return;
224
+ }
225
+ };
226
+ }
227
+
228
+ componentDidMount() {
229
+ const { maxValue, minValue, allowFloats } = this.props;
230
+ const nextValue = this.props.value?.toString();
231
+ const input = this.inputRef.current;
232
+ if (input) {
233
+ input.value = getClampedNumber(
234
+ nextValue,
235
+ minValue,
236
+ maxValue,
237
+ allowFloats
238
+ );
239
+ }
240
+ if (this.props.autoFocus || this.props.autoSelect) {
241
+ setTimeout(() => {
242
+ input.focus();
243
+
244
+ if (this.props.autoSelect) {
245
+ input.select();
246
+ }
247
+ }, 1);
248
+ }
249
+ }
250
+
251
+ componentDidUpdate(prevProps, _) {
252
+ const { maxValue, minValue, allowFloats } = this.props;
253
+ const { editing } = this.state;
254
+ const prevValue = prevProps.value?.toString();
255
+ const nextValue = this.props.value?.toString();
256
+ const input = this.inputRef.current;
257
+ if (input && !editing) {
258
+ if (nextValue !== prevValue && nextValue !== input.value) {
259
+ input.value = getClampedNumber(
260
+ nextValue,
261
+ minValue,
262
+ maxValue,
263
+ allowFloats
264
+ );
265
+ }
266
+ }
267
+ }
268
+
269
+ setEditing(editing) {
270
+ this.setState({ editing });
271
+ }
272
+
273
+ render() {
274
+ const { props } = this;
275
+ const { onChange, onEnter, onInput, onBlur, value, ...boxProps } = props;
276
+ const { className, fluid, monospace, ...rest } = boxProps;
277
+ return (
278
+ <Box
279
+ className={classes([
280
+ 'Input',
281
+ fluid && 'Input--fluid',
282
+ monospace && 'Input--monospace',
283
+ className,
284
+ ])}
285
+ {...rest}
286
+ >
287
+ <div className="Input__baseline">.</div>
288
+ <input
289
+ className="Input__input"
290
+ onChange={this.handleChange}
291
+ onInput={this.handleInput}
292
+ onFocus={this.handleFocus}
293
+ onBlur={this.handleBlur}
294
+ onKeyDown={this.handleKeyDown}
295
+ ref={this.inputRef}
296
+ type="number | string"
297
+ />
298
+ </Box>
299
+ );
300
+ }
301
+ }