247
packages/pl-fe/src/features/forms/index.tsx
Normal file
247
packages/pl-fe/src/features/forms/index.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import clsx from 'clsx';
|
||||
import MultiselectReactDropdown from 'multiselect-react-dropdown';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Icon, Select } from '../../components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
selectPlaceholder: { id: 'select.placeholder', defaultMessage: 'Select' },
|
||||
selectNoOptions: { id: 'select.no_options', defaultMessage: 'No options available' },
|
||||
});
|
||||
|
||||
interface IInputContainer {
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
required?: boolean;
|
||||
type?: string;
|
||||
extraClass?: string;
|
||||
error?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const InputContainer: React.FC<IInputContainer> = (props) => {
|
||||
const containerClass = clsx('input', {
|
||||
'with_label': props.label,
|
||||
'required': props.required,
|
||||
'boolean': props.type === 'checkbox',
|
||||
'field_with_errors': props.error,
|
||||
}, props.extraClass);
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
{props.children}
|
||||
{props.hint && <span className='hint'>{props.hint}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ILabelInputContainer {
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const LabelInputContainer: React.FC<ILabelInputContainer> = ({ label, hint, children }) => {
|
||||
const [id] = useState(uuidv4());
|
||||
const childrenWithProps = React.Children.map(children, child => (
|
||||
// @ts-ignore: not sure how to get the right type here
|
||||
React.cloneElement(child, { id, key: id })
|
||||
));
|
||||
|
||||
return (
|
||||
<div className='label_input'>
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<div className='label_input__wrapper'>
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
{hint && <span className='hint'>{hint}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ILabelInput {
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
const LabelInput: React.FC<ILabelInput> = ({ label, ...props }) => (
|
||||
<LabelInputContainer label={label}>
|
||||
<input {...props} />
|
||||
</LabelInputContainer>
|
||||
);
|
||||
|
||||
interface ILabelTextarea {
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
const LabelTextarea: React.FC<ILabelTextarea> = ({ label, ...props }) => (
|
||||
<LabelInputContainer label={label}>
|
||||
<textarea {...props} />
|
||||
</LabelInputContainer>
|
||||
);
|
||||
|
||||
interface ISimpleInput {
|
||||
type: string;
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
error?: boolean;
|
||||
onChange?: React.ChangeEventHandler;
|
||||
min?: number;
|
||||
max?: number;
|
||||
pattern?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
value?: string | number;
|
||||
autoComplete?: string;
|
||||
autoCorrect?: string;
|
||||
autoCapitalize?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const SimpleInput: React.FC<ISimpleInput> = (props) => {
|
||||
const { hint, error, ...rest } = props;
|
||||
const Input = props.label ? LabelInput : 'input';
|
||||
|
||||
return (
|
||||
<InputContainer {...props}>
|
||||
<Input {...rest} />
|
||||
</InputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICheckbox {
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
name?: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<ICheckbox> = (props) => (
|
||||
<SimpleInput type='checkbox' {...props} />
|
||||
);
|
||||
|
||||
interface ISelectDropdown {
|
||||
className?: string;
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
items: Record<string, string>;
|
||||
defaultValue?: string;
|
||||
onChange?: React.ChangeEventHandler;
|
||||
}
|
||||
|
||||
const SelectDropdown: React.FC<ISelectDropdown> = (props) => {
|
||||
const { label, hint, items, ...rest } = props;
|
||||
|
||||
const optionElems = Object.keys(items).map(item => (
|
||||
<option key={item} value={item}>{items[item]}</option>
|
||||
));
|
||||
|
||||
// @ts-ignore
|
||||
const selectElem = <Select {...rest}>{optionElems}</Select>;
|
||||
|
||||
return label ? (
|
||||
<LabelInputContainer label={label} hint={hint}>{selectElem}</LabelInputContainer>
|
||||
) : selectElem;
|
||||
};
|
||||
|
||||
interface IMultiselect {
|
||||
className?: string;
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
items: Record<string, string>;
|
||||
value?: string[];
|
||||
onChange?: ((values: string[]) => void);
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Mutliselect: React.FC<IMultiselect> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { label, hint, items, value, onChange, disabled } = props;
|
||||
|
||||
const options = useMemo(() => Object.entries(items).map(([key, value]) => ({ key, value })), [items]);
|
||||
const selectedValues = value?.map(key => options.find(option => option.key === key)).filter(value => value);
|
||||
|
||||
const handleChange = (values: Record<'key' | 'value', string>[]) => {
|
||||
onChange?.(values.map(({ key }) => key));
|
||||
};
|
||||
|
||||
const selectElem = (
|
||||
<MultiselectReactDropdown
|
||||
className='plfe-multiselect'
|
||||
options={options}
|
||||
selectedValues={selectedValues}
|
||||
onSelect={handleChange}
|
||||
onRemove={handleChange}
|
||||
displayValue='value'
|
||||
disable={disabled}
|
||||
customCloseIcon={<Icon className='ml-1 h-4 w-4 hover:cursor-pointer' src={require('@tabler/icons/outline/circle-x.svg')} />}
|
||||
placeholder={intl.formatMessage(messages.selectPlaceholder)}
|
||||
emptyRecordMsg={intl.formatMessage(messages.selectNoOptions)}
|
||||
/>
|
||||
);
|
||||
|
||||
return label ? (
|
||||
<LabelInputContainer label={label} hint={hint}>{selectElem}</LabelInputContainer>
|
||||
) : selectElem;
|
||||
};
|
||||
|
||||
interface ITextInput {
|
||||
name?: string;
|
||||
onChange?: React.ChangeEventHandler;
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
autoComplete?: string;
|
||||
autoCorrect?: string;
|
||||
autoCapitalize?: string;
|
||||
pattern?: string;
|
||||
error?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const TextInput: React.FC<ITextInput> = props => (
|
||||
<SimpleInput type='text' {...props} />
|
||||
);
|
||||
|
||||
const FileChooser : React.FC = (props) => (
|
||||
<SimpleInput type='file' {...props} />
|
||||
);
|
||||
|
||||
FileChooser.defaultProps = {
|
||||
accept: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||
};
|
||||
|
||||
interface IFileChooserLogo {
|
||||
label?: React.ReactNode;
|
||||
hint?: React.ReactNode;
|
||||
name?: string;
|
||||
accept?: string[];
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const FileChooserLogo: React.FC<IFileChooserLogo> = props => (
|
||||
<SimpleInput type='file' {...props} />
|
||||
);
|
||||
|
||||
FileChooserLogo.defaultProps = {
|
||||
accept: ['image/svg', 'image/png'],
|
||||
};
|
||||
|
||||
export {
|
||||
InputContainer,
|
||||
LabelInputContainer,
|
||||
LabelInput,
|
||||
LabelTextarea,
|
||||
SimpleInput,
|
||||
Checkbox,
|
||||
SelectDropdown,
|
||||
Mutliselect,
|
||||
TextInput,
|
||||
FileChooser,
|
||||
FileChooserLogo,
|
||||
};
|
||||
Reference in New Issue
Block a user