Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/antd-bootstrap.tsx
1496 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
We use so little of react-bootstrap in CoCalc that for a first quick round
8
of switching to antd, I'm going to see if it isn't easy to re-implement
9
much of the same functionality on top of antd
10
11
Obviously, this is meant to be temporary, since it is far better if our
12
code consistently uses the antd api explicitly. However, there are
13
some serious problems / bug /issues with using our stupid old react-bootstrap
14
*at all*, hence this.
15
*/
16
17
import {
18
Alert as AntdAlert,
19
Button as AntdButton,
20
Card as AntdCard,
21
Checkbox as AntdCheckbox,
22
Col as AntdCol,
23
Modal as AntdModal,
24
Row as AntdRow,
25
Tabs as AntdTabs,
26
TabsProps as AntdTabsProps,
27
Space,
28
Tooltip,
29
} from "antd";
30
import type { MouseEventHandler } from "react";
31
32
import { inDarkMode } from "@cocalc/frontend/account/dark-mode";
33
import { Gap } from "@cocalc/frontend/components/gap";
34
import { r_join } from "@cocalc/frontend/components/r_join";
35
import { COLORS } from "@cocalc/util/theme";
36
37
// Note regarding buttons -- there are 6 semantics meanings in bootstrap, but
38
// only four in antd, and it we can't automatically collapse them down in a meaningful
39
// way without fundamentally removing information and breaking our UI (e.g., buttons
40
// change look after an assignment is sent successfully in a course).
41
export type ButtonStyle =
42
| "primary"
43
| "success"
44
| "default"
45
| "info"
46
| "warning"
47
| "danger"
48
| "link"
49
| "ghost";
50
51
const BS_STYLE_TO_TYPE: {
52
[name in ButtonStyle]:
53
| "primary"
54
| "default"
55
| "dashed"
56
| "danger"
57
| "link"
58
| "text";
59
} = {
60
primary: "primary",
61
success: "default", // antd doesn't have this so we do it via style below.
62
default: "default",
63
info: "default", // antd doesn't have this so we do it via style below.
64
warning: "default", // antd doesn't have this so we do it via style below.
65
danger: "danger",
66
link: "link",
67
ghost: "text",
68
};
69
70
export type ButtonSize = "large" | "small" | "xsmall";
71
72
function parse_bsStyle(props: {
73
bsStyle?: ButtonStyle;
74
style?: React.CSSProperties;
75
disabled?: boolean;
76
}): {
77
type: "primary" | "default" | "dashed" | "link" | "text";
78
style: React.CSSProperties;
79
danger?: boolean;
80
ghost?: boolean;
81
disabled?: boolean;
82
loading?: boolean;
83
} {
84
let type =
85
props.bsStyle == null
86
? "default"
87
: BS_STYLE_TO_TYPE[props.bsStyle] ?? "default";
88
89
let style: React.CSSProperties | undefined = undefined;
90
// antd has no analogue of "success" & "warning", it's not clear to me what
91
// it should be so for now just copy the style from react-bootstrap.
92
if (!inDarkMode()) {
93
if (props.bsStyle === "warning") {
94
// antd has no analogue of "warning", it's not clear to me what
95
// it should be so for
96
// now just copy the style.
97
style = {
98
backgroundColor: COLORS.BG_WARNING,
99
borderColor: "#eea236",
100
color: "#ffffff",
101
};
102
} else if (props.bsStyle === "success") {
103
style = {
104
backgroundColor: "#5cb85c",
105
borderColor: "#4cae4c",
106
color: "#ffffff",
107
};
108
} else if (props.bsStyle == "info") {
109
style = {
110
backgroundColor: "rgb(91, 192, 222)",
111
borderColor: "rgb(70, 184, 218)",
112
color: "#ffffff",
113
};
114
}
115
}
116
if (props.disabled && style != null) {
117
style.opacity = 0.65;
118
}
119
120
style = { ...style, ...props.style };
121
let danger: boolean | undefined = undefined;
122
let loading: boolean | undefined = undefined; // nothing mapped to this yet
123
let ghost: boolean | undefined = undefined; // nothing mapped to this yet
124
if (type == "danger") {
125
type = "default";
126
danger = true;
127
}
128
return { type, style, danger, ghost, loading };
129
}
130
131
export const Button = (props: {
132
bsStyle?: ButtonStyle;
133
bsSize?: ButtonSize;
134
style?: React.CSSProperties;
135
disabled?: boolean;
136
onClick?: (e?: any) => void;
137
key?;
138
children?: any;
139
className?: string;
140
href?: string;
141
target?: string;
142
title?: string | React.JSX.Element;
143
tabIndex?: number;
144
active?: boolean;
145
id?: string;
146
autoFocus?: boolean;
147
placement?;
148
block?: boolean;
149
}) => {
150
// The span is needed inside below, otherwise icons and labels get squashed together
151
// due to button having word-spacing 0.
152
const { type, style, danger, ghost, loading } = parse_bsStyle(props);
153
let size: "middle" | "large" | "small" | undefined = undefined;
154
if (props.bsSize == "large") {
155
size = "large";
156
} else if (props.bsSize == "small") {
157
size = "middle";
158
} else if (props.bsSize == "xsmall") {
159
size = "small";
160
}
161
if (props.active) {
162
style.backgroundColor = "#d4d4d4";
163
style.boxShadow = "inset 0 3px 5px rgb(0 0 0 / 13%)";
164
}
165
const btn = (
166
<AntdButton
167
onClick={props.onClick}
168
type={type}
169
disabled={props.disabled}
170
style={style}
171
size={size}
172
className={props.className}
173
href={props.href}
174
target={props.target}
175
danger={danger}
176
ghost={ghost}
177
loading={loading}
178
tabIndex={props.tabIndex}
179
id={props.id}
180
autoFocus={props.autoFocus}
181
block={props.block}
182
>
183
<>{props.children}</>
184
</AntdButton>
185
);
186
if (props.title) {
187
return (
188
<Tooltip
189
title={props.title}
190
mouseEnterDelay={0.7}
191
placement={props.placement}
192
>
193
{btn}
194
</Tooltip>
195
);
196
} else {
197
return btn;
198
}
199
};
200
201
export function ButtonGroup(props: {
202
style?: React.CSSProperties;
203
children?: any;
204
className?: string;
205
}) {
206
return (
207
<Space.Compact className={props.className} style={props.style}>
208
{props.children}
209
</Space.Compact>
210
);
211
}
212
213
export function ButtonToolbar(props: {
214
style?: React.CSSProperties;
215
children?: any;
216
className?: string;
217
}) {
218
return (
219
<div className={props.className} style={props.style}>
220
{r_join(props.children, <Gap />)}
221
</div>
222
);
223
}
224
225
export function Grid(props: {
226
onClick?: MouseEventHandler<HTMLDivElement>;
227
style?: React.CSSProperties;
228
children?: any;
229
}) {
230
return (
231
<div
232
onClick={props.onClick}
233
style={{ ...{ padding: "0 8px" }, ...props.style }}
234
>
235
{props.children}
236
</div>
237
);
238
}
239
240
export function Well(props: {
241
style?: React.CSSProperties;
242
children?: any;
243
className?: string;
244
onDoubleClick?;
245
onMouseDown?;
246
}) {
247
let style: React.CSSProperties = {
248
...{ backgroundColor: "white", border: "1px solid #e3e3e3" },
249
...props.style,
250
};
251
return (
252
<AntdCard
253
style={style}
254
className={props.className}
255
onDoubleClick={props.onDoubleClick}
256
onMouseDown={props.onMouseDown}
257
>
258
{props.children}
259
</AntdCard>
260
);
261
}
262
263
export function Checkbox(props) {
264
const style: React.CSSProperties = props.style != null ? props.style : {};
265
if (style.fontWeight == null) {
266
// Antd checkbox uses the label DOM element, and bootstrap css
267
// changes the weight of that DOM element to 700, which is
268
// really ugly and conflicts with the antd design style. So
269
// we manually change it back here. This will go away if/when
270
// we no longer include bootstrap css...
271
style.fontWeight = 400;
272
}
273
// The margin and div is to be like react-bootstrap which
274
// has that margin.
275
return (
276
<div style={{ margin: "10px 0" }}>
277
<AntdCheckbox {...{ ...props, style }}>{props.children}</AntdCheckbox>
278
</div>
279
);
280
}
281
282
export function Row(props: any) {
283
props = { ...{ gutter: 16 }, ...props };
284
return <AntdRow {...props}>{props.children}</AntdRow>;
285
}
286
287
export function Col(props: {
288
xs?: number;
289
sm?: number;
290
md?: number;
291
lg?: number;
292
xsOffset?: number;
293
smOffset?: number;
294
mdOffset?: number;
295
lgOffset?: number;
296
style?: React.CSSProperties;
297
className?: string;
298
onClick?;
299
children?: any;
300
push?;
301
pull?;
302
}) {
303
const props2: any = {};
304
for (const p of ["xs", "sm", "md", "lg", "push", "pull"]) {
305
if (props[p] != null) {
306
if (props2[p] == null) {
307
props2[p] = {};
308
}
309
props2[p].span = 2 * props[p];
310
}
311
if (props[p + "Offset"] != null) {
312
if (props2[p] == null) {
313
props2[p] = {};
314
}
315
props2[p].offset = 2 * props[p + "Offset"];
316
}
317
}
318
for (const p of ["className", "onClick", "style"]) {
319
props2[p] = props[p];
320
}
321
return <AntdCol {...props2}>{props.children}</AntdCol>;
322
}
323
324
export type AntdTabItem = NonNullable<AntdTabsProps["items"]>[number];
325
326
interface TabsProps {
327
id?: string;
328
key?;
329
activeKey: string;
330
onSelect?: (activeKey: string) => void;
331
animation?: boolean;
332
style?: React.CSSProperties;
333
tabBarExtraContent?;
334
tabPosition?: "left" | "top" | "right" | "bottom";
335
size?: "small";
336
items: AntdTabItem[]; // This is mandatory: Tabs.TabPane (was in "Tab") is deprecated.
337
}
338
339
export function Tabs(props: Readonly<TabsProps>) {
340
return (
341
<AntdTabs
342
activeKey={props.activeKey}
343
onChange={props.onSelect}
344
animated={props.animation ?? false}
345
style={props.style}
346
tabBarExtraContent={props.tabBarExtraContent}
347
tabPosition={props.tabPosition}
348
size={props.size}
349
items={props.items}
350
/>
351
);
352
}
353
354
export function Tab(props: {
355
id?: string;
356
key?: string;
357
eventKey: string;
358
title: string | React.JSX.Element;
359
children?: any;
360
style?: React.CSSProperties;
361
}): AntdTabItem {
362
let title = props.title;
363
if (!title) {
364
// In case of useless title, some sort of fallback.
365
// This is important since a tab with no title can't
366
// be selected.
367
title = props.eventKey ?? props.key;
368
if (!title) title = "Tab";
369
}
370
371
// Get rid of the fade transition, which is inconsistent with
372
// react-bootstrap (and also really annoying to me). See
373
// https://github.com/ant-design/ant-design/issues/951#issuecomment-176291275
374
const style = { ...{ transition: "0s" }, ...props.style };
375
376
return {
377
key: props.key ?? props.eventKey,
378
label: title,
379
style,
380
children: props.children,
381
};
382
}
383
384
export function Modal(props: {
385
show?: boolean;
386
onHide: () => void;
387
children?: any;
388
}) {
389
return (
390
<AntdModal open={props.show} footer={null} closable={false}>
391
{props.children}
392
</AntdModal>
393
);
394
}
395
396
Modal.Body = function (props: any) {
397
return <>{props.children}</>;
398
};
399
400
interface AlertProps {
401
bsStyle?: ButtonStyle;
402
style?: React.CSSProperties;
403
banner?: boolean;
404
children?: any;
405
icon?: React.JSX.Element;
406
}
407
408
export function Alert(props: AlertProps) {
409
const { bsStyle, style, banner, children, icon } = props;
410
411
let type: "success" | "info" | "warning" | "error" | undefined = undefined;
412
// success, info, warning, error
413
if (bsStyle == "success" || bsStyle == "warning" || bsStyle == "info") {
414
type = bsStyle;
415
} else if (bsStyle == "danger") {
416
type = "error";
417
} else if (bsStyle == "link") {
418
type = "info";
419
} else if (bsStyle == "primary") {
420
type = "success";
421
}
422
return (
423
<AntdAlert
424
message={children}
425
type={type}
426
style={style}
427
banner={banner}
428
icon={icon}
429
/>
430
);
431
}
432
433
export function Panel(props: {
434
key?;
435
style?: React.CSSProperties;
436
header?;
437
children?: any;
438
onClick?;
439
}) {
440
const style = { ...{ marginBottom: "20px" }, ...props.style };
441
return (
442
<AntdCard
443
style={style}
444
title={props.header}
445
styles={{
446
header: { color: COLORS.GRAY_DD, backgroundColor: COLORS.GRAY_LLL },
447
}}
448
onClick={props.onClick}
449
>
450
{props.children}
451
</AntdCard>
452
);
453
}
454
455