stacker.news/components/form.js

286 lines
7.9 KiB
JavaScript
Raw Normal View History

2021-04-14 00:57:32 +00:00
import Button from 'react-bootstrap/Button'
import InputGroup from 'react-bootstrap/InputGroup'
import BootstrapForm from 'react-bootstrap/Form'
import Alert from 'react-bootstrap/Alert'
import { Formik, Form as FormikForm, useFormikContext, useField } from 'formik'
2022-01-07 18:28:23 +00:00
import React, { useEffect, useRef, useState } from 'react'
2021-05-13 13:28:38 +00:00
import copy from 'clipboard-copy'
import Thumb from '../svgs/thumb-up-fill.svg'
2021-07-01 23:51:58 +00:00
import { Nav } from 'react-bootstrap'
import Markdown from '../svgs/markdown-line.svg'
import styles from './form.module.css'
import Text from '../components/text'
2021-04-14 00:57:32 +00:00
2021-09-10 18:55:36 +00:00
export function SubmitButton ({ children, variant, value, onClick, ...props }) {
const { isSubmitting, setFieldValue } = useFormikContext()
2021-04-14 00:57:32 +00:00
return (
<Button
variant={variant || 'main'}
type='submit'
disabled={isSubmitting}
2021-09-10 18:55:36 +00:00
onClick={value
? e => {
setFieldValue('submit', value)
onClick && onClick(e)
}
: onClick}
2021-04-14 00:57:32 +00:00
{...props}
>
{children}
</Button>
)
}
2021-05-13 13:28:38 +00:00
export function CopyInput (props) {
const [copied, setCopied] = useState(false)
const handleClick = () => {
copy(props.placeholder)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
return (
<Input
onClick={handleClick}
2021-06-02 23:15:28 +00:00
append={
<Button
size={props.size}
onClick={handleClick}
>
{copied ? <Thumb width={18} height={18} /> : 'copy'}
</Button>
}
2021-05-13 13:28:38 +00:00
{...props}
/>
)
}
export function InputSkeleton ({ label }) {
return (
<BootstrapForm.Group>
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
2021-11-04 18:22:03 +00:00
<div className='form-control clouds' />
2021-05-13 13:28:38 +00:00
</BootstrapForm.Group>
)
}
2021-07-01 23:51:58 +00:00
export function MarkdownInput ({ label, groupClassName, ...props }) {
const [tab, setTab] = useState('write')
const [, meta] = useField(props)
useEffect(() => {
!meta.value && setTab('write')
}, [meta.value])
return (
<FormGroup label={label} className={groupClassName}>
<div className={`${styles.markdownInput} ${tab === 'write' ? styles.noTopLeftRadius : ''}`}>
<Nav variant='tabs' defaultActiveKey='write' activeKey={tab} onSelect={tab => setTab(tab)}>
<Nav.Item>
<Nav.Link eventKey='write'>write</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey='preview' disabled={!meta.value}>preview</Nav.Link>
</Nav.Item>
<a
className='ml-auto text-muted d-flex align-items-center'
href='https://guides.github.com/features/mastering-markdown/' target='_blank' rel='noreferrer'
>
<Markdown width={18} height={18} />
</a>
</Nav>
<div className={tab !== 'write' ? 'd-none' : ''}>
<InputInner
{...props}
/>
</div>
2021-08-10 22:59:06 +00:00
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
<div className={`${styles.text} form-control`}>
2021-07-01 23:51:58 +00:00
<Text>{meta.value}</Text>
</div>
</div>
</div>
</FormGroup>
)
}
function FormGroup ({ className, label, children }) {
2021-04-14 00:57:32 +00:00
return (
2021-07-01 23:51:58 +00:00
<BootstrapForm.Group className={className}>
2021-04-14 00:57:32 +00:00
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
2021-07-01 23:51:58 +00:00
{children}
</BootstrapForm.Group>
)
}
2022-01-07 18:28:23 +00:00
function InputInner ({
prepend, append, hint, showValid, onChange, overrideValue,
innerRef, storageKeyPrefix, ...props
}) {
2021-08-22 15:25:17 +00:00
const [field, meta, helpers] = props.readOnly ? [{}, {}, {}] : useField(props)
2021-08-19 21:13:33 +00:00
const formik = props.readOnly ? null : useFormikContext()
2021-08-22 15:25:17 +00:00
2022-01-07 18:28:23 +00:00
const storageKey = storageKeyPrefix ? storageKeyPrefix + '-' + props.name : undefined
2021-08-22 15:25:17 +00:00
useEffect(() => {
if (overrideValue) {
helpers.setValue(overrideValue)
2022-01-07 18:28:23 +00:00
if (storageKey) {
localStorage.setItem(storageKey, overrideValue)
}
} else if (storageKey) {
const draft = localStorage.getItem(storageKey)
if (draft) {
// for some reason we have to turn off validation to get formik to
// not assume this is invalid
helpers.setValue(draft, false)
}
2021-08-22 15:25:17 +00:00
}
}, [overrideValue])
2021-07-01 23:51:58 +00:00
return (
<>
2021-04-14 00:57:32 +00:00
<InputGroup hasValidation>
{prepend && (
<InputGroup.Prepend>
2021-05-13 13:28:38 +00:00
{prepend}
2021-04-14 00:57:32 +00:00
</InputGroup.Prepend>
)}
<BootstrapForm.Control
2021-08-19 21:13:33 +00:00
onKeyDown={(e) => {
if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
formik?.submitForm()
}
}}
2021-09-10 18:55:36 +00:00
ref={innerRef}
2021-04-14 00:57:32 +00:00
{...field} {...props}
2021-08-22 15:25:17 +00:00
onChange={(e) => {
field.onChange(e)
2022-01-07 18:28:23 +00:00
if (storageKey) {
localStorage.setItem(storageKey, e.target.value)
}
2021-08-22 15:25:17 +00:00
if (onChange) {
onChange(formik, e)
}
}}
2021-04-14 00:57:32 +00:00
isInvalid={meta.touched && meta.error}
2021-05-21 22:32:21 +00:00
isValid={showValid && meta.touched && !meta.error}
2021-04-14 00:57:32 +00:00
/>
{append && (
<InputGroup.Append>
2021-05-13 13:28:38 +00:00
{append}
2021-04-14 00:57:32 +00:00
</InputGroup.Append>
)}
<BootstrapForm.Control.Feedback type='invalid'>
{meta.touched && meta.error}
</BootstrapForm.Control.Feedback>
</InputGroup>
2021-05-25 00:08:56 +00:00
{hint && (
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>
)}
2021-07-01 23:51:58 +00:00
</>
)
}
export function Input ({ label, groupClassName, ...props }) {
return (
<FormGroup label={label} className={groupClassName}>
<InputInner {...props} />
</FormGroup>
2021-04-14 00:57:32 +00:00
)
}
2021-12-16 17:27:12 +00:00
export function Checkbox ({ children, label, extra, handleChange, inline, ...props }) {
2021-09-12 16:55:38 +00:00
// 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
2021-12-16 17:27:12 +00:00
const [field] = useField({ ...props, type: 'checkbox' })
2021-09-12 16:55:38 +00:00
return (
2021-12-16 17:27:12 +00:00
<BootstrapForm.Check
custom
id={props.id || props.name}
inline={inline}
>
<BootstrapForm.Check.Input
{...field} {...props} type='checkbox' onChange={(e) => {
field.onChange(e)
handleChange && handleChange(e.target.checked)
}}
/>
<BootstrapForm.Check.Label className='d-flex'>
<div className='flex-grow-1'>{label}</div>
{extra &&
<div className={styles.checkboxExtra}>
{extra}
</div>}
</BootstrapForm.Check.Label>
</BootstrapForm.Check>
2021-09-12 16:55:38 +00:00
)
}
2021-04-14 00:57:32 +00:00
export function Form ({
2022-01-07 18:28:23 +00:00
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, ...props
2021-04-14 00:57:32 +00:00
}) {
2021-05-13 21:19:51 +00:00
const [error, setError] = useState(initialError)
2021-04-14 00:57:32 +00:00
return (
<Formik
initialValues={initial}
validationSchema={schema}
2021-05-25 00:08:56 +00:00
initialTouched={validateImmediately && initial}
validateOnBlur={false}
2021-04-24 21:05:07 +00:00
onSubmit={async (...args) =>
2022-01-07 18:28:23 +00:00
onSubmit && onSubmit(...args).then(() => {
if (!storageKeyPrefix) return
Object.keys(...args).forEach(v =>
localStorage.removeItem(storageKeyPrefix + '-' + v))
}).catch(e => setError(e.message || e))}
2021-04-14 00:57:32 +00:00
>
<FormikForm {...props} noValidate>
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
2022-01-07 18:28:23 +00:00
{storageKeyPrefix
? React.Children.map(children, (child) => {
if (child) {
return React.cloneElement(child, {
storageKeyPrefix
})
}
})
: children}
2021-04-14 00:57:32 +00:00
</FormikForm>
</Formik>
)
}
2021-04-24 21:05:07 +00:00
export function SyncForm ({
initial, schema, children, action, ...props
}) {
const ref = useRef(null)
return (
<Formik
initialValues={initial}
validationSchema={schema}
validateOnBlur={false}
onSubmit={() => ref.current.submit()}
>
{props => (
<form
ref={ref}
onSubmit={props.handleSubmit}
onReset={props.handleReset}
action={action}
method='POST'
noValidate
>
{children}
</form>
)}
</Formik>
)
}