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