CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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