Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/antd-bootstrap.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6We use so little of react-bootstrap in CoCalc that for a first quick round7of switching to antd, I'm going to see if it isn't easy to re-implement8much of the same functionality on top of antd910Obviously, this is meant to be temporary, since it is far better if our11code consistently uses the antd api explicitly. However, there are12some serious problems / bug /issues with using our stupid old react-bootstrap13*at all*, hence this.14*/1516import {17Alert as AntdAlert,18Button as AntdButton,19Card as AntdCard,20Checkbox as AntdCheckbox,21Col as AntdCol,22Modal as AntdModal,23Row as AntdRow,24Tabs as AntdTabs,25TabsProps as AntdTabsProps,26Tooltip,27} from "antd";28import type { MouseEventHandler } from "react";2930import { inDarkMode } from "@cocalc/frontend/account/dark-mode";31import { Gap } from "@cocalc/frontend/components/gap";32import { r_join } from "@cocalc/frontend/components/r_join";33import { COLORS } from "@cocalc/util/theme";3435// Note regarding buttons -- there are 6 semantics meanings in bootstrap, but36// only four in antd, and it we can't automatically collapse them down in a meaningful37// way without fundamentally removing information and breaking our UI (e.g., buttons38// change look after an assignment is sent successfully in a course).39export type ButtonStyle =40| "primary"41| "success"42| "default"43| "info"44| "warning"45| "danger"46| "link"47| "ghost";4849const BS_STYLE_TO_TYPE: {50[name in ButtonStyle]:51| "primary"52| "default"53| "dashed"54| "danger"55| "link"56| "text";57} = {58primary: "primary",59success: "default", // antd doesn't have this so we do it via style below.60default: "default",61info: "default", // antd doesn't have this so we do it via style below.62warning: "default", // antd doesn't have this so we do it via style below.63danger: "danger",64link: "link",65ghost: "text",66};6768export type ButtonSize = "large" | "small" | "xsmall";6970function parse_bsStyle(props: {71bsStyle?: ButtonStyle;72style?: React.CSSProperties;73disabled?: boolean;74}): {75type: "primary" | "default" | "dashed" | "link" | "text";76style: React.CSSProperties;77danger?: boolean;78ghost?: boolean;79disabled?: boolean;80loading?: boolean;81} {82let type =83props.bsStyle == null84? "default"85: BS_STYLE_TO_TYPE[props.bsStyle] ?? "default";8687let style: React.CSSProperties | undefined = undefined;88// antd has no analogue of "success" & "warning", it's not clear to me what89// it should be so for now just copy the style from react-bootstrap.90if (!inDarkMode()) {91if (props.bsStyle === "warning") {92// antd has no analogue of "warning", it's not clear to me what93// it should be so for94// now just copy the style.95style = {96backgroundColor: COLORS.BG_WARNING,97borderColor: "#eea236",98color: "#ffffff",99};100} else if (props.bsStyle === "success") {101style = {102backgroundColor: "#5cb85c",103borderColor: "#4cae4c",104color: "#ffffff",105};106} else if (props.bsStyle == "info") {107style = {108backgroundColor: "rgb(91, 192, 222)",109borderColor: "rgb(70, 184, 218)",110color: "#ffffff",111};112}113}114if (props.disabled && style != null) {115style.opacity = 0.65;116}117118style = { ...style, ...props.style };119let danger: boolean | undefined = undefined;120let loading: boolean | undefined = undefined; // nothing mapped to this yet121let ghost: boolean | undefined = undefined; // nothing mapped to this yet122if (type == "danger") {123type = "default";124danger = true;125}126return { type, style, danger, ghost, loading };127}128129export const Button = (props: {130bsStyle?: ButtonStyle;131bsSize?: ButtonSize;132style?: React.CSSProperties;133disabled?: boolean;134onClick?: (e?: any) => void;135key?;136children?: any;137className?: string;138href?: string;139target?: string;140title?: string | JSX.Element;141tabIndex?: number;142active?: boolean;143id?: string;144autoFocus?: boolean;145placement?;146block?: boolean;147}) => {148// The span is needed inside below, otherwise icons and labels get squashed together149// due to button having word-spacing 0.150const { type, style, danger, ghost, loading } = parse_bsStyle(props);151let size: "middle" | "large" | "small" | undefined = undefined;152if (props.bsSize == "large") {153size = "large";154} else if (props.bsSize == "small") {155size = "middle";156} else if (props.bsSize == "xsmall") {157size = "small";158}159if (props.active) {160style.backgroundColor = "#d4d4d4";161style.boxShadow = "inset 0 3px 5px rgb(0 0 0 / 13%)";162}163const btn = (164<AntdButton165onClick={props.onClick}166type={type}167disabled={props.disabled}168style={style}169size={size}170className={props.className}171href={props.href}172target={props.target}173danger={danger}174ghost={ghost}175loading={loading}176tabIndex={props.tabIndex}177id={props.id}178autoFocus={props.autoFocus}179block={props.block}180>181<>{props.children}</>182</AntdButton>183);184if (props.title) {185return (186<Tooltip187title={props.title}188mouseEnterDelay={0.7}189placement={props.placement}190>191{btn}192</Tooltip>193);194} else {195return btn;196}197};198199export function ButtonGroup(props: {200style?: React.CSSProperties;201children?: any;202className?: string;203}) {204return (205<AntdButton.Group className={props.className} style={props.style}>206{props.children}207</AntdButton.Group>208);209}210211export function ButtonToolbar(props: {212style?: React.CSSProperties;213children?: any;214className?: string;215}) {216return (217<div className={props.className} style={props.style}>218{r_join(props.children, <Gap />)}219</div>220);221}222223export function Grid(props: {224onClick?: MouseEventHandler<HTMLDivElement>;225style?: React.CSSProperties;226children?: any;227}) {228return (229<div230onClick={props.onClick}231style={{ ...{ padding: "0 8px" }, ...props.style }}232>233{props.children}234</div>235);236}237238export function Well(props: {239style?: React.CSSProperties;240children?: any;241className?: string;242onDoubleClick?;243onMouseDown?;244}) {245let style: React.CSSProperties = {246...{ backgroundColor: "white", border: "1px solid #e3e3e3" },247...props.style,248};249return (250<AntdCard251style={style}252className={props.className}253onDoubleClick={props.onDoubleClick}254onMouseDown={props.onMouseDown}255>256{props.children}257</AntdCard>258);259}260261export function Checkbox(props) {262const style: React.CSSProperties = props.style != null ? props.style : {};263if (style.fontWeight == null) {264// Antd checkbox uses the label DOM element, and bootstrap css265// changes the weight of that DOM element to 700, which is266// really ugly and conflicts with the antd design style. So267// we manualy change it back here. This will go away if/when268// we no longer include bootstrap css...269style.fontWeight = 400;270}271// The margin and div is to be like react-bootstrap which272// has that margin.273return (274<div style={{ margin: "10px 0" }}>275<AntdCheckbox {...{ ...props, style }}>{props.children}</AntdCheckbox>276</div>277);278}279280export function Row(props: any) {281props = { ...{ gutter: 16 }, ...props };282return <AntdRow {...props}>{props.children}</AntdRow>;283}284285export function Col(props: {286xs?: number;287sm?: number;288md?: number;289lg?: number;290xsOffset?: number;291smOffset?: number;292mdOffset?: number;293lgOffset?: number;294style?: React.CSSProperties;295className?: string;296onClick?;297children?: any;298push?;299pull?;300}) {301const props2: any = {};302for (const p of ["xs", "sm", "md", "lg", "push", "pull"]) {303if (props[p] != null) {304if (props2[p] == null) {305props2[p] = {};306}307props2[p].span = 2 * props[p];308}309if (props[p + "Offset"] != null) {310if (props2[p] == null) {311props2[p] = {};312}313props2[p].offset = 2 * props[p + "Offset"];314}315}316for (const p of ["className", "onClick", "style"]) {317props2[p] = props[p];318}319return <AntdCol {...props2}>{props.children}</AntdCol>;320}321322export type AntdTabItem = NonNullable<AntdTabsProps["items"]>[number];323324interface TabsProps {325id?: string;326key?;327activeKey: string;328onSelect?: (activeKey: string) => void;329animation?: boolean;330style?: React.CSSProperties;331tabBarExtraContent?;332tabPosition?: "left" | "top" | "right" | "bottom";333size?: "small";334items: AntdTabItem[]; // This is mandatory: Tabs.TabPane (was in "Tab") is deprecated.335}336337export function Tabs(props: Readonly<TabsProps>) {338return (339<AntdTabs340activeKey={props.activeKey}341onChange={props.onSelect}342animated={props.animation ?? false}343style={props.style}344tabBarExtraContent={props.tabBarExtraContent}345tabPosition={props.tabPosition}346size={props.size}347items={props.items}348/>349);350}351352export function Tab(props: {353id?: string;354key?: string;355eventKey: string;356title: string | JSX.Element;357children?: any;358style?: React.CSSProperties;359}): AntdTabItem {360let title = props.title;361if (!title) {362// In case of useless title, some sort of fallback.363// This is important since a tab with no title can't364// be selected.365title = props.eventKey ?? props.key;366if (!title) title = "Tab";367}368369// Get rid of the fade transition, which is inconsistent with370// react-bootstrap (and also really annoying to me). See371// https://github.com/ant-design/ant-design/issues/951#issuecomment-176291275372const style = { ...{ transition: "0s" }, ...props.style };373374return {375key: props.key ?? props.eventKey,376label: title,377style,378children: props.children,379};380}381382export function Modal(props: {383show?: boolean;384onHide: () => void;385children?: any;386}) {387return (388<AntdModal open={props.show} footer={null} closable={false}>389{props.children}390</AntdModal>391);392}393394Modal.Body = function (props: any) {395return <>{props.children}</>;396};397398interface AlertProps {399bsStyle?: ButtonStyle;400style?: React.CSSProperties;401banner?: boolean;402children?: any;403icon?: JSX.Element;404}405406export function Alert(props: AlertProps) {407const { bsStyle, style, banner, children, icon } = props;408409let type: "success" | "info" | "warning" | "error" | undefined = undefined;410// success, info, warning, error411if (bsStyle == "success" || bsStyle == "warning" || bsStyle == "info") {412type = bsStyle;413} else if (bsStyle == "danger") {414type = "error";415} else if (bsStyle == "link") {416type = "info";417} else if (bsStyle == "primary") {418type = "success";419}420return (421<AntdAlert422message={children}423type={type}424style={style}425banner={banner}426icon={icon}427/>428);429}430431export function Panel(props: {432key?;433style?: React.CSSProperties;434header?;435children?: any;436onClick?;437}) {438const style = { ...{ marginBottom: "20px" }, ...props.style };439return (440<AntdCard441style={style}442title={props.header}443styles={{444header: { color: COLORS.GRAY_DD, backgroundColor: COLORS.GRAY_LLL },445}}446onClick={props.onClick}447>448{props.children}449</AntdCard>450);451}452453454