| 1 | // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt |
| 2 | |
| 3 | import { createElement as h, useEffect, useState } from 'react' |
| 4 | import { StringField, StringFieldProps } from './StringField' |
| 5 | import { FieldProps } from '.' |
| 6 | import { |
| 7 | Box, Checkbox, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, IconButton, |
| 8 | InputAdornment, Switch |
| 9 | } from '@mui/material' |
| 10 | import { Cancel } from '@mui/icons-material' |
| 11 | import _ from 'lodash' |
| 12 | import { useGetSize } from '@hfs/shared' |
| 13 | |
| 14 | export function DisplayField({ value, empty='-', ...props }: Omit<StringFieldProps, 'onChange'> & { value: any }) { |
| 15 | if (!props.toField && empty !== undefined && value !== 0 && !value) |
| 16 | value = empty |
| 17 | return h(StringField, { ...props, value, disabled: true, onChange() {} }) |
| 18 | } |
| 19 | |
| 20 | export function NumberField({ value, onChange, setApi, required, min=0, max, step, unit, clearable, ...props }: FieldProps<number | null>) { |
| 21 | setApi?.({ |
| 22 | getError() { |
| 23 | return value == null ? (required ? "required" : false) |
| 24 | : (value < min ? "too low" : value > max ? "too high" : false) |
| 25 | } |
| 26 | }) |
| 27 | const size = useGetSize() |
| 28 | return h(StringField, { |
| 29 | type: 'number', |
| 30 | value: value == null ? '' : String(value), |
| 31 | onChange(v, { was, ...rest }) { |
| 32 | onChange(!v ? null : Number(v), { |
| 33 | ...rest, |
| 34 | was: was ? Number(was) : null, |
| 35 | }) |
| 36 | }, |
| 37 | inputProps: { min, max, step, }, |
| 38 | InputProps: _.merge({ |
| 39 | sx: { '& input': { appearance: 'textfield' } }, |
| 40 | startAdornment: (clearable ?? props.placeholder) && size.w! > 100 && (value || value === 0) && h(InputAdornment, { |
| 41 | position: 'start', |
| 42 | }, h(IconButton, { |
| 43 | size: 'small', |
| 44 | edge: 'start', |
| 45 | sx: { ml: -1, opacity: .5 }, |
| 46 | 'aria-label': "clear", |
| 47 | onClick(event){ onChange(null, { was: value, event }) } |
| 48 | }, h(Cancel))), |
| 49 | }, unit && { |
| 50 | sx: { pr: '6px', '& input': { pl: '.2em', textAlign: 'right' } }, |
| 51 | endAdornment: h(InputAdornment, { |
| 52 | position: 'end', |
| 53 | sx: { mt: '1.2em', ml: '5px', '& p': { fontSize: '80%', '.Mui-focused &': { color: 'text.primary' } } } |
| 54 | }, unit), |
| 55 | }), |
| 56 | ...props, |
| 57 | fieldRef: size.refToPass, |
| 58 | }) |
| 59 | } |
| 60 | |
| 61 | export function BoolField({ label='', value, onChange, setApi, helperText, error, Control=Switch, |
| 62 | type, // avoid passing this by accident, as it disrupts the control |
| 63 | ...props }: FieldProps<boolean>) { |
| 64 | const setter = () => value ?? false |
| 65 | const [state, setState] = useState(setter) |
| 66 | useEffect(() => setState(setter), |
| 67 | [value]) //eslint-disable-line |
| 68 | const control = h(Control, { |
| 69 | checked: state, |
| 70 | ...props, |
| 71 | onChange(event) { |
| 72 | onChange((event.target as any).checked, { event, was: value }) |
| 73 | } |
| 74 | }) |
| 75 | return h(Box, { sx: { ml: 1, ...error && { color: 'error.main', outlineOffset: 6, outline: '1px solid' } } }, |
| 76 | h(FormControlLabel, { label, control, labelPlacement: 'end', sx: { mr: 0, ...props.size==='small' && { '& .MuiFormControlLabel-label': { fontSize: '.9rem' } } } }), |
| 77 | helperText && h(FormHelperText, { sx: { mt: 0 }, error }, helperText) |
| 78 | ) |
| 79 | } |
| 80 | |
| 81 | export function CheckboxField(props: FieldProps<boolean>) { |
| 82 | return h(BoolField, { Control: Checkbox, ...props }) |
| 83 | } |
| 84 | |
| 85 | export function CheckboxesField({ label, options, value, onChange, columns, columnWidth }: FieldProps<string[]> & { options: string[] }) { |
| 86 | const doCols = columns > 1 || Boolean(columnWidth) |
| 87 | return h(FormControl, { fullWidth: doCols }, |
| 88 | label && h(FormLabel, {}, label), |
| 89 | h(FormGroup, { sx: { ...doCols && { columns, columnWidth, '&, & label': { display: 'block' } } } }, |
| 90 | options.map(o => { |
| 91 | const checked = value?.includes(o) |
| 92 | return h(FormControlLabel, { |
| 93 | key: o, |
| 94 | checked, |
| 95 | control: h(Checkbox), |
| 96 | label: o, |
| 97 | onClick(event) { |
| 98 | const newValue = checked ? value!.filter(x => x !== o) : [...value||[], o] |
| 99 | onChange(newValue, { was: value, event }) |
| 100 | } |
| 101 | }) |
| 102 | })) |
| 103 | ) |
| 104 | } |