2025-05-28 15:36:51 -07:00

245 lines
5.6 KiB
TypeScript

import React from "react";
import {
ApiAdapterPromise,
ApiAdapterUtils,
ApiResponse,
} from "@dndbeyond/character-rules-engine/es";
interface Props<T extends any = any, TData extends any = any> {
loadOptions?: () => ApiAdapterPromise<ApiResponse<Array<TData>>>;
clsNames: Array<string>;
id?: any; //TODO type?
options: Array<any>; //TODO type Options
disabled: boolean;
value: React.ReactText | null;
placeholder: string;
onChange?: (value: string) => void;
onChangePromise?: (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
) => void;
parseLoadedOptions?: (data: Array<TData>) => Array<T>;
resetAfterChoice: boolean;
initialOptionRemoved: boolean;
preventClickPropagating: boolean;
isReadonly?: boolean;
className: string;
}
interface State {
isLazy: boolean;
lazyOptions: Array<any>;
value: React.ReactText;
resetAfterChoiceValues: Array<string>;
}
export default class Select extends React.Component<Props, State> {
static defaultProps = {
clsNames: [],
className: "",
disabled: false,
resetAfterChoice: false,
initialOptionRemoved: false,
placeholder: "-- Choose an Option --",
options: [],
preventClickPropagating: false,
};
constructor(props: Props) {
super(props);
this.state = {
isLazy: !!props.loadOptions,
lazyOptions: [],
value: props.value === null ? "" : props.value,
resetAfterChoiceValues: this.getResetAfterValueChoices(props),
};
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<State>,
snapshot?: any
): void {
const { value } = this.props;
if (value !== prevProps.value) {
this.setState({
value: value === null ? "" : value,
});
}
}
componentDidMount() {
const { isLazy } = this.state;
const { loadOptions, parseLoadedOptions } = this.props;
if (isLazy && loadOptions && parseLoadedOptions) {
loadOptions().then((response) => {
const data = ApiAdapterUtils.getResponseData(response);
if (data === null) {
return;
}
this.setState({
lazyOptions: parseLoadedOptions(data),
});
});
}
}
getResetAfterValueChoices = (props: Props): Array<string> => {
const { options, resetAfterChoice } = props;
if (resetAfterChoice) {
return options.reduce((acc, option) => {
if (option.options) {
acc.push(...option.options.map((opt) => "" + opt.value));
} else {
acc.push("" + option.value);
}
return acc;
}, []);
}
return options.reduce((acc, option) => {
if (option.options && option.resetAfterChoice) {
acc.push(...option.options.map((opt) => "" + opt.value));
}
return acc;
}, []);
};
handleChange = (evt: React.ChangeEvent<HTMLSelectElement>): void => {
const { value, resetAfterChoiceValues } = this.state;
const { onChangePromise, onChange, resetAfterChoice } = this.props;
const newValue: string = evt.target.value;
if (onChangePromise) {
onChangePromise(
newValue,
String(value),
() => {
this.setState({
value: newValue,
});
if (onChange) {
onChange(newValue);
}
if (resetAfterChoice) {
evt.target.selectedIndex = 0;
}
},
() => {}
);
} else {
if (resetAfterChoice || resetAfterChoiceValues.includes(newValue)) {
evt.target.selectedIndex = 0;
} else {
this.setState({
value: newValue,
});
}
if (onChange) {
onChange(newValue);
}
}
};
handleClick = (evt: React.MouseEvent): void => {
const { preventClickPropagating } = this.props;
if (preventClickPropagating) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
}
};
renderOption = (option: any): React.ReactNode => {
return (
<option
key={option.value}
value={String(option.value)}
disabled={option.disabled}
>
{option.label}
</option>
);
};
renderOptGroup = (
label: string,
options: Array<any>,
resetAfterChoice: boolean
) => {
if (!options.length) {
return null;
}
return (
<optgroup label={label} key={label}>
{options.map((option) => this.renderOption(option))}
</optgroup>
);
};
render() {
const { isLazy, lazyOptions, value } = this.state;
const {
options,
placeholder,
initialOptionRemoved,
id,
disabled,
clsNames,
isReadonly,
className,
} = this.props;
let displayOptions: Array<any> = options;
if (isLazy) {
displayOptions = lazyOptions;
}
const conClassNames: Array<string> = [
"ddbc-select",
...clsNames,
className,
];
return (
<select
id={id}
className={conClassNames.join(" ")}
value={value}
onChange={this.handleChange}
onClick={this.handleClick}
disabled={disabled || isReadonly}
>
<option
value=""
disabled={initialOptionRemoved}
hidden={initialOptionRemoved}
>
{placeholder}
</option>
{displayOptions.map((option) => {
if (option.options) {
return this.renderOptGroup(
option.optGroupLabel,
option.options,
option.resetAfterChoice
);
} else {
return this.renderOption(option);
}
})}
</select>
);
}
}