{children
? children({ index: i, readOnly: i < readOnlyLen, placeholder: i >= min ? 'optional' : undefined })
: = min ? 'optional' : undefined} />}
{options.length - 1 === i && options.length !== max
? fieldArrayHelpers.push(emptyItem)} />
// filler div for col alignment across rows
: }
{options.length - 1 === i &&
<>
{hint && {hint}}
{form.touched[name] && typeof form.errors[name] === 'string' &&
{form.errors[name]}
}
>}
))}
>
)
}}
)
}
export function Checkbox ({
children, label, groupClassName, type = 'checkbox',
hiddenLabel, extra, handleChange, inline, disabled, ...props
}) {
// React treats radios and checkbox inputs differently other input types, select, and textarea.
// Formik does this too! When you specify `type` to useField(), it will
// return the correct bag of props for you
const [field, meta, helpers] = useField({ ...props, type })
return (
{hiddenLabel && {label}}
{
field.onChange(e)
handleChange && handleChange(e.target.checked, helpers.setValue)
}}
/>
{label}
{extra &&
{extra}
}
)
}
export function CheckboxGroup ({ label, groupClassName, children, ...props }) {
const [, meta] = useField(props)
return (
{children}
{/* force the feedback to display with d-block */}
{meta.touched && meta.error}
)
}
const StorageKeyPrefixContext = createContext()
export function Form ({
initial, validate, schema, onSubmit, children, initialError, validateImmediately,
storageKeyPrefix, validateOnChange = true, requireSession, innerRef, enableReinitialize,
...props
}) {
const toaster = useToast()
const initialErrorToasted = useRef(false)
const { me } = useMe()
useEffect(() => {
if (initialError && !initialErrorToasted.current) {
toaster.danger('form error: ' + initialError.message || initialError.toString?.())
initialErrorToasted.current = true
}
}, [])
const clearLocalStorage = useCallback((values) => {
Object.keys(values).forEach(v => {
window.localStorage.removeItem(storageKeyPrefix + '-' + v)
if (Array.isArray(values[v])) {
values[v].forEach(
(iv, i) => {
Object.keys(iv).forEach(k => {
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}].${k}`)
})
window.localStorage.removeItem(`${storageKeyPrefix}-${v}[${i}]`)
})
}
})
}, [storageKeyPrefix])
const onSubmitInner = useCallback(async ({ amount, ...values }, ...args) => {
const variables = { amount, ...values }
if (requireSession && !me) {
throw new SessionRequiredError()
}
try {
if (onSubmit) {
await onSubmit(variables, ...args)
}
} catch (err) {
console.log(err.message, err)
toaster.danger(err.message ?? err.toString?.())
return
}
if (!storageKeyPrefix) return
clearLocalStorage(values)
}, [me, onSubmit, clearLocalStorage, storageKeyPrefix])
return (
{children}
)
}
export function Select ({ label, items, info, groupClassName, onChange, noForm, overrideValue, hint, ...props }) {
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
const formik = noForm ? null : useFormikContext()
const invalid = meta.touched && meta.error
useEffect(() => {
if (overrideValue) {
helpers.setValue(overrideValue)
}
}, [overrideValue])
return (
{
if (field?.onChange) {
field.onChange(e)
}
if (onChange) {
onChange(formik, e)
}
}}
isInvalid={invalid}
>
{items.map(item => {
if (item && typeof item === 'object') {
return (
)
} else {
return
}
})}
{info && {info}}
{meta.touched && meta.error}
{hint &&
{hint}
}
)
}
export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
const formik = noForm ? null : useFormikContext()
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
const [,, toHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: toName })
const { minDate, maxDate } = props
const [[innerFrom, innerTo], setRange] = useState(whenRange(when, from, to))
useEffect(() => {
setRange(whenRange(when, from, to))
if (!noForm) {
fromHelpers.setValue(from)
toHelpers.setValue(to)
}
}, [when, from, to])
const dateFormat = useMemo(() => {
const now = new Date(2013, 11, 31)
let str = now.toLocaleDateString()
str = str.replace('31', 'dd')
str = str.replace('12', 'MM')
str = str.replace('2013', 'yy')
return str
}, [])
const innerOnChange = ([from, to], e) => {
if (from) {
from = new Date(new Date(from).setHours(0, 0, 0, 0))
}
if (to) {
to = new Date(new Date(to).setHours(23, 59, 59, 999))
}
setRange([from, to])
if (!noForm) {
fromHelpers.setValue(from)
toHelpers.setValue(to)
}
if (!from || !to) return
onChange?.(formik, [from, to], e)
}
const onChangeRawHandler = (e) => {
// raw user data can be incomplete while typing, so quietly bail on exceptions
try {
const dateStrings = e.target.value.split('-', 2)
const dates = dateStrings.map(s => new Date(s))
let [from, to] = dates
if (from) {
from = new Date(from.setHours(0, 0, 0, 0))
if (minDate) from = new Date(Math.max(from.getTime(), minDate.getTime()))
try {
if (to) {
to = new Date(to.setHours(23, 59, 59, 999))
if (maxDate) to = new Date(Math.min(to.getTime(), maxDate.getTime()))
}
// if end date isn't valid, set it to the start date
if (!(to instanceof Date && !isNaN(to)) || to < from) to = new Date(from.setHours(23, 59, 59, 999))
} catch {
to = new Date(from.setHours(23, 59, 59, 999))
}
innerOnChange([from, to], e)
}
} catch { }
}
return (
)
}
export function DateTimeInput ({ label, groupClassName, name, ...props }) {
const [, meta] = useField({ ...props, name })
return (
{meta.error}
)
}
function DateTimePicker ({ name, className, ...props }) {
const [field, , helpers] = useField({ ...props, name })
return (
{
helpers.setValue(val)
}}
/>
)
}
function Client (Component) {
return ({ initialValue, ...props }) => {
// This component can be used for Formik fields
// where the initial value is not available on first render.
// Example: value is stored in localStorage which is fetched
// after first render using an useEffect hook.
const [,, helpers] = props.noForm ? [{}, {}, {}] : useField(props)
useEffect(() => {
initialValue && helpers.setValue(initialValue)
}, [initialValue])
return
}
}
function PasswordHider ({ onClick, showPass }) {
return (
{!showPass
?
: }
)
}
function QrPassword ({ value }) {
const showModal = useShowModal()
const toaster = useToast()
const showQr = useCallback(() => {
showModal(close => (
Import this passphrase into another device by navigating to device sync settings and scanning this QR code
)
})
}}
>
)
}
export function PasswordInput ({ newPass, qr, copy, readOnly, append, value: initialValue, ...props }) {
const [showPass, setShowPass] = useState(false)
const [value, setValue] = useState(initialValue)
const [field,, helpers] = props.noForm ? [{ value }, {}, { setValue }] : useField(props)
const Append = useMemo(() => {
return (
<>
setShowPass(!showPass)} />
{copy && (
)}
{qr && (readOnly
?
: helpers.setValue(v)}
/>)}
{append}
>
)
}, [showPass, copy, field?.value, helpers.setValue, qr, readOnly, append])
const style = props.style ? { ...props.style } : {}
if (props.as === 'textarea') {
if (!showPass) {
style.WebkitTextSecurity = 'disc'
} else {
if (style.WebkitTextSecurity) delete style.WebkitTextSecurity
}
}
return (
{Append}
)
: undefined}
/>
)
}
export function MultiInput ({
name, label, groupClassName, length = 4, charLength = 1, upperCase, showSequence,
onChange, autoFocus, hideError, inputType = 'text',
...props
}) {
const [inputs, setInputs] = useState(new Array(length).fill(''))
const inputRefs = useRef(new Array(length).fill(null))
const [, meta, helpers] = useField({ name })
useEffect(() => {
autoFocus && inputRefs.current[0].focus() // focus the first input if autoFocus is true
}, [autoFocus])
const updateInputs = useCallback((newInputs) => {
setInputs(newInputs)
const combinedValue = newInputs.join('') // join the inputs to get the value
helpers.setValue(combinedValue) // set the value to the formik field
onChange?.(combinedValue)
}, [onChange, helpers])
const handleChange = useCallback((formik, e, index) => { // formik is not used but it's required to get the value
const value = e.target.value.slice(-charLength)
const processedValue = upperCase ? value.toUpperCase() : value // convert the input to uppercase if upperCase is tru
const newInputs = [...inputs]
newInputs[index] = processedValue
updateInputs(newInputs)
// focus the next input if the current input is filled
if (processedValue.length === charLength && index < length - 1) {
inputRefs.current[index + 1].focus()
}
}, [inputs, charLength, upperCase, onChange, length])
const handlePaste = useCallback((e) => {
e.preventDefault()
const pastedValues = e.clipboardData.getData('text').slice(0, length)
const processedValues = upperCase ? pastedValues.toUpperCase() : pastedValues
const chars = processedValues.split('')
const newInputs = [...inputs]
chars.forEach((char, i) => {
newInputs[i] = char.slice(0, charLength)
})
updateInputs(newInputs)
inputRefs.current[length - 1]?.focus() // simulating the paste by focusing the last input
}, [inputs, length, charLength, upperCase, updateInputs])
const handleKeyDown = useCallback((e, index) => {
switch (e.key) {
case 'Backspace': {
e.preventDefault()
const newInputs = [...inputs]
// if current input is empty move focus to the previous input else clear the current input
const targetIndex = inputs[index] === '' && index > 0 ? index - 1 : index
newInputs[targetIndex] = ''
updateInputs(newInputs)
inputRefs.current[targetIndex]?.focus()
break
}
case 'ArrowLeft': {
if (index > 0) { // focus the previous input if it's not the first input
e.preventDefault()
inputRefs.current[index - 1]?.focus()
}
break
}
case 'ArrowRight': {
if (index < length - 1) { // focus the next input if it's not the last input
e.preventDefault()
inputRefs.current[index + 1]?.focus()
}
break
}
}
}, [inputs, length, updateInputs])
return (
{inputs.map((value, index) => (
{ inputRefs.current[index] = el }}
onChange={(formik, e) => handleChange(formik, e, index)}
onKeyDown={e => handleKeyDown(e, index)}
onPaste={e => handlePaste(e, index)}
style={{
textAlign: 'center',
maxWidth: `${charLength * 44}px` // adjusts the max width of the input based on the charLength
}}
prepend={showSequence && {index + 1}} // show the index of the input
hideError
{...props}
/>
))}
{hideError && meta.touched && meta.error && ( // custom error message is showed if hideError is true
{meta.error}
)}