aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common/input/InputGroup.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views/common/input/InputGroup.tsx')
-rw-r--r--FrontEnd/src/views/common/input/InputGroup.tsx461
1 files changed, 0 insertions, 461 deletions
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx
deleted file mode 100644
index d95bb29e..00000000
--- a/FrontEnd/src/views/common/input/InputGroup.tsx
+++ /dev/null
@@ -1,461 +0,0 @@
-/**
- * Some notes for InputGroup:
- * This is one of the most complicated components in this project.
- * Probably because the feature is complex and involved user inputs.
- *
- * I hope it contains following features:
- * - Input features
- * - Supports a wide range of input types.
- * - Validator to validate user inputs.
- * - Can set initial values.
- * - Dirty, aka, has user touched this input.
- * - Developer friendly
- * - Easy to use APIs.
- * - Type check as much as possible.
- * - UI
- * - Configurable appearance.
- * - Can display helper and error messages.
- * - Easy to extend, like new input types.
- *
- * So here is some design decisions:
- * Inputs are identified by its _key_.
- * `InputGroup` component takes care of only UI and no logic.
- * `useInputs` hook takes care of logic and generate props for `InputGroup`.
- */
-
-import { useState, Ref, useId } from "react";
-import classNames from "classnames";
-
-import { useC, Text, ThemeColor } from "../common";
-
-import "./InputGroup.css";
-
-export interface InputBase {
- key: string;
- label: Text;
- helper?: Text;
- disabled?: boolean;
- error?: Text;
-}
-
-export interface TextInput extends InputBase {
- type: "text";
- value: string;
- password?: boolean;
-}
-
-export interface BoolInput extends InputBase {
- type: "bool";
- value: boolean;
-}
-
-export interface SelectInputOption {
- value: string;
- label: Text;
- icon?: string;
-}
-
-export interface SelectInput extends InputBase {
- type: "select";
- value: string;
- options: SelectInputOption[];
-}
-
-export type Input = TextInput | BoolInput | SelectInput;
-
-export type InputValue = Input["value"];
-
-export type InputValueDict = Record<string, InputValue>;
-export type InputErrorDict = Record<string, Text>;
-export type InputDisabledDict = Record<string, boolean>;
-export type InputDirtyDict = Record<string, boolean>;
-
-export type GeneralInputErrorDict =
- | {
- [key: string]: Text | null | undefined;
- }
- | null
- | undefined;
-
-type MakeInputInfo<I extends Input> = Omit<I, "value" | "error" | "disabled">;
-
-export type InputInfo = {
- [I in Input as I["type"]]: MakeInputInfo<I>;
-}[Input["type"]];
-
-export type Validator = (
- values: InputValueDict,
- inputs: InputInfo[],
-) => GeneralInputErrorDict;
-
-export type InputScheme = {
- inputs: InputInfo[];
- validator?: Validator;
-};
-
-export type InputData = {
- values: InputValueDict;
- errors: InputErrorDict;
- disabled: InputDisabledDict;
- dirties: InputDirtyDict;
-};
-
-export type State = {
- scheme: InputScheme;
- data: InputData;
-};
-
-export type DataInitialization = {
- values?: InputValueDict;
- errors?: GeneralInputErrorDict;
- disabled?: InputDisabledDict;
- dirties?: InputDirtyDict;
-};
-
-export type Initialization = {
- scheme: InputScheme;
- dataInit?: DataInitialization;
-};
-
-export type GeneralInitialization = Initialization | InputScheme | InputInfo[];
-
-export type Initializer = GeneralInitialization | (() => GeneralInitialization);
-
-export interface InputGroupProps {
- color?: ThemeColor;
- containerClassName?: string;
- containerRef?: Ref<HTMLDivElement>;
-
- inputs: Input[];
- onChange: (index: number, value: Input["value"]) => void;
-}
-
-function cleanObject<V>(o: Record<string, V>): Record<string, NonNullable<V>> {
- const result = { ...o };
- for (const key of Object.keys(result)) {
- if (result[key] == null) {
- delete result[key];
- }
- }
- return result as never;
-}
-
-export type ConfirmResult =
- | {
- type: "ok";
- values: InputValueDict;
- }
- | {
- type: "error";
- errors: InputErrorDict;
- };
-
-function validate(
- validator: Validator | null | undefined,
- values: InputValueDict,
- inputs: InputInfo[],
-): InputErrorDict {
- return cleanObject(validator?.(values, inputs) ?? {});
-}
-
-export function useInputs(options: { init: Initializer }): {
- inputGroupProps: InputGroupProps;
- hasError: boolean;
- hasErrorAndDirty: boolean;
- confirm: () => ConfirmResult;
- setAllDisabled: (disabled: boolean) => void;
-} {
- function initializeValue(
- input: InputInfo,
- value?: InputValue | null,
- ): InputValue {
- if (input.type === "text") {
- return value ?? "";
- } else if (input.type === "bool") {
- return value ?? false;
- } else if (input.type === "select") {
- return value ?? input.options[0].value;
- }
- throw new Error("Unknown input type");
- }
-
- function initialize(generalInitialization: GeneralInitialization): State {
- const initialization: Initialization = Array.isArray(generalInitialization)
- ? { scheme: { inputs: generalInitialization } }
- : "scheme" in generalInitialization
- ? generalInitialization
- : { scheme: generalInitialization };
-
- const { scheme, dataInit } = initialization;
- const { inputs, validator } = scheme;
- const keys = inputs.map((input) => input.key);
-
- if (process.env.NODE_ENV === "development") {
- const checkKeys = (dict: Record<string, unknown> | undefined) => {
- if (dict != null) {
- for (const key of Object.keys(dict)) {
- if (!keys.includes(key)) {
- console.warn("");
- }
- }
- }
- };
-
- checkKeys(dataInit?.values);
- checkKeys(dataInit?.errors ?? {});
- checkKeys(dataInit?.disabled);
- checkKeys(dataInit?.dirties);
- }
-
- function clean<V>(
- dict: Record<string, V> | null | undefined,
- ): Record<string, NonNullable<V>> {
- return dict != null ? cleanObject(dict) : {};
- }
-
- const values: InputValueDict = {};
- const disabled: InputDisabledDict = clean(dataInit?.disabled);
- const dirties: InputDirtyDict = clean(dataInit?.dirties);
- const isErrorSet = dataInit?.errors != null;
- let errors: InputErrorDict = clean(dataInit?.errors);
-
- for (let i = 0; i < inputs.length; i++) {
- const input = inputs[i];
- const { key } = input;
-
- values[key] = initializeValue(input, dataInit?.values?.[key]);
- }
-
- if (isErrorSet) {
- if (process.env.NODE_ENV === "development") {
- console.log(
- "You explicitly set errors (not undefined) in initializer, so validator won't run.",
- );
- }
- } else {
- errors = validate(validator, values, inputs);
- }
-
- return {
- scheme,
- data: {
- values,
- errors,
- disabled,
- dirties,
- },
- };
- }
-
- const { init } = options;
- const initializer = typeof init === "function" ? init : () => init;
-
- const [state, setState] = useState<State>(() => initialize(initializer()));
-
- const { scheme, data } = state;
- const { validator } = scheme;
-
- function createAllBooleanDict(value: boolean): Record<string, boolean> {
- const result: InputDirtyDict = {};
- for (const key of scheme.inputs.map((input) => input.key)) {
- result[key] = value;
- }
- return result;
- }
-
- const createAllDirties = () => createAllBooleanDict(true);
-
- const componentInputs: Input[] = [];
-
- for (let i = 0; i < scheme.inputs.length; i++) {
- const input = scheme.inputs[i];
- const value = data.values[input.key];
- const error = data.errors[input.key];
- const disabled = data.disabled[input.key] ?? false;
- const dirty = data.dirties[input.key] ?? false;
- const componentInput: Input = {
- ...input,
- value: value as never,
- disabled,
- error: dirty ? error : undefined,
- };
- componentInputs.push(componentInput);
- }
-
- const hasError = Object.keys(data.errors).length > 0;
- const hasDirty = Object.keys(data.dirties).some((key) => data.dirties[key]);
-
- return {
- inputGroupProps: {
- inputs: componentInputs,
- onChange: (index, value) => {
- const input = scheme.inputs[index];
- const { key } = input;
- const newValues = { ...data.values, [key]: value };
- const newDirties = { ...data.dirties, [key]: true };
- const newErrors = validate(validator, newValues, scheme.inputs);
- setState({
- scheme,
- data: {
- ...data,
- values: newValues,
- errors: newErrors,
- dirties: newDirties,
- },
- });
- },
- },
- hasError,
- hasErrorAndDirty: hasError && hasDirty,
- confirm() {
- const newDirties = createAllDirties();
- const newErrors = validate(validator, data.values, scheme.inputs);
-
- setState({
- scheme,
- data: {
- ...data,
- dirties: newDirties,
- errors: newErrors,
- },
- });
-
- if (Object.keys(newErrors).length !== 0) {
- return {
- type: "error",
- errors: newErrors,
- };
- } else {
- return {
- type: "ok",
- values: data.values,
- };
- }
- },
- setAllDisabled(disabled: boolean) {
- setState({
- scheme,
- data: {
- ...data,
- disabled: createAllBooleanDict(disabled),
- },
- });
- },
- };
-}
-
-export function InputGroup({
- color,
- inputs,
- onChange,
- containerRef,
- containerClassName,
-}: InputGroupProps) {
- const c = useC();
-
- const id = useId();
-
- return (
- <div
- ref={containerRef}
- className={classNames(
- "cru-input-group",
- `cru-clickable-${color ?? "primary"}`,
- containerClassName,
- )}
- >
- {inputs.map((item, index) => {
- const { key, type, value, label, error, helper, disabled } = item;
-
- const getContainerClassName = (
- ...additionalClassNames: classNames.ArgumentArray
- ) =>
- classNames(
- `cru-input-container cru-input-type-${type}`,
- error && "error",
- ...additionalClassNames,
- );
-
- const changeValue = (value: InputValue) => {
- onChange(index, value);
- };
-
- const inputId = `${id}-${key}`;
-
- if (type === "text") {
- const { password } = item;
- return (
- <div
- key={key}
- className={getContainerClassName(password && "password")}
- >
- {label && (
- <label className="cru-input-label" htmlFor={inputId}>
- {c(label)}
- </label>
- )}
- <input
- id={inputId}
- type={password ? "password" : "text"}
- value={value}
- onChange={(event) => {
- const v = event.target.value;
- changeValue(v);
- }}
- disabled={disabled}
- />
- {error && <div className="cru-input-error">{c(error)}</div>}
- {helper && <div className="cru-input-helper">{c(helper)}</div>}
- </div>
- );
- } else if (type === "bool") {
- return (
- <div key={key} className={getContainerClassName()}>
- <input
- id={inputId}
- type="checkbox"
- checked={value}
- onChange={(event) => {
- const v = event.currentTarget.checked;
- changeValue(v);
- }}
- disabled={disabled}
- />
- <label className="cru-input-label-inline" htmlFor={inputId}>
- {c(label)}
- </label>
- {error && <div className="cru-input-error">{c(error)}</div>}
- {helper && <div className="cru-input-helper">{c(helper)}</div>}
- </div>
- );
- } else if (type === "select") {
- return (
- <div key={key} className={getContainerClassName()}>
- <label className="cru-input-label" htmlFor={inputId}>
- {c(label)}
- </label>
- <select
- id={inputId}
- value={value}
- onChange={(event) => {
- const e = event.target.value;
- changeValue(e);
- }}
- disabled={disabled}
- >
- {item.options.map((option) => {
- return (
- <option value={option.value} key={option.value}>
- {option.icon}
- {c(option.label)}
- </option>
- );
- })}
- </select>
- </div>
- );
- }
- })}
- </div>
- );
-}