145 lines
3.8 KiB
TypeScript
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
|
|
}
|