diff options
author | crupest <crupest@outlook.com> | 2022-04-28 14:25:09 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2022-04-28 14:25:09 +0800 |
commit | 41e872d37677e8eba75a868f07205319889ffe9f (patch) | |
tree | 61afeb1ed09013e00f559d5ab7964da2708b5609 /FrontEnd/src/views/common/input/InputPanel.tsx | |
parent | a4f5080c6dc8c3fc7f76aebb13cbf54c0ed7ef15 (diff) | |
download | timeline-41e872d37677e8eba75a868f07205319889ffe9f.tar.gz timeline-41e872d37677e8eba75a868f07205319889ffe9f.tar.bz2 timeline-41e872d37677e8eba75a868f07205319889ffe9f.zip |
...
Diffstat (limited to 'FrontEnd/src/views/common/input/InputPanel.tsx')
-rw-r--r-- | FrontEnd/src/views/common/input/InputPanel.tsx | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx new file mode 100644 index 00000000..1270cc53 --- /dev/null +++ b/FrontEnd/src/views/common/input/InputPanel.tsx @@ -0,0 +1,247 @@ +import React from "react"; +import classNames from "classnames"; +import { useTranslation } from "react-i18next"; +import { TwitterPicker } from "react-color"; + +import { convertI18nText, I18nText } from "@/common"; + +import "./InputPanel.css"; + +export interface TextInput { + type: "text"; + label?: I18nText; + helper?: I18nText; + password?: boolean; +} + +export interface BoolInput { + type: "bool"; + label: I18nText; + helper?: I18nText; +} + +export interface SelectInputOption { + value: string; + label: I18nText; + icon?: React.ReactElement; +} + +export interface SelectInput { + type: "select"; + label: I18nText; + options: SelectInputOption[]; +} + +export interface ColorInput { + type: "color"; + label?: I18nText; +} + +export interface DateTimeInput { + type: "datetime"; + label?: I18nText; + helper?: I18nText; +} + +export type Input = + | TextInput + | BoolInput + | SelectInput + | ColorInput + | DateTimeInput; + +interface InputTypeToValueTypeMap { + text: string; + bool: boolean; + select: string; + color: string; + datetime: string; +} + +type ValueTypes = InputTypeToValueTypeMap[keyof InputTypeToValueTypeMap]; + +type MapInputTypeToValueType<Type> = Type extends keyof InputTypeToValueTypeMap + ? InputTypeToValueTypeMap[Type] + : never; + +type MapInputToValueType<T> = T extends Input + ? MapInputTypeToValueType<T["type"]> + : T; + +type MapInputListToValueTypeList<Tuple extends readonly Input[]> = { + [Index in keyof Tuple]: MapInputToValueType<Tuple[Index]>; +} & { length: Tuple["length"] }; + +export type OperationInputError = (I18nText | null | undefined)[]; + +export interface InputPanelProps<InputList extends readonly Input[]> { + scheme: InputList; + values: MapInputListToValueTypeList<InputList>; + onChange: ( + values: MapInputListToValueTypeList<InputList>, + index: number + ) => void; + error?: OperationInputError; + disable?: boolean; +} + +const InputPanel = <InputList extends readonly Input[]>( + props: InputPanelProps<InputList> +): React.ReactElement => { + const { values, onChange, scheme, error, disable } = props; + + const { t } = useTranslation(); + + const updateValue = (index: number, newValue: ValueTypes): void => { + const oldValues = values; + const newValues = oldValues.slice(); + newValues[index] = newValue; + onChange( + newValues as unknown as MapInputListToValueTypeList<InputList>, + index + ); + }; + + return ( + <div> + {scheme.map((item, index) => { + const v = values[index]; + const e: string | null = convertI18nText(error?.[index], t); + + if (item.type === "text") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + {item.label && ( + <label className="cru-input-panel-label"> + {convertI18nText(item.label, t)} + </label> + )} + <input + type={item.password === true ? "password" : "text"} + value={v as string} + onChange={(e) => { + const v = e.target.value; + updateValue(index, v); + }} + disabled={disable} + /> + {e && <div className="cru-input-panel-error-text">{e}</div>} + {item.helper && ( + <div className="cru-input-panel-helper-text"> + {convertI18nText(item.helper, t)} + </div> + )} + </div> + ); + } else if (item.type === "bool") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + <input + type="checkbox" + checked={v as boolean} + onChange={(event) => { + const value = event.currentTarget.checked; + updateValue(index, value); + }} + disabled={disable} + /> + <label className="cru-input-panel-inline-label"> + {convertI18nText(item.label, t)} + </label> + {e != null && ( + <div className="cru-input-panel-error-text">{e}</div> + )} + {item.helper && ( + <div className="cru-input-panel-helper-text"> + {convertI18nText(item.helper, t)} + </div> + )} + </div> + ); + } else if (item.type === "select") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + <label className="cru-input-panel-label"> + {convertI18nText(item.label, t)} + </label> + <select + value={v as string} + onChange={(event) => { + const value = event.target.value; + updateValue(index, value); + }} + disabled={disable} + > + {item.options.map((option, i) => { + return ( + <option value={option.value} key={i}> + {option.icon} + {convertI18nText(option.label, t)} + </option> + ); + })} + </select> + </div> + ); + } else if (item.type === "color") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + <label className="cru-input-panel-inline-label"> + {convertI18nText(item.label, t)} + </label> + <TwitterPicker + color={v as string} + triangle="hide" + onChange={(result) => updateValue(index, result.hex)} + /> + </div> + ); + } else if (item.type === "datetime") { + return ( + <div + key={index} + className={classNames("cru-input-panel-group", e && "error")} + > + {item.label && ( + <label className="cru-input-panel-label"> + {convertI18nText(item.label, t)} + </label> + )} + <input + type="datetime-local" + value={v as string} + onChange={(e) => { + const v = e.target.value; + updateValue(index, v); + }} + disabled={disable} + /> + {e != null && ( + <div className="cru-input-panel-error-text">{e}</div> + )} + {item.helper && ( + <div className="cru-input-panel-helper-text"> + {convertI18nText(item.helper, t)} + </div> + )} + </div> + ); + } + })} + </div> + ); +}; + +export default InputPanel; |