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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BooleanLike, classes } from '../common/react';
|
|
8
|
+
import {
|
|
9
|
+
createElement,
|
|
10
|
+
KeyboardEventHandler,
|
|
11
|
+
MouseEventHandler,
|
|
12
|
+
ReactNode,
|
|
13
|
+
UIEventHandler,
|
|
14
|
+
} from 'react';
|
|
15
|
+
|
|
16
|
+
import { CSS_COLORS } from '../constants';
|
|
17
|
+
|
|
18
|
+
type BooleanProps = Partial<Record<keyof typeof booleanStyleMap, boolean>>;
|
|
19
|
+
type StringProps = Partial<
|
|
20
|
+
Record<keyof typeof stringStyleMap, string | BooleanLike>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export type EventHandlers = Partial<{
|
|
24
|
+
onClick: MouseEventHandler<HTMLDivElement>;
|
|
25
|
+
onContextMenu: MouseEventHandler<HTMLDivElement>;
|
|
26
|
+
onDoubleClick: MouseEventHandler<HTMLDivElement>;
|
|
27
|
+
onKeyDown: KeyboardEventHandler<HTMLDivElement>;
|
|
28
|
+
onKeyUp: KeyboardEventHandler<HTMLDivElement>;
|
|
29
|
+
onMouseDown: MouseEventHandler<HTMLDivElement>;
|
|
30
|
+
onMouseMove: MouseEventHandler<HTMLDivElement>;
|
|
31
|
+
onMouseOver: MouseEventHandler<HTMLDivElement>;
|
|
32
|
+
onMouseUp: MouseEventHandler<HTMLDivElement>;
|
|
33
|
+
onScroll: UIEventHandler<HTMLDivElement>;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
export type BoxProps = Partial<{
|
|
37
|
+
as: string;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
className: string | BooleanLike;
|
|
40
|
+
style: Partial<CSSStyleDeclaration>;
|
|
41
|
+
}> &
|
|
42
|
+
BooleanProps &
|
|
43
|
+
StringProps &
|
|
44
|
+
EventHandlers;
|
|
45
|
+
|
|
46
|
+
// Don't you dare put this elsewhere
|
|
47
|
+
type DangerDoNotUse = {
|
|
48
|
+
dangerouslySetInnerHTML?: {
|
|
49
|
+
__html: any;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Coverts our rem-like spacing unit into a CSS unit.
|
|
55
|
+
*/
|
|
56
|
+
export const unit = (value: unknown) => {
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
// Transparently convert pixels into rem units
|
|
59
|
+
if (value.endsWith('px')) {
|
|
60
|
+
return parseFloat(value) / 12 + 'rem';
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'number') {
|
|
65
|
+
return value + 'rem';
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Same as `unit`, but half the size for integers numbers.
|
|
71
|
+
*/
|
|
72
|
+
export const halfUnit = (value: unknown) => {
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
return unit(value);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === 'number') {
|
|
77
|
+
return unit(value * 0.5);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const isColorCode = (str: unknown) => !isColorClass(str);
|
|
82
|
+
|
|
83
|
+
const isColorClass = (str: unknown): boolean => {
|
|
84
|
+
return typeof str === 'string' && CSS_COLORS.includes(str as any);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const mapRawPropTo = (attrName) => (style, value) => {
|
|
88
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
89
|
+
style[attrName] = value;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const mapUnitPropTo = (attrName, unit) => (style, value) => {
|
|
94
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
95
|
+
style[attrName] = unit(value);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mapBooleanPropTo = (attrName, attrValue) => (style, value) => {
|
|
100
|
+
if (value) {
|
|
101
|
+
style[attrName] = attrValue;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const mapDirectionalUnitPropTo = (attrName, unit, dirs) => (style, value) => {
|
|
106
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
107
|
+
for (let i = 0; i < dirs.length; i++) {
|
|
108
|
+
style[attrName + '-' + dirs[i]] = unit(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const mapColorPropTo = (attrName) => (style, value) => {
|
|
114
|
+
if (isColorCode(value)) {
|
|
115
|
+
style[attrName] = value;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// String / number props
|
|
120
|
+
const stringStyleMap = {
|
|
121
|
+
align: mapRawPropTo('textAlign'),
|
|
122
|
+
bottom: mapUnitPropTo('bottom', unit),
|
|
123
|
+
fontFamily: mapRawPropTo('fontFamily'),
|
|
124
|
+
fontSize: mapUnitPropTo('fontSize', unit),
|
|
125
|
+
fontWeight: mapRawPropTo('fontWeight'),
|
|
126
|
+
height: mapUnitPropTo('height', unit),
|
|
127
|
+
left: mapUnitPropTo('left', unit),
|
|
128
|
+
maxHeight: mapUnitPropTo('maxHeight', unit),
|
|
129
|
+
maxWidth: mapUnitPropTo('maxWidth', unit),
|
|
130
|
+
minHeight: mapUnitPropTo('minHeight', unit),
|
|
131
|
+
minWidth: mapUnitPropTo('minWidth', unit),
|
|
132
|
+
opacity: mapRawPropTo('opacity'),
|
|
133
|
+
overflow: mapRawPropTo('overflow'),
|
|
134
|
+
overflowX: mapRawPropTo('overflowX'),
|
|
135
|
+
overflowY: mapRawPropTo('overflowY'),
|
|
136
|
+
position: mapRawPropTo('position'),
|
|
137
|
+
right: mapUnitPropTo('right', unit),
|
|
138
|
+
textAlign: mapRawPropTo('textAlign'),
|
|
139
|
+
top: mapUnitPropTo('top', unit),
|
|
140
|
+
verticalAlign: mapRawPropTo('verticalAlign'),
|
|
141
|
+
width: mapUnitPropTo('width', unit),
|
|
142
|
+
|
|
143
|
+
lineHeight: (style, value) => {
|
|
144
|
+
if (typeof value === 'number') {
|
|
145
|
+
style['lineHeight'] = value;
|
|
146
|
+
} else if (typeof value === 'string') {
|
|
147
|
+
style['lineHeight'] = unit(value);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
// Margin
|
|
151
|
+
m: mapDirectionalUnitPropTo('margin', halfUnit, [
|
|
152
|
+
'Top',
|
|
153
|
+
'Bottom',
|
|
154
|
+
'Left',
|
|
155
|
+
'Right',
|
|
156
|
+
]),
|
|
157
|
+
mb: mapUnitPropTo('marginBottom', halfUnit),
|
|
158
|
+
ml: mapUnitPropTo('marginLeft', halfUnit),
|
|
159
|
+
mr: mapUnitPropTo('marginRight', halfUnit),
|
|
160
|
+
mt: mapUnitPropTo('marginTop', halfUnit),
|
|
161
|
+
mx: mapDirectionalUnitPropTo('margin', halfUnit, ['Left', 'Right']),
|
|
162
|
+
my: mapDirectionalUnitPropTo('margin', halfUnit, ['Top', 'Bottom']),
|
|
163
|
+
// Padding
|
|
164
|
+
p: mapDirectionalUnitPropTo('padding', halfUnit, [
|
|
165
|
+
'Top',
|
|
166
|
+
'Bottom',
|
|
167
|
+
'Left',
|
|
168
|
+
'Right',
|
|
169
|
+
]),
|
|
170
|
+
pb: mapUnitPropTo('paddingBottom', halfUnit),
|
|
171
|
+
pl: mapUnitPropTo('paddingLeft', halfUnit),
|
|
172
|
+
pr: mapUnitPropTo('paddingRight', halfUnit),
|
|
173
|
+
pt: mapUnitPropTo('paddingTop', halfUnit),
|
|
174
|
+
px: mapDirectionalUnitPropTo('padding', halfUnit, ['Left', 'Right']),
|
|
175
|
+
py: mapDirectionalUnitPropTo('padding', halfUnit, ['Top', 'Bottom']),
|
|
176
|
+
// Color props
|
|
177
|
+
color: mapColorPropTo('color'),
|
|
178
|
+
textColor: mapColorPropTo('color'),
|
|
179
|
+
backgroundColor: mapColorPropTo('backgroundColor'),
|
|
180
|
+
} as const;
|
|
181
|
+
|
|
182
|
+
// Boolean props
|
|
183
|
+
const booleanStyleMap = {
|
|
184
|
+
bold: mapBooleanPropTo('fontWeight', 'bold'),
|
|
185
|
+
fillPositionedParent: (style, value) => {
|
|
186
|
+
if (value) {
|
|
187
|
+
style['position'] = 'absolute';
|
|
188
|
+
style['top'] = 0;
|
|
189
|
+
style['bottom'] = 0;
|
|
190
|
+
style['left'] = 0;
|
|
191
|
+
style['right'] = 0;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
inline: mapBooleanPropTo('display', 'inline-block'),
|
|
195
|
+
italic: mapBooleanPropTo('fontStyle', 'italic'),
|
|
196
|
+
nowrap: mapBooleanPropTo('whiteSpace', 'nowrap'),
|
|
197
|
+
preserveWhitespace: mapBooleanPropTo('whiteSpace', 'pre-wrap'),
|
|
198
|
+
} as const;
|
|
199
|
+
|
|
200
|
+
export const computeBoxProps = (props) => {
|
|
201
|
+
const computedProps: Record<string, any> = {};
|
|
202
|
+
const computedStyles: Record<string, string | number> = {};
|
|
203
|
+
|
|
204
|
+
// Compute props
|
|
205
|
+
for (let propName of Object.keys(props)) {
|
|
206
|
+
if (propName === 'style') {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const propValue = props[propName];
|
|
211
|
+
|
|
212
|
+
const mapPropToStyle =
|
|
213
|
+
stringStyleMap[propName] || booleanStyleMap[propName];
|
|
214
|
+
|
|
215
|
+
if (mapPropToStyle) {
|
|
216
|
+
mapPropToStyle(computedStyles, propValue);
|
|
217
|
+
} else {
|
|
218
|
+
computedProps[propName] = propValue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Merge computed styles and any directly provided styles
|
|
223
|
+
computedProps.style = { ...computedStyles, ...props.style };
|
|
224
|
+
|
|
225
|
+
return computedProps;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const computeBoxClassName = (props: BoxProps) => {
|
|
229
|
+
const color = props.textColor || props.color;
|
|
230
|
+
const backgroundColor = props.backgroundColor;
|
|
231
|
+
return classes([
|
|
232
|
+
isColorClass(color) && 'color-' + color,
|
|
233
|
+
isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
|
|
234
|
+
]);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const Box = (props: BoxProps & DangerDoNotUse) => {
|
|
238
|
+
const { as = 'div', className, children, ...rest } = props;
|
|
239
|
+
|
|
240
|
+
// Compute class name and styles
|
|
241
|
+
const computedClassName = className
|
|
242
|
+
? `${className} ${computeBoxClassName(rest)}`
|
|
243
|
+
: computeBoxClassName(rest);
|
|
244
|
+
const computedProps = computeBoxProps(rest);
|
|
245
|
+
|
|
246
|
+
// Render the component
|
|
247
|
+
return createElement(
|
|
248
|
+
typeof as === 'string' ? as : 'div',
|
|
249
|
+
{
|
|
250
|
+
...computedProps,
|
|
251
|
+
className: computedClassName,
|
|
252
|
+
},
|
|
253
|
+
children
|
|
254
|
+
);
|
|
255
|
+
};
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @copyright 2020 Aleksej Komarov
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Placement } from '@popperjs/core';
|
|
8
|
+
import { KEY } from '../common/keys';
|
|
9
|
+
import { BooleanLike, classes } from '../common/react';
|
|
10
|
+
import {
|
|
11
|
+
ChangeEvent,
|
|
12
|
+
createRef,
|
|
13
|
+
MouseEvent,
|
|
14
|
+
ReactNode,
|
|
15
|
+
useEffect,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
} from 'react';
|
|
19
|
+
|
|
20
|
+
import { Box, BoxProps, computeBoxClassName, computeBoxProps } from './Box';
|
|
21
|
+
import { Icon } from './Icon';
|
|
22
|
+
import { Tooltip } from './Tooltip';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Getting ellipses to work requires that you use:
|
|
26
|
+
* 1. A string rather than a node
|
|
27
|
+
* 2. A fixed width here or in a parent
|
|
28
|
+
* 3. Children prop rather than content
|
|
29
|
+
*/
|
|
30
|
+
type EllipsisUnion =
|
|
31
|
+
| {
|
|
32
|
+
ellipsis: true;
|
|
33
|
+
children: string;
|
|
34
|
+
/** @deprecated use children instead */
|
|
35
|
+
content?: never;
|
|
36
|
+
}
|
|
37
|
+
| Partial<{
|
|
38
|
+
ellipsis: undefined;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
/** @deprecated use children instead */
|
|
41
|
+
content: ReactNode;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
type Props = Partial<{
|
|
45
|
+
captureKeys: boolean;
|
|
46
|
+
circular: boolean;
|
|
47
|
+
compact: boolean;
|
|
48
|
+
disabled: BooleanLike;
|
|
49
|
+
fluid: boolean;
|
|
50
|
+
icon: string | false;
|
|
51
|
+
iconColor: string;
|
|
52
|
+
iconPosition: string;
|
|
53
|
+
iconRotation: number;
|
|
54
|
+
iconSpin: BooleanLike;
|
|
55
|
+
onClick: (e: any) => void;
|
|
56
|
+
selected: BooleanLike;
|
|
57
|
+
tooltip: ReactNode;
|
|
58
|
+
tooltipPosition: Placement;
|
|
59
|
+
verticalAlignContent: string;
|
|
60
|
+
}> &
|
|
61
|
+
EllipsisUnion &
|
|
62
|
+
BoxProps;
|
|
63
|
+
|
|
64
|
+
/** Clickable button. Comes with variants. Read more in the documentation. */
|
|
65
|
+
export const Button = (props: Props) => {
|
|
66
|
+
const {
|
|
67
|
+
captureKeys = true,
|
|
68
|
+
children,
|
|
69
|
+
circular,
|
|
70
|
+
className,
|
|
71
|
+
color,
|
|
72
|
+
compact,
|
|
73
|
+
content,
|
|
74
|
+
disabled,
|
|
75
|
+
ellipsis,
|
|
76
|
+
fluid,
|
|
77
|
+
icon,
|
|
78
|
+
iconColor,
|
|
79
|
+
iconPosition,
|
|
80
|
+
iconRotation,
|
|
81
|
+
iconSpin,
|
|
82
|
+
onClick,
|
|
83
|
+
selected,
|
|
84
|
+
tooltip,
|
|
85
|
+
tooltipPosition,
|
|
86
|
+
verticalAlignContent,
|
|
87
|
+
...rest
|
|
88
|
+
} = props;
|
|
89
|
+
|
|
90
|
+
const toDisplay: ReactNode = content || children;
|
|
91
|
+
|
|
92
|
+
let buttonContent = (
|
|
93
|
+
<div
|
|
94
|
+
className={classes([
|
|
95
|
+
'Button',
|
|
96
|
+
fluid && 'Button--fluid',
|
|
97
|
+
disabled && 'Button--disabled',
|
|
98
|
+
selected && 'Button--selected',
|
|
99
|
+
!!toDisplay && 'Button--hasContent',
|
|
100
|
+
circular && 'Button--circular',
|
|
101
|
+
compact && 'Button--compact',
|
|
102
|
+
iconPosition && 'Button--iconPosition--' + iconPosition,
|
|
103
|
+
verticalAlignContent && 'Button--flex',
|
|
104
|
+
verticalAlignContent && fluid && 'Button--flex--fluid',
|
|
105
|
+
verticalAlignContent &&
|
|
106
|
+
'Button--verticalAlignContent--' + verticalAlignContent,
|
|
107
|
+
color && typeof color === 'string'
|
|
108
|
+
? 'Button--color--' + color
|
|
109
|
+
: 'Button--color--default',
|
|
110
|
+
className,
|
|
111
|
+
computeBoxClassName(rest),
|
|
112
|
+
])}
|
|
113
|
+
tabIndex={!disabled ? 0 : undefined}
|
|
114
|
+
onClick={(event) => {
|
|
115
|
+
if (!disabled && onClick) {
|
|
116
|
+
onClick(event);
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
onKeyDown={(event) => {
|
|
120
|
+
if (!captureKeys) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Simulate a click when pressing space or enter.
|
|
125
|
+
if (event.key === KEY.Space || event.key === KEY.Enter) {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
if (!disabled && onClick) {
|
|
128
|
+
onClick(event);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Refocus layout on pressing escape.
|
|
134
|
+
if (event.key === KEY.Escape) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
{...computeBoxProps(rest)}
|
|
139
|
+
>
|
|
140
|
+
<div className="Button__content">
|
|
141
|
+
{icon && iconPosition !== 'right' && (
|
|
142
|
+
<Icon
|
|
143
|
+
name={icon}
|
|
144
|
+
color={iconColor}
|
|
145
|
+
rotation={iconRotation}
|
|
146
|
+
spin={iconSpin}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
{!ellipsis ? (
|
|
150
|
+
toDisplay
|
|
151
|
+
) : (
|
|
152
|
+
<span
|
|
153
|
+
className={classes([
|
|
154
|
+
'Button--ellipsis',
|
|
155
|
+
icon && 'Button__textMargin',
|
|
156
|
+
])}
|
|
157
|
+
>
|
|
158
|
+
{toDisplay}
|
|
159
|
+
</span>
|
|
160
|
+
)}
|
|
161
|
+
{icon && iconPosition === 'right' && (
|
|
162
|
+
<Icon
|
|
163
|
+
name={icon}
|
|
164
|
+
color={iconColor}
|
|
165
|
+
rotation={iconRotation}
|
|
166
|
+
spin={iconSpin}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (tooltip) {
|
|
174
|
+
buttonContent = (
|
|
175
|
+
<Tooltip content={tooltip} position={tooltipPosition as Placement}>
|
|
176
|
+
{buttonContent}
|
|
177
|
+
</Tooltip>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return buttonContent;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type CheckProps = Partial<{
|
|
185
|
+
checked: BooleanLike;
|
|
186
|
+
}> &
|
|
187
|
+
Props;
|
|
188
|
+
|
|
189
|
+
/** Visually toggles between checked and unchecked states. */
|
|
190
|
+
export const ButtonCheckbox = (props: CheckProps) => {
|
|
191
|
+
const { checked, ...rest } = props;
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Button
|
|
195
|
+
color="transparent"
|
|
196
|
+
icon={checked ? 'check-square-o' : 'square-o'}
|
|
197
|
+
selected={checked}
|
|
198
|
+
{...rest}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
Button.Checkbox = ButtonCheckbox;
|
|
204
|
+
|
|
205
|
+
type ConfirmProps = Partial<{
|
|
206
|
+
confirmColor: string;
|
|
207
|
+
confirmContent: ReactNode;
|
|
208
|
+
confirmIcon: string;
|
|
209
|
+
}> &
|
|
210
|
+
Props;
|
|
211
|
+
|
|
212
|
+
/** Requires user confirmation before triggering its action. */
|
|
213
|
+
const ButtonConfirm = (props: ConfirmProps) => {
|
|
214
|
+
const {
|
|
215
|
+
children,
|
|
216
|
+
color,
|
|
217
|
+
confirmColor = 'bad',
|
|
218
|
+
confirmContent = 'Confirm?',
|
|
219
|
+
confirmIcon,
|
|
220
|
+
ellipsis = true,
|
|
221
|
+
icon,
|
|
222
|
+
onClick,
|
|
223
|
+
...rest
|
|
224
|
+
} = props;
|
|
225
|
+
const [clickedOnce, setClickedOnce] = useState(false);
|
|
226
|
+
|
|
227
|
+
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
|
|
228
|
+
if (!clickedOnce) {
|
|
229
|
+
setClickedOnce(true);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
onClick?.(event);
|
|
234
|
+
setClickedOnce(false);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Button
|
|
239
|
+
icon={clickedOnce ? confirmIcon : icon}
|
|
240
|
+
color={clickedOnce ? confirmColor : color}
|
|
241
|
+
onClick={handleClick}
|
|
242
|
+
{...rest}
|
|
243
|
+
>
|
|
244
|
+
{clickedOnce ? confirmContent : children}
|
|
245
|
+
</Button>
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
Button.Confirm = ButtonConfirm;
|
|
250
|
+
|
|
251
|
+
type InputProps = Partial<{
|
|
252
|
+
currentValue: string;
|
|
253
|
+
defaultValue: string;
|
|
254
|
+
fluid: boolean;
|
|
255
|
+
maxLength: number;
|
|
256
|
+
onCommit: (e: any, value: string) => void;
|
|
257
|
+
placeholder: string;
|
|
258
|
+
}> &
|
|
259
|
+
Props;
|
|
260
|
+
|
|
261
|
+
/** Accepts and handles user input. */
|
|
262
|
+
const ButtonInput = (props: InputProps) => {
|
|
263
|
+
const {
|
|
264
|
+
children,
|
|
265
|
+
color = 'default',
|
|
266
|
+
content,
|
|
267
|
+
currentValue,
|
|
268
|
+
defaultValue,
|
|
269
|
+
disabled,
|
|
270
|
+
fluid,
|
|
271
|
+
icon,
|
|
272
|
+
iconRotation,
|
|
273
|
+
iconSpin,
|
|
274
|
+
maxLength,
|
|
275
|
+
onCommit = () => null,
|
|
276
|
+
placeholder,
|
|
277
|
+
tooltip,
|
|
278
|
+
tooltipPosition,
|
|
279
|
+
...rest
|
|
280
|
+
} = props;
|
|
281
|
+
const [inInput, setInInput] = useState(false);
|
|
282
|
+
const inputRef = createRef<HTMLInputElement>();
|
|
283
|
+
|
|
284
|
+
const toDisplay = content || children;
|
|
285
|
+
|
|
286
|
+
const commitResult = (e) => {
|
|
287
|
+
const input = inputRef.current;
|
|
288
|
+
if (!input) return;
|
|
289
|
+
|
|
290
|
+
const hasValue = input.value !== '';
|
|
291
|
+
if (hasValue) {
|
|
292
|
+
onCommit(e, input.value);
|
|
293
|
+
} else {
|
|
294
|
+
if (defaultValue) {
|
|
295
|
+
onCommit(e, defaultValue);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
const input = inputRef.current;
|
|
302
|
+
if (!input) return;
|
|
303
|
+
|
|
304
|
+
if (inInput) {
|
|
305
|
+
input.value = currentValue || '';
|
|
306
|
+
try {
|
|
307
|
+
input.focus();
|
|
308
|
+
input.select();
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
}, [inInput, currentValue]);
|
|
312
|
+
|
|
313
|
+
let buttonContent = (
|
|
314
|
+
<Box
|
|
315
|
+
className={classes([
|
|
316
|
+
'Button',
|
|
317
|
+
fluid && 'Button--fluid',
|
|
318
|
+
'Button--color--' + color,
|
|
319
|
+
])}
|
|
320
|
+
{...rest}
|
|
321
|
+
onClick={() => setInInput(true)}
|
|
322
|
+
>
|
|
323
|
+
{icon && <Icon name={icon} rotation={iconRotation} spin={iconSpin} />}
|
|
324
|
+
<div>{toDisplay}</div>
|
|
325
|
+
<input
|
|
326
|
+
disabled={!!disabled}
|
|
327
|
+
ref={inputRef}
|
|
328
|
+
className="NumberInput__input"
|
|
329
|
+
style={{
|
|
330
|
+
display: !inInput ? 'none' : '',
|
|
331
|
+
textAlign: 'left',
|
|
332
|
+
}}
|
|
333
|
+
onBlur={(event) => {
|
|
334
|
+
if (!inInput) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
setInInput(false);
|
|
338
|
+
commitResult(event);
|
|
339
|
+
}}
|
|
340
|
+
onKeyDown={(event) => {
|
|
341
|
+
if (event.key === KEY.Enter) {
|
|
342
|
+
setInInput(false);
|
|
343
|
+
commitResult(event);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (event.key === KEY.Escape) {
|
|
347
|
+
setInInput(false);
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
/>
|
|
351
|
+
</Box>
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (tooltip) {
|
|
355
|
+
buttonContent = (
|
|
356
|
+
<Tooltip content={tooltip} position={tooltipPosition as Placement}>
|
|
357
|
+
{buttonContent}
|
|
358
|
+
</Tooltip>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return buttonContent;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
Button.Input = ButtonInput;
|
|
366
|
+
|
|
367
|
+
type FileProps = {
|
|
368
|
+
accept: string;
|
|
369
|
+
multiple?: boolean;
|
|
370
|
+
onSelectFiles: (files: string | string[]) => void;
|
|
371
|
+
} & Props;
|
|
372
|
+
|
|
373
|
+
/** Accepts file input */
|
|
374
|
+
function ButtonFile(props: FileProps) {
|
|
375
|
+
const { accept, multiple, onSelectFiles, ...rest } = props;
|
|
376
|
+
|
|
377
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
378
|
+
|
|
379
|
+
async function read(files: FileList) {
|
|
380
|
+
const promises = Array.from(files).map((file) => {
|
|
381
|
+
const reader = new FileReader();
|
|
382
|
+
|
|
383
|
+
return new Promise<string>((resolve) => {
|
|
384
|
+
reader.onload = () => resolve(reader.result as string);
|
|
385
|
+
reader.readAsText(file);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return await Promise.all(promises);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
|
393
|
+
const files = event.target.files;
|
|
394
|
+
if (files?.length) {
|
|
395
|
+
const readFiles = await read(files);
|
|
396
|
+
onSelectFiles(multiple ? readFiles : readFiles[0]);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<>
|
|
402
|
+
<Button onClick={() => inputRef.current?.click()} {...rest} />
|
|
403
|
+
<input
|
|
404
|
+
hidden
|
|
405
|
+
type="file"
|
|
406
|
+
ref={inputRef}
|
|
407
|
+
accept={accept}
|
|
408
|
+
multiple={multiple}
|
|
409
|
+
onChange={handleChange}
|
|
410
|
+
/>
|
|
411
|
+
</>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
Button.File = ButtonFile;
|