brf/client/public/shared/components/input_factory.tsx
2025-11-24 17:09:09 +01:00

145 lines
3.8 KiB
TypeScript

import { h, type FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import cn from 'classnames'
import escapeRegex from '../utils/escape_regex.ts'
export default function inputFactory(styles: {
base?: string
touched?: string
element?: string
label?: string
statusIcon?: string
noMargin?: string
icon?: string
labelIcon?: string
center?: string
}): FunctionComponent<{
autoFocus?: boolean
autoComplete?: string
className?: string
defaultValue?: string
disabled?: boolean
name: string
noMargin?: boolean
label: string
pattern?: string
placeholder?: string
required?: boolean
type?: string
showValidity?: boolean
}> {
const Input = ({
autoComplete,
autoFocus,
center,
className,
classNames,
color,
defaultValue,
design,
disabled,
icon,
label,
name,
noMargin,
pattern,
placeholder,
required,
sameAs,
size = 'medium',
type = 'text',
value,
showValidity = true,
}) => {
const [touched, setTouched] = useState(false)
const inputRef = useRef<HTMLInputElement>()
const onBlur = useCallback(() => setTouched(true), [])
useEffect(() => {
function onInput(e) {
inputRef.current!.pattern = escapeRegex(e.target.value)
}
const form = inputRef.current!.form
const resetTouched = setTouched.bind(null, false)
form.addEventListener('reset', resetTouched)
let sameAsInput
if (sameAs) {
sameAsInput = form[sameAs]
sameAsInput.addEventListener('input', onInput)
}
return () => {
form.removeEventListener('reset', resetTouched)
if (sameAsInput) sameAsInput.removeEventListener('input', onInput)
}
}, [])
useEffect(() => {
if (autoFocus) inputRef.current.focus()
}, [autoFocus])
const id = styles.element + '_' + name
return (
<div
className={cn(
styles.base,
design && cn(styles[design], classNames?.[design]),
color && cn(styles[color], classNames?.[color]),
size && cn(styles[size], classNames?.[size]),
noMargin && cn(styles.noMargin, classNames?.noMargin),
touched && cn(styles.touched, classNames?.touched),
center && cn(styles.center, classNames?.center),
className,
)}
>
{icon && (styles.icon || classNames?.icon) && (
<div className={cn(styles.icon, classNames?.icon)} style={{ maskImage: icon }} />
)}
<input
autoComplete={autoComplete}
autoFocus={autoFocus}
className={cn(styles.element, classNames?.element)}
defaultValue={
defaultValue && type === 'datetime-local' ? new Date(defaultValue).toLocaleString('sv-SE') : defaultValue
}
disabled={disabled}
id={id}
name={name}
onBlur={onBlur}
pattern={pattern}
placeholder={placeholder ?? label}
ref={inputRef}
required={required}
type={type}
value={value}
/>
{showValidity && <i className={cn(styles.statusIcon, classNames?.statusIcon)} />}
{/**
* <label> comes after <input> to be able to use next sibling CSS selector
* (see public input.module.scss). Use `flexbox` and `order` if <label> needs to be
* displayed before <input> (see admin input.module.scss)
*/}
{label && (
<label className={cn(styles.label, classNames?.label)} htmlFor={id}>
{icon && (styles.labelIcon || classNames?.labelIcon) && (
<div className={cn(styles.labelIcon, classNames?.labelIcon)} style={{ maskImage: icon }} />
)}
{label}
</label>
)}
</div>
)
}
return Input
}