Grabbed dndbeyond's source code
``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { ThemeButton } from "../ThemeButton";
|
||||
|
||||
//Eventually remove and use character-components/Button/RemoveButton with theme override when needed
|
||||
|
||||
interface Props {
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
styledDisabled?: boolean;
|
||||
block?: boolean;
|
||||
size?: string;
|
||||
style?: string;
|
||||
active?: boolean;
|
||||
stopPropagation?: boolean;
|
||||
enableConfirm?: boolean;
|
||||
isInteractive?: boolean;
|
||||
}
|
||||
export default class RemoveButton extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
size: "small",
|
||||
style: "outline",
|
||||
stopPropagation: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, children, ...restProps } = this.props;
|
||||
|
||||
let classNames: Array<string> = ["ct-remove-button"];
|
||||
if (className) {
|
||||
classNames.push(className);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeButton {...restProps} className={classNames.join(" ")}>
|
||||
{children ? children : "Delete"}
|
||||
</ThemeButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import RemoveButton from "./RemoveButton";
|
||||
|
||||
export default RemoveButton;
|
||||
export { RemoveButton };
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "@dndbeyond/character-components/es";
|
||||
|
||||
interface Props {
|
||||
onClick?: ((evt: React.MouseEvent<HTMLButtonElement>) => void) | (() => void);
|
||||
className: string;
|
||||
disabled?: boolean;
|
||||
styledDisabled?: boolean;
|
||||
block?: boolean;
|
||||
size?: string;
|
||||
style?: string;
|
||||
active?: boolean;
|
||||
stopPropagation?: boolean;
|
||||
enableConfirm?: boolean;
|
||||
confirmText?: string;
|
||||
confirmDuration?: number;
|
||||
isInteractive?: boolean;
|
||||
}
|
||||
export default class ThemeButton extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
style: "filled",
|
||||
stopPropagation: true,
|
||||
isInteractive: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { style, className, isInteractive, ...restProps } = this.props;
|
||||
|
||||
let classNames: Array<string> = ["ct-theme-button"];
|
||||
if (className) {
|
||||
classNames.push(className);
|
||||
}
|
||||
if (style) {
|
||||
classNames.push(`ct-theme-button--${style}`);
|
||||
} else {
|
||||
classNames.push("ct-theme-button--filled");
|
||||
}
|
||||
if (isInteractive) {
|
||||
classNames.push("ct-theme-button--interactive");
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...restProps}
|
||||
className={classNames.join(" ")}
|
||||
isInteractive={isInteractive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ThemeButton from "./ThemeButton";
|
||||
|
||||
export default ThemeButton;
|
||||
export { ThemeButton };
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import List from "@mui/material/List";
|
||||
import ListSubheader from "@mui/material/ListSubheader";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Popover from "@mui/material/Popover";
|
||||
import React from "react";
|
||||
|
||||
import { GroupedMenuOption } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { ThemeButton } from "..";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
groupedOptions: Array<GroupedMenuOption>;
|
||||
className?: string;
|
||||
size?: string;
|
||||
buttonStyle?: string;
|
||||
showSingleOption?: boolean;
|
||||
onSelect: (containerDefinitionKey: string) => void;
|
||||
containerEl?: HTMLElement;
|
||||
}
|
||||
|
||||
export const ThemeButtonWithMenu: React.FC<Props> = ({
|
||||
className,
|
||||
children,
|
||||
groupedOptions,
|
||||
size = "small",
|
||||
buttonStyle = "filled",
|
||||
onSelect,
|
||||
showSingleOption = false,
|
||||
containerEl,
|
||||
}) => {
|
||||
const singleOption =
|
||||
groupedOptions.length === 1 &&
|
||||
groupedOptions[0].options?.length === 1 &&
|
||||
!showSingleOption;
|
||||
|
||||
const classNames = [className, "ct-theme-button-with-menu"];
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!singleOption) {
|
||||
setAnchorEl(event.currentTarget);
|
||||
} else if (singleOption) {
|
||||
onSelect(groupedOptions[0].options[0]?.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (value?: string) => {
|
||||
if (value) {
|
||||
onSelect(value);
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={classNames.join(" ")}>
|
||||
<ThemeButton
|
||||
size={size}
|
||||
style={buttonStyle}
|
||||
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
handleClick(evt);
|
||||
}}
|
||||
data-testid={`theme-button-with-menu-${children?.toString()}`
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, "-")}
|
||||
>
|
||||
{children}{" "}
|
||||
{!singleOption && (
|
||||
<KeyboardArrowDownIcon sx={{ width: 8, height: 8 }} />
|
||||
)}
|
||||
</ThemeButton>
|
||||
{!singleOption && (
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
container={containerEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={(evt: any) => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent?.stopImmediatePropagation();
|
||||
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<List
|
||||
sx={{ bgcolor: "background.default" }}
|
||||
onClick={(evt) => {
|
||||
evt.nativeEvent?.stopImmediatePropagation();
|
||||
evt.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{groupedOptions.map((groupedOption) => (
|
||||
<React.Fragment key={groupedOption.label}>
|
||||
<ListSubheader
|
||||
sx={{
|
||||
lineHeight: "32px",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 700,
|
||||
fontSize: "12px",
|
||||
bgcolor: "background.default",
|
||||
}}
|
||||
data-testid={`theme-button-with-menu-subheader-${groupedOption.label}`
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, "-")}
|
||||
>
|
||||
{groupedOption.label}
|
||||
</ListSubheader>
|
||||
{groupedOption.options.map((option) => {
|
||||
return (
|
||||
<MenuItem
|
||||
sx={{ bgcolor: "background.default" }}
|
||||
key={option.value}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent?.stopImmediatePropagation();
|
||||
handleClose(option.value);
|
||||
}}
|
||||
data-testid={`theme-button-with-menu-item-${option.label}`
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, "-")}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</Popover>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeButtonWithMenu;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ThemeButtonWithMenu } from "./ThemeButtonWithMenu";
|
||||
|
||||
export default ThemeButtonWithMenu;
|
||||
export { ThemeButtonWithMenu };
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import clsx from "clsx";
|
||||
import { orderBy } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { Checkbox } from "~/components/Checkbox";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
|
||||
import { Accordion } from "../../../../../../components/Accordion";
|
||||
import styles from "./styles.module.css";
|
||||
import { CheckboxInfo } from "./typings";
|
||||
|
||||
export enum CheckboxVariant {
|
||||
BUILDER = "builder",
|
||||
SIDEBAR = "sidebar",
|
||||
DEFAULT = "default",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
checkboxes: Array<CheckboxInfo>;
|
||||
heading: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
checkUncheckAllEnabled?: boolean;
|
||||
onCheckUncheckAll?: CheckboxInfo;
|
||||
showAccordion?: boolean;
|
||||
accordionHeading?: string;
|
||||
themed?: boolean;
|
||||
darkMode?: boolean;
|
||||
variant?: "builder" | "sidebar" | "default";
|
||||
allText?: string;
|
||||
}
|
||||
|
||||
export const FormCheckBoxesField: React.FC<Props> = ({
|
||||
heading,
|
||||
description,
|
||||
checkboxes,
|
||||
showAccordion,
|
||||
checkUncheckAllEnabled,
|
||||
onCheckUncheckAll,
|
||||
themed,
|
||||
darkMode,
|
||||
accordionHeading = "Options",
|
||||
variant = CheckboxVariant.DEFAULT,
|
||||
allText = "All",
|
||||
}) => {
|
||||
const sortedChecks = orderBy(checkboxes, "sortOrder");
|
||||
const allChecksEnabled = sortedChecks.every(
|
||||
(checkbox) => checkbox.initiallyEnabled
|
||||
);
|
||||
|
||||
if (!checkboxes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderSingleCheckbox = () => {
|
||||
const [checkbox] = sortedChecks;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx([
|
||||
styles.checkboxesContainer,
|
||||
variant === CheckboxVariant.SIDEBAR &&
|
||||
styles.checkboxesContainerSidebar,
|
||||
])}
|
||||
>
|
||||
<div className={styles.summary}>
|
||||
<div
|
||||
className={clsx([
|
||||
styles.summaryHeadingGroup,
|
||||
styles.summaryHeadingGroupSingle,
|
||||
])}
|
||||
>
|
||||
<div
|
||||
className={clsx([
|
||||
styles.summaryHeading,
|
||||
variant === CheckboxVariant.SIDEBAR &&
|
||||
styles.summaryHeadingSidebar,
|
||||
darkMode && styles.summaryHeadingSidebarDark,
|
||||
])}
|
||||
>
|
||||
{heading}
|
||||
</div>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox
|
||||
checked={checkbox.initiallyEnabled}
|
||||
aria-label={checkbox.label}
|
||||
onClick={checkbox.onChange}
|
||||
onChangePromise={checkbox.onChangePromise}
|
||||
themed={themed}
|
||||
darkMode={darkMode}
|
||||
disabled={checkbox.enabled === false}
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<div
|
||||
className={clsx([
|
||||
styles.summaryDescription,
|
||||
variant === CheckboxVariant.SIDEBAR &&
|
||||
styles.summaryDescriptionSidebar,
|
||||
darkMode && styles.darkMode,
|
||||
])}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{sortedChecks.map((checkbox, idx) => (
|
||||
<div
|
||||
className={styles.checkboxWrapper}
|
||||
key={idx}
|
||||
data-testid="content-preference-checkbox"
|
||||
>
|
||||
<div className={styles.group} key={idx}>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox
|
||||
checked={checkbox.initiallyEnabled}
|
||||
aria-label={checkbox.label}
|
||||
onClick={checkbox.onChange}
|
||||
onChangePromise={checkbox.onChangePromise}
|
||||
themed={themed}
|
||||
darkMode={darkMode}
|
||||
disabled={checkbox.enabled === false}
|
||||
variant={variant}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx([
|
||||
styles.groupLabel,
|
||||
variant === CheckboxVariant.DEFAULT &&
|
||||
styles.groupLabelDefault,
|
||||
])}
|
||||
>
|
||||
{checkbox.label}
|
||||
</div>
|
||||
</div>
|
||||
{checkbox.description && (
|
||||
<HtmlContent
|
||||
html={checkbox.description}
|
||||
className={clsx([
|
||||
styles.description,
|
||||
variant === CheckboxVariant.DEFAULT &&
|
||||
styles.descriptionDefault,
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return checkboxes.length > 1 ||
|
||||
variant === CheckboxVariant.BUILDER ||
|
||||
variant === CheckboxVariant.DEFAULT ? (
|
||||
<div
|
||||
className={clsx(
|
||||
[styles.checkboxesContainer],
|
||||
variant === CheckboxVariant.SIDEBAR && styles.checkboxesContainerSidebar
|
||||
)}
|
||||
>
|
||||
{((variant === CheckboxVariant.SIDEBAR && !showAccordion) ||
|
||||
variant === CheckboxVariant.BUILDER ||
|
||||
variant === CheckboxVariant.DEFAULT) && (
|
||||
<div className={styles.summary}>
|
||||
<div className={styles.summaryHeadingGroup}>
|
||||
{checkUncheckAllEnabled &&
|
||||
onCheckUncheckAll &&
|
||||
variant === CheckboxVariant.DEFAULT && (
|
||||
<Checkbox
|
||||
checked={allChecksEnabled}
|
||||
aria-label="Check/Uncheck all"
|
||||
onClick={onCheckUncheckAll.onChange}
|
||||
themed={themed}
|
||||
darkMode={darkMode}
|
||||
variant={variant}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={clsx([
|
||||
styles.summaryHeading,
|
||||
variant === CheckboxVariant.SIDEBAR &&
|
||||
styles.summaryHeadingSidebar,
|
||||
darkMode && styles.summaryHeadingSidebarDark,
|
||||
variant === CheckboxVariant.DEFAULT &&
|
||||
styles.summaryHeadingDefault,
|
||||
])}
|
||||
>
|
||||
{heading}
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<div
|
||||
className={clsx([
|
||||
styles.summaryDescription,
|
||||
variant === CheckboxVariant.SIDEBAR &&
|
||||
styles.summaryDescriptionSidebar,
|
||||
darkMode && styles.darkMode,
|
||||
])}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variant === CheckboxVariant.BUILDER && checkUncheckAllEnabled ? (
|
||||
<div className={styles.summaryBuilder}>
|
||||
<div className={styles.summaryHeadingGroup}>
|
||||
{checkUncheckAllEnabled && onCheckUncheckAll && (
|
||||
<Checkbox
|
||||
checked={allChecksEnabled}
|
||||
aria-label="Check/Uncheck all"
|
||||
onClick={onCheckUncheckAll.onChange}
|
||||
themed={themed}
|
||||
darkMode={darkMode}
|
||||
variant={variant}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.summaryHeadingAll}>{allText}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showAccordion ? (
|
||||
<Accordion
|
||||
summary={
|
||||
variant !== CheckboxVariant.SIDEBAR ? accordionHeading : heading
|
||||
}
|
||||
variant="text"
|
||||
resetOpen={!allChecksEnabled}
|
||||
className={clsx([
|
||||
styles.accordion,
|
||||
variant === CheckboxVariant.BUILDER && styles.accordionBuilder,
|
||||
variant === CheckboxVariant.SIDEBAR && styles.accordionSidebar,
|
||||
variant === CheckboxVariant.SIDEBAR &&
|
||||
darkMode &&
|
||||
styles.accordionHeadingDark,
|
||||
])}
|
||||
>
|
||||
{variant === CheckboxVariant.SIDEBAR && (
|
||||
<p className={clsx([styles.accordionSidebarDescription])}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{renderCheckboxes()}
|
||||
</Accordion>
|
||||
) : (
|
||||
renderCheckboxes()
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
renderSingleCheckbox()
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
FocusEvent,
|
||||
HTMLAttributes,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { useMaxLengthErrorHandling } from "~/hooks/useErrorHandling/useMaxLengthErrorHandling";
|
||||
|
||||
export interface FormInputFieldProps
|
||||
extends Omit<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
"onChange" | "onBlur" | "onFocus"
|
||||
> {
|
||||
label: string;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
initialValue?: string | number | null;
|
||||
inputAttributes?: HTMLAttributes<HTMLInputElement>;
|
||||
maxLength?: number;
|
||||
maxLengthErrorMsg?: string;
|
||||
variant?: "default" | "small";
|
||||
// Handlers
|
||||
onChange?: (value: string | number | null) => void;
|
||||
onBlur?: (value: string | number | null) => void;
|
||||
onFocus?: (value: string | number | null) => void;
|
||||
transformValueOnChange?: (value: string) => string | number | null;
|
||||
transformValueOnBlur?: (value: string) => string | number | null;
|
||||
transformValueOnFocus?: (value: string) => string | number | null;
|
||||
}
|
||||
|
||||
export const FormInputField: FC<FormInputFieldProps> = ({
|
||||
label, // req
|
||||
type = "text",
|
||||
placeholder = "",
|
||||
initialValue = "",
|
||||
inputAttributes = {},
|
||||
maxLength,
|
||||
maxLengthErrorMsg = "",
|
||||
...handlers
|
||||
}) => {
|
||||
const prevInitialValue = useRef(initialValue);
|
||||
const initialLengthErrorState = maxLength
|
||||
? (initialValue?.toString().length ?? 0) >= maxLength
|
||||
: false;
|
||||
const { MaxLengthErrorMessage, hideError, handleMaxLengthErrorMsg } =
|
||||
useMaxLengthErrorHandling(
|
||||
initialLengthErrorState,
|
||||
maxLength ?? null,
|
||||
maxLengthErrorMsg
|
||||
);
|
||||
const guid = `FIF_${uuidv4()}`;
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const handleOnChange = (evt: ChangeEvent<HTMLInputElement>): void => {
|
||||
const { onChange = null, transformValueOnChange = null } = handlers;
|
||||
const { value: targetElementValue } = evt.target;
|
||||
const value = transformValueOnChange
|
||||
? transformValueOnChange?.(targetElementValue)
|
||||
: targetElementValue;
|
||||
|
||||
onChange?.(value);
|
||||
setValue(value);
|
||||
if (value !== null) {
|
||||
handleMaxLengthErrorMsg(value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = (evt: FocusEvent<HTMLInputElement>) => {
|
||||
const { onBlur = null, transformValueOnBlur = null } = handlers;
|
||||
const { value: targetElementValue } = evt.target;
|
||||
const value = transformValueOnBlur
|
||||
? transformValueOnBlur?.(targetElementValue)
|
||||
: targetElementValue;
|
||||
|
||||
onBlur?.(value);
|
||||
if (value !== targetElementValue) {
|
||||
setValue(value);
|
||||
}
|
||||
// Always hide any length warnings when leaving the field since you can't go over the limit anyways
|
||||
hideError();
|
||||
};
|
||||
|
||||
const handleOnFocus = (evt: FocusEvent<HTMLInputElement>) => {
|
||||
const { onFocus = null, transformValueOnFocus = null } = handlers;
|
||||
const { value: targetElementValue } = evt.target;
|
||||
const value = transformValueOnFocus
|
||||
? transformValueOnFocus?.(targetElementValue)
|
||||
: targetElementValue;
|
||||
|
||||
onFocus?.(value);
|
||||
if (value !== targetElementValue) {
|
||||
setValue(value);
|
||||
}
|
||||
if (value !== null) {
|
||||
handleMaxLengthErrorMsg(value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue !== prevInitialValue.current) {
|
||||
setValue(initialValue ?? "");
|
||||
prevInitialValue.current = initialValue;
|
||||
}
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<div className={clsx(["builder-field", "form-input-field"])}>
|
||||
<span className="builder-field-label">
|
||||
<label
|
||||
className="builder-field-heading form-input-field-label"
|
||||
htmlFor={guid}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</span>
|
||||
<span className="builder-field-input">
|
||||
<input
|
||||
className="builder-field-value"
|
||||
id={guid}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
onChange={handleOnChange}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleOnFocus}
|
||||
value={value ?? ""}
|
||||
maxLength={maxLength || undefined}
|
||||
{...inputAttributes}
|
||||
/>
|
||||
</span>
|
||||
<MaxLengthErrorMessage />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import React from "react";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props {
|
||||
initialValue: string | null;
|
||||
onChange?: (value: string) => void;
|
||||
onChangePromise?: (
|
||||
newValue: string,
|
||||
oldValue: string,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
heading: React.ReactNode;
|
||||
clsNames: Array<string>;
|
||||
initialOptionRemoved: boolean;
|
||||
options: Array<any>;
|
||||
description?: React.ReactNode;
|
||||
block: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
interface State {
|
||||
value: string | null;
|
||||
}
|
||||
export default class FormSelectField extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
initialValue: null,
|
||||
clsNames: [],
|
||||
initialOptionRemoved: false,
|
||||
block: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: props.initialValue,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<Props>,
|
||||
prevState: Readonly<State>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
const { initialValue } = this.props;
|
||||
|
||||
if (initialValue !== prevProps.initialValue) {
|
||||
this.setState({
|
||||
value: initialValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (value: string): void => {
|
||||
const { onChange } = this.props;
|
||||
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
//TODO test that removing newValue from state still works in builder choices
|
||||
handleChangePromise = (
|
||||
newValue: string,
|
||||
oldValue: string,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
): void => {
|
||||
const { onChangePromise } = this.props;
|
||||
|
||||
if (onChangePromise) {
|
||||
onChangePromise(newValue, oldValue, accept, reject);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
const {
|
||||
onChangePromise,
|
||||
onChange,
|
||||
block,
|
||||
heading,
|
||||
options,
|
||||
clsNames,
|
||||
initialOptionRemoved,
|
||||
description,
|
||||
placeholder,
|
||||
} = this.props;
|
||||
|
||||
const uId: string = uniqueId("qry-");
|
||||
|
||||
let conClsNames: Array<string> = ["builder-field", "builder-field-select"];
|
||||
if (clsNames.length) {
|
||||
conClsNames = [...conClsNames, ...clsNames];
|
||||
}
|
||||
if (block) {
|
||||
conClsNames.push("builder-field-select-block");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={conClsNames.join(" ")}>
|
||||
<div className="builder-field-select-summary">
|
||||
<label
|
||||
className="builder-field-heading form-input-field-label"
|
||||
htmlFor={uId}
|
||||
>
|
||||
{heading}
|
||||
</label>
|
||||
<div className={styles.summaryDescription}>{description}</div>
|
||||
</div>
|
||||
<div className="builder-field-select-input">
|
||||
<Select
|
||||
options={options}
|
||||
onChange={onChange ? this.handleChange : undefined}
|
||||
onChangePromise={
|
||||
onChangePromise ? this.handleChangePromise : undefined
|
||||
}
|
||||
value={value}
|
||||
initialOptionRemoved={initialOptionRemoved}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import FormSelectField from "./FormSelectField";
|
||||
|
||||
export default FormSelectField;
|
||||
export { FormSelectField };
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
|
||||
interface Props {
|
||||
initiallyEnabled: boolean;
|
||||
onChange?: (isEnabled?: boolean) => void;
|
||||
onChangePromise?: (
|
||||
newIsEnabled: boolean,
|
||||
accept: () => void,
|
||||
reject: () => void
|
||||
) => void;
|
||||
heading: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
toggleLabel: string;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
export default class FormToggleField extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
initiallyEnabled: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
heading,
|
||||
description,
|
||||
initiallyEnabled,
|
||||
onChange,
|
||||
onChangePromise,
|
||||
toggleLabel,
|
||||
isReadOnly,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="builder-field builder-field-toggle">
|
||||
<div className="builder-field-toggle-summary">
|
||||
<div className="builder-field-toggle-heading">{heading}</div>
|
||||
{description && (
|
||||
<div className="builder-field-toggle-description">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="builder-field-toggle-input">
|
||||
{onChange && (
|
||||
<Toggle
|
||||
checked={initiallyEnabled}
|
||||
onClick={onChange}
|
||||
color="secondary"
|
||||
aria-label={toggleLabel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import FormToggleField from "./FormToggleField";
|
||||
|
||||
export default FormToggleField;
|
||||
export { FormToggleField };
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
nodes: Array<React.ReactNode>;
|
||||
sep?: string;
|
||||
}
|
||||
|
||||
export default function InlineSeparatedNodes({ nodes, sep = ", " }: Props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{nodes.map((node, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{node}
|
||||
{idx < nodes.length - 1 && sep}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import InlineSeparatedNodes from "./InlineSeparatedNodes";
|
||||
|
||||
export default InlineSeparatedNodes;
|
||||
export { InlineSeparatedNodes };
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
|
||||
import LinkButton from "../LinkButton";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
className?: string;
|
||||
block?: boolean;
|
||||
size: "oversized" | "large" | "medium" | "small";
|
||||
disabled?: boolean;
|
||||
download?: string | boolean;
|
||||
onClick?: (isDisabled: boolean) => void;
|
||||
}
|
||||
export default class BuilderLinkButton extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, ...restProps } = this.props;
|
||||
|
||||
let classNames: Array<string> = ["builder-button"];
|
||||
if (className) {
|
||||
classNames.push(className);
|
||||
}
|
||||
|
||||
return <LinkButton {...restProps} className={classNames.join(" ")} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import BuilderLinkButton from "./BuilderLinkButton";
|
||||
|
||||
export default BuilderLinkButton;
|
||||
export { BuilderLinkButton };
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
className: string;
|
||||
block: boolean;
|
||||
size: "oversized" | "large" | "medium" | "small";
|
||||
disabled: boolean;
|
||||
download?: string | boolean;
|
||||
onClick?: (isDisabled: boolean) => void;
|
||||
target?: string;
|
||||
}
|
||||
export default class LinkButton extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
clsNames: [],
|
||||
block: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
handleClick = (evt: React.MouseEvent) => {
|
||||
const { onClick, disabled } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
onClick(disabled);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
disabled,
|
||||
block,
|
||||
size,
|
||||
className,
|
||||
url,
|
||||
children,
|
||||
download,
|
||||
target,
|
||||
} = this.props;
|
||||
|
||||
let classNames: Array<string> = ["ct-button", className];
|
||||
|
||||
let buttonSizeBase: string = "character-button";
|
||||
if (block) {
|
||||
buttonSizeBase += "-block";
|
||||
}
|
||||
|
||||
let buttonSizeFinal: string;
|
||||
switch (size) {
|
||||
case "oversized":
|
||||
case "large":
|
||||
case "medium":
|
||||
case "small":
|
||||
buttonSizeFinal = `${buttonSizeBase}-${size}`;
|
||||
break;
|
||||
default:
|
||||
buttonSizeFinal = buttonSizeBase;
|
||||
}
|
||||
classNames.push(buttonSizeFinal);
|
||||
|
||||
if (disabled) {
|
||||
classNames.push("character-button-disabled");
|
||||
classNames.push(`${buttonSizeFinal}-disabled`);
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<span className={classNames.join(" ")} onClick={this.handleClick}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target={target}
|
||||
download={download}
|
||||
className={classNames.join(" ")}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import * as React from "react";
|
||||
|
||||
import LinkButton from "../LinkButton";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
className?: string;
|
||||
block?: boolean;
|
||||
size: "oversized" | "large" | "medium" | "small";
|
||||
style: "filled" | "outline";
|
||||
disabled?: boolean;
|
||||
download?: string | boolean;
|
||||
target?: string;
|
||||
}
|
||||
export default class ThemeLinkButton extends React.PureComponent<Props, {}> {
|
||||
static defaultProps = {
|
||||
className: "",
|
||||
style: "filled",
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, style, ...restProps } = this.props;
|
||||
|
||||
let classNames: Array<string> = [
|
||||
"ct-theme-button",
|
||||
`ct-theme-button--${style}`,
|
||||
];
|
||||
if (className) {
|
||||
classNames.push(className);
|
||||
}
|
||||
|
||||
return <LinkButton {...restProps} className={classNames.join(" ")} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import ThemeLinkButton from "./ThemeLinkButton";
|
||||
|
||||
export default ThemeLinkButton;
|
||||
export { ThemeLinkButton };
|
||||
Reference in New Issue
Block a user