widgets/client/form/components/Field.tsx
import Datetime from '@nateradebaugh/react-datetime';
import * as React from 'react';
import uploadHandler from '../../uploadHandler';
import {
COMPANY_BUSINESS_TYPES,
DEFAULT_COMPANY_INDUSTRY_TYPES,
COUNTRIES
} from '../constants';
import { FieldValue, IField, IFieldError, ILocationOption } from '../types';
import MSFmultiSelect from '../multipleSelectScript';
import { __ } from '../../utils';
import Map from './Map';
import Marker from './Marker';
type Props = {
field: IField;
error?: IFieldError;
value?: FieldValue;
currentLocation?: ILocationOption;
color?: string;
mapScriptLoaded?: boolean;
onChange: (params: {
fieldId: string;
value: FieldValue;
associatedFieldId?: string;
groupId?: string;
}) => void;
};
type State = {
dateValue?: Date | string;
dateTimeValue: Date | string;
isAttachingFile?: boolean;
multipleSelectValues?: string[];
isMapDraggable: boolean;
currentLocation: ILocationOption;
};
export default class Field extends React.Component<Props, State> {
static renderSelect(options: string[] = [], attrs: any = {}) {
return (
<select
{...attrs}
className="form-control"
id={attrs.multiple ? `_id${attrs.id}` : ''}>
{options.map((option, index) => (
<option key={index} value={option} selected={true}>
{option}
</option>
))}
</select>
);
}
static renderInput(attrs: any) {
return <input {...attrs} className="form-control" />;
}
static renderTextarea(attrs: any) {
return <textarea {...attrs} className="form-control" />;
}
static renderCheckboxes(
name: string,
options: string[],
id: string,
onChange: () => void,
value?: string
) {
let values: string[] = [];
if (value) {
values = value.split(',,');
}
return (
<div className="check-control">
{options.map((option, index) => {
const checked = values.indexOf(option) > -1 ? true : false;
return (
<div key={index}>
<label>
{Field.renderInput({
type: 'checkbox',
'data-option': option,
name,
id,
onChange,
checked
})}
{option}
</label>
</div>
);
})}
</div>
);
}
static renderRadioButtons(
name: string,
options: string[],
id: string,
onChange: (e: React.FormEvent<HTMLInputElement>) => void,
value?: string
) {
const selectedIndex = options.indexOf(value || '');
return (
<div>
{options.map((option, index) => (
<div key={index}>
{Field.renderInput({
type: 'radio',
'data-option': option,
name,
id,
onChange,
checked: index === selectedIndex
})}
<span>{option}</span>
</div>
))}
</div>
);
}
constructor(props: Props) {
super(props);
let isMapDraggable = true;
const locationOptions = props.field.locationOptions || [];
if (locationOptions.length > 0) {
isMapDraggable = false;
}
this.state = {
dateValue: '',
dateTimeValue: '',
multipleSelectValues: [],
isMapDraggable,
currentLocation: props.currentLocation || { lat: 0.0, lng: 0.0 }
};
}
componentDidMount() {
const { field } = this.props;
if (field.type === 'multiSelect' || field.type === 'industry') {
const multiSelects = Array.from(
document.querySelectorAll(`#_id${field._id}`)
);
const onChange = (checked: boolean, value: string) => {
let multipleSelectValues = this.state.multipleSelectValues || [];
if (multipleSelectValues) {
if (checked) {
multipleSelectValues.push(value);
} else {
multipleSelectValues = multipleSelectValues.filter(
e => e === value
);
}
this.onChange(multipleSelectValues.toString());
}
this.setState({ multipleSelectValues });
};
const afterSelectAll = (_checked: boolean, values: string[]) => {
this.setState({ multipleSelectValues: values });
};
multiSelects.map(query => {
const select = new MSFmultiSelect(query, {
theme: 'theme2',
selectAll: true,
searchBox: true,
onChange,
afterSelectAll
});
const options =
field.type === 'industry'
? DEFAULT_COMPANY_INDUSTRY_TYPES
: field.options || [];
const selectedValues: any = this.props.value || [];
select.loadSource(
options.map(e => {
const selected = selectedValues.indexOf(e) > -1 ? true : false;
return { caption: e, value: e, selected };
})
);
return select;
});
}
}
onChange = (value: FieldValue) => {
const { onChange, field } = this.props;
onChange({
fieldId: field._id,
value,
associatedFieldId: field.associatedFieldId,
groupId: field.groupId
});
};
onInputChange = (e: React.FormEvent<HTMLInputElement>) => {
this.onChange(e.currentTarget.value);
};
handleFileInput = (e: React.FormEvent<HTMLInputElement>) => {
const files = e.currentTarget.files;
const self = this;
const attachments: any[] = [];
if (files && files.length > 0) {
for (const file of Array.from(files)) {
uploadHandler({
file,
beforeUpload() {
self.setState({ isAttachingFile: true });
},
// upload to server
afterUpload({ response, fileInfo }: any) {
const attachment = { url: response, ...fileInfo };
attachments.push(attachment);
self.setState({ isAttachingFile: false });
},
onError: message => {
alert(message);
self.setState({ isAttachingFile: false });
}
});
}
}
self.onChange(attachments);
};
onDateChange = (date?: Date | string) => {
this.setState({ dateValue: date || '' });
this.onChange(date || '');
};
onDateTimeChange = (date?: Date | string) => {
this.setState({ dateTimeValue: date || '' });
this.onChange(date || '');
};
onRadioButtonsChange = (e: React.FormEvent<HTMLInputElement>) => {
this.onChange(e.currentTarget.getAttribute('data-option') || '');
};
onCheckboxesChange = () => {
const values: string[] = [];
const { field } = this.props;
const elements = document.getElementsByName(field._id);
// tslint:disable-next-line
for (let i = 0; i < elements.length; i++) {
const checkbox: any = elements[i];
if (checkbox.checked) {
values.push(checkbox.dataset.option);
}
}
this.onChange(values.join(',,'));
};
onTextAreaChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
this.onChange(e.currentTarget.value);
};
onSelectChange = (e: React.FormEvent<HTMLSelectElement>) => {
this.onChange(e.currentTarget.value);
};
onMultpleSelectChange = (e: React.FormEvent<HTMLSelectElement>) => {
const selectedValue = e.currentTarget.value;
const { multipleSelectValues } = this.state;
if (multipleSelectValues) {
if (
multipleSelectValues.filter(value => value === selectedValue).length ===
0
) {
multipleSelectValues.push(selectedValue);
}
this.onChange(multipleSelectValues);
}
this.setState({ multipleSelectValues });
};
onLocationChange = (option: ILocationOption) => {
this.onChange(option || '');
};
renderDatepicker(id: string) {
let defaultValue = new Date();
if (this.props.value) {
defaultValue = new Date(String(this.props.value));
}
return (
<Datetime
inputProps={{ id }}
value={this.state.dateValue}
viewDate={new Date()}
defaultValue={defaultValue}
onChange={this.onDateChange}
dateFormat="YYYY/MM/DD"
timeFormat={false}
/>
);
}
renderHtml(content: string, id: string) {
return (
<div
id={id}
dangerouslySetInnerHTML={{
__html: content
}}
/>
);
}
renderDateTimepicker(id: string) {
let defaultValue = new Date();
if (this.props.value) {
defaultValue = new Date(String(this.props.value));
}
return (
<Datetime
inputProps={{ id }}
value={this.state.dateTimeValue}
viewDate={new Date()}
defaultValue={defaultValue}
onChange={this.onDateTimeChange}
timeFormat="HH:mm"
dateFormat="YYYY/MM/DD"
/>
);
}
renderMap(field: IField, selectedValue?: FieldValue) {
const locationOptions: ILocationOption[] = field.locationOptions || [];
let { currentLocation } = this.state;
let selectedOption: ILocationOption = { lat: 0, lng: 0 };
if (selectedValue) {
selectedOption = selectedValue as ILocationOption;
currentLocation = { lat: selectedOption.lat, lng: selectedOption.lng };
}
return (
<div style={{ height: '250px', width: '100%' }}>
{this.props.mapScriptLoaded && (
<Map
center={
new google.maps.LatLng(currentLocation.lat, currentLocation.lng)
}
controlSize={25}
streetViewControl={false}
zoom={4}
style={{ width: '100%', height: '250px' }}>
{locationOptions.length > 0 ? (
locationOptions.map((option, index) => (
<Marker
color={
option.lat === selectedOption.lat &&
option.lng === selectedOption.lng
? 'red'
: this.props.color
}
key={index}
position={new google.maps.LatLng(option.lat, option.lng)}
content={option.description}
draggable={false}
onChange={this.onLocationChange}
/>
))
) : (
<Marker
color={this.props.color}
position={
new google.maps.LatLng(
currentLocation.lat,
currentLocation.lng
)
}
content={__('Select your location')}
draggable={true}
onChange={this.onLocationChange}
/>
)}
</Map>
)}
</div>
);
}
renderProduct(field: IField) {
const { products = [] } = field;
return (
<select
onChange={this.onSelectChange}
className="form-control"
id={field._id}>
<option>-</option>
{products.map(({ _id, name, unitPrice }) => (
<option key={_id} value={_id}>
{`${name} - ${unitPrice.toLocaleString()}`}
</option>
))}
</select>
);
}
renderControl() {
const { field, value } = this.props;
const { options = [], validation = 'text' } = field;
const name = field._id;
if (validation === 'date') {
return this.renderDatepicker(field._id);
}
if (validation === 'datetime') {
return this.renderDateTimepicker(field._id);
}
switch (field.type) {
case 'select':
return Field.renderSelect(options, {
onChange: this.onSelectChange,
id: field._id,
value: String(value)
});
case 'multiSelect':
return Field.renderSelect(options, {
value: this.state.multipleSelectValues,
onChange: this.onMultpleSelectChange,
id: field._id,
multiple: true
});
case 'pronoun':
return Field.renderSelect(['Male', 'Female', 'Not applicable'], {
onChange: this.onSelectChange,
id: field._id,
value: String(value)
});
case 'businessType':
return Field.renderSelect(COMPANY_BUSINESS_TYPES, {
onChange: this.onSelectChange,
id: field._id,
value: String(value)
});
case 'location':
return Field.renderSelect(COUNTRIES, {
onChange: this.onSelectChange,
id: field._id,
value: String(value)
});
case 'industry':
return Field.renderSelect(DEFAULT_COMPANY_INDUSTRY_TYPES, {
value: this.state.multipleSelectValues,
onChange: this.onMultpleSelectChange,
id: field._id,
multiple: true
});
case 'check':
const values: any = value;
return Field.renderCheckboxes(
name,
options,
field._id,
this.onCheckboxesChange,
values
);
case 'radio':
return Field.renderRadioButtons(
name,
options,
field._id,
this.onRadioButtonsChange,
String(value)
);
case 'isSubscribed':
return Field.renderRadioButtons(
name,
['Yes', 'No'],
field._id,
this.onRadioButtonsChange,
String(value)
);
case 'company_isSubscribed':
return Field.renderRadioButtons(
name,
['Yes', 'No'],
field._id,
this.onRadioButtonsChange,
String(value)
);
case 'hasAuthority':
return Field.renderRadioButtons(
name,
['Yes', 'No'],
field._id,
this.onRadioButtonsChange,
String(value)
);
case 'file':
return Field.renderInput({
onChange: this.handleFileInput,
type: 'file',
id: field._id,
multiple: true
});
case 'avatar':
return Field.renderInput({
onChange: this.handleFileInput,
type: 'file',
id: field._id
});
case 'company_avatar':
return Field.renderInput({
onChange: this.handleFileInput,
type: 'file',
id: field._id
});
case 'textarea':
return Field.renderTextarea({
onChange: this.onTextAreaChange,
id: field._id,
value
});
case 'description':
return Field.renderTextarea({
onChange: this.onTextAreaChange,
id: field._id,
value
});
case 'company_description':
return Field.renderTextarea({
onChange: this.onTextAreaChange,
id: field._id,
value
});
case 'birthDate':
return this.renderDatepicker(field._id);
case 'html':
return this.renderHtml(field.content || '', field._id);
case 'map':
return this.renderMap(field, value);
case 'productCategory':
return this.renderProduct(field);
default:
return Field.renderInput({
onChange: this.onInputChange,
type: validation,
id: field._id,
value
});
}
}
render() {
const { field, error } = this.props;
const { isAttachingFile } = this.state;
const fieldStyle = () => {
if (field.column) {
return {
width: `${100 / field.column}%`,
display: 'inline-block'
};
}
};
return (
<div className="form-group" style={fieldStyle()}>
<label className="control-label" htmlFor={`field-${field._id}`}>
{field.text}
{field.isRequired ? <span className="required">*</span> : null}
</label>
<span className="error">{error && error.text}</span>
{field.description ? (
<span className="description">{field.description}</span>
) : null}
{this.renderControl()}
{isAttachingFile ? <div className="loader" /> : null}
</div>
);
}
}