UNPKG

@arudovwen/react-form-builder

Version:
567 lines (556 loc) 20.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireDefault(require("react")); var _reactDom = _interopRequireDefault(require("react-dom")); var _fbemitter = require("fbemitter"); var _reactIntl = require("react-intl"); var _formValidator = _interopRequireDefault(require("./form-validator")); var _formElements = _interopRequireDefault(require("./form-elements")); var _multiColumn = require("./multi-column"); var _fieldset = require("./fieldset"); var _customElement = _interopRequireDefault(require("./form-elements/custom-element")); var _registry = _interopRequireDefault(require("./stores/registry")); var _ToggleFieldsContainer = _interopRequireDefault(require("./ToggleFieldsContainer")); const { Image, Checkboxes, Signature, Download, Camera, FileUpload } = _formElements.default; // ---- validators (hoisted) ---- const EMAIL_RE = // eslint-disable-next-line no-useless-escape /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}(\.\d{1,3}){3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const validateEmail = email => EMAIL_RE.test(String(email || '').trim()); const validatePhone = phone => /^(\+)?([0-9]{1,4}[\s\-()]?){2,}$/.test(String(phone || '').trim()); class ReactForm extends _react.default.Component { constructor(props) { super(props); (0, _defineProperty2.default)(this, "form", null); (0, _defineProperty2.default)(this, "inputs", {}); // map field_name -> ref/component (0, _defineProperty2.default)(this, "emitter", new _fbemitter.EventEmitter()); (0, _defineProperty2.default)(this, "onChangeDebounceId", null); (0, _defineProperty2.default)(this, "_getDefaultValue", item => this.state.answers?.[item.field_name]); (0, _defineProperty2.default)(this, "_getOptionKeyValue", option => this.props.option_key_value === 'value' ? option?.value : option?.key); (0, _defineProperty2.default)(this, "handleRenderSubmit", () => { const name = this.props.action_name || this.props.actionName || 'Submit'; return this.props.submitButton || /*#__PURE__*/_react.default.createElement("input", { type: "submit", className: "btn btn-big", value: name }); }); (0, _defineProperty2.default)(this, "handleRenderBack", () => { const name = this.props.back_name || this.props.backName || 'Cancel'; if (!this.props.back_action) return null; return this.props.backButton || /*#__PURE__*/_react.default.createElement("a", { href: this.props.back_action, className: "btn btn-default btn-cancel btn-big" }, name); }); this.state = { answers: this._convert(props.answer_data) || {} // canonical answers }; this.getDataById = this.getDataById.bind(this); this.handleBlur = this.handleBlur.bind(this); this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } componentWillUnmount() { if (this.onChangeDebounceId) clearTimeout(this.onChangeDebounceId); } // Convert array<{name,value}> to object _convert(answers) { if (Array.isArray(answers)) { const result = {}; answers.forEach(x => { if (x?.name?.indexOf('tags_') > -1) { result[x.name] = (x.value || []).map(y => y.value); } else { result[x.name] = x.value; } }); return result; } return answers || {}; } // answers accessor with variable overlays (read-only variables) _answersWithVariables() { const { answers } = this.state; const { variables = {}, data = [] } = this.props; if (!variables || !data?.length) return answers; const overlay = { ...answers }; for (const item of data) { if (!item) continue; if (item.readOnly && item.variableKey && variables[item.variableKey] != null) { overlay[item.field_name] = variables[item.variableKey]; } } return overlay; } _optionsDefaultValue(item) { const defaultValue = this._getDefaultValue(item); if (defaultValue) return defaultValue; const def = []; (item.options || []).forEach(option => { const key = option?.key; if (key && this.state.answers?.[`option_${key}`]) { def.push(key); } }); return def; } _getItemValue(item, ref, trimValue) { // Return uniform { element, value } const $ = { element: item.element, value: '' }; if (!item || !ref) return $; switch (item.element) { case 'Rating': $.value = ref?.inputField?.current?.state?.rating ?? ''; return $; case 'Tags': $.value = ref?.inputField?.current?.state?.value ?? ''; return $; case 'DatePicker': $.value = ref?.state?.value ?? ''; return $; case 'Camera': $.value = ref?.state?.img ?? ''; return $; case 'TableInput': $.value = ref?.state?.tableData ?? ''; return $; case 'RadioButton': $.value = ref?.state?.selectedValue ?? ''; return $; case 'DynamicMultiInput': case 'DataGridInput': case 'CascadeSelect': case 'CustomDatePicker': case 'ArithmeticInput': case 'CustomSelect': $.value = ref?.state?.dataList ?? ''; return $; case 'FileUpload': case 'MultiFileUpload': $.value = ref?.state?.fileUpload ?? []; return $; default: { // Native input path: prefer an explicit ref from child if provided const inputRef = ref?.inputField?.current; if (inputRef) { const dom = inputRef; // current is a DOM node for native inputs let v = dom.value; if (trimValue && typeof v === 'string') v = v.trim(); $.value = v; return $; } // Fallback (legacy): try findDOMNode on child wrapper const dom = _reactDom.default.findDOMNode(ref?.inputField?.current || ref); if (dom && dom.value != null) { $.value = trimValue && typeof dom.value === 'string' ? dom.value.trim() : dom.value; } return $; } } } _isIncorrect(item) { if (!item?.canHaveAnswer) return false; const ref = this.inputs[item.field_name]; if (!ref) return false; if (item.element === 'Checkboxes' || item.element === 'RadioButtons') { for (const option of item.options || []) { const child = ref.options?.[`child_ref_${option.key}`]; const dom = child && _reactDom.default.findDOMNode(child); const checked = !!dom?.checked; const hasCorrect = Object.prototype.hasOwnProperty.call(option || {}, 'correct'); if (hasCorrect && !checked || !hasCorrect && checked) return true; } return false; } const $item = this._getItemValue(item, ref, true); if (item.element === 'Rating') { return String($item.value) !== String(item.correct); } return String($item.value || '').toLowerCase() !== String(item.correct || '').trim().toLowerCase(); } _isInvalid(item) { if (!item?.required) return false; const ref = this.inputs[item.field_name]; if (!ref) return true; if (item.element === 'Checkboxes' || item.element === 'RadioButtons') { let checked = 0; for (const option of item.options || []) { const child = ref.options?.[`child_ref_${option.key}`]; const dom = child && _reactDom.default.findDOMNode(child); if (dom?.checked) checked += 1; } return checked < 1; } const $item = this._getItemValue(item, ref, false); if (item.element === 'Rating') return Number($item.value) === 0; if (item.element === 'MultiFileUpload') return ($item.value || []).some(dt => !dt?.fileData); return !$item.value || $item.value?.length < 1; } _collect(item, trimValue) { if (!item?.field_name) return null; const ref = this.inputs[item.field_name]; const itemData = { id: item.id, name: item.field_name, custom_name: item.custom_name || item.field_name }; if (item.element === 'Checkboxes' || item.element === 'RadioButtons') { const checked = []; for (const option of item.options || []) { const child = this.inputs[item.field_name]?.options?.[`child_ref_${option.key}`]; const dom = child && _reactDom.default.findDOMNode(child); if (dom?.checked) checked.push(this._getOptionKeyValue(option)); } itemData.value = checked; return itemData; } if (!ref) return null; itemData.value = this._getItemValue(item, ref, trimValue).value; return itemData; } _collectFormData(items, trimValue) { const out = []; for (const item of items || []) { const d = this._collect(item, trimValue); if (d) out.push(d); } return out; } _getSignatureImg(item) { const ref = this.inputs[item.field_name]; const canvas = ref?.canvas?.current; if (!canvas) return; const base64 = canvas.toDataURL().replace('data:image/png;base64,', ''); const isEmpty = canvas.isEmpty(); const input = ref?.inputField?.current && _reactDom.default.findDOMNode(ref.inputField.current); if (input) input.value = isEmpty ? '' : base64; } // ---- events ---- handleSubmit(e) { e.preventDefault(); let errors = []; if (!this.props.skip_validations) { errors = this.validateForm(); this.emitter.emit('formValidation', errors); } if (errors.length < 1) { const data = this._collectFormData(this.props.data, true); if (this.props.onSubmit) { this.props.onSubmit(data); } else { const $form = _reactDom.default.findDOMNode(this.form); $form && $form.submit && $form.submit(); } } } handleBlur() { // snapshot answers on blur (trimmed) const data = this._collectFormData(this.props.data, true); const next = {}; data.forEach(item => next[item.name] = item.value); this.setState({ answers: next }, () => { this.props.onBlur?.(data); }); } handleChange() { // snapshot answers on change (untrimmed), debounce parent onChange const data = this._collectFormData(this.props.data, false); const next = {}; data.forEach(item => next[item.name] = item.value); this.setState({ answers: next }); if (this.onChangeDebounceId) clearTimeout(this.onChangeDebounceId); this.onChangeDebounceId = setTimeout(() => { this.props.onChange?.(data); }, 120); } // ---- validation ---- validateForm() { const errors = []; const { intl, display_short } = this.props; const data_items = display_short ? (this.props.data || []).filter(i => i?.alternateForm === true) : this.props.data || []; for (const item of data_items) { if (!item) continue; if (item.element === 'Signature') this._getSignatureImg(item); if (this._isInvalid(item)) { errors.push(`${item.label} ${intl.formatMessage({ id: 'message.is-required' })}!`); } if (item.element === 'EmailInput') { const ref = this.inputs[item.field_name]; const emailValue = this._getItemValue(item, ref).value; if (emailValue && !validateEmail(emailValue)) { errors.push(`${item.label} ${intl.formatMessage({ id: 'message.invalid-email' })}`); } } if (item.element === 'PhoneNumber') { const ref = this.inputs[item.field_name]; const phoneValue = this._getItemValue(item, ref).value; if (phoneValue && !validatePhone(phoneValue)) { errors.push(`${item.label} ${intl.formatMessage({ id: 'message.invalid-phone-number' })}`); } } if (this.props.validateForCorrectness && this._isIncorrect(item)) { errors.push(`${item.label} ${intl.formatMessage({ id: 'message.was-answered-incorrectly' })}!`); } } return errors; } // ---- rendering ---- getDataById(id) { return (this.props.data || []).find(x => x?.id === id); } getInputElement(item) { if (!item) return null; if (item.custom) return this.getCustomElement(item); const Input = _formElements.default[item.element]; const ro = this.props.read_only || item?.readOnly; // Pass live answers (with variable overlays) so dependent fields update const liveAnswers = this._answersWithVariables(); console.log({ item }); return /*#__PURE__*/_react.default.createElement("div", { key: `form_${item.id}` }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Input, { handleChange: this.handleChange, ref: c => this.inputs[item.field_name] = c, mutable: true, data: item, read_only: ro, defaultValue: this._getDefaultValue(item), resultData: liveAnswers }))); } getContainerElement(item, Element) { const controls = (item.childItems || []).map(x => x ? this.getInputElement(this.getDataById(x)) : /*#__PURE__*/_react.default.createElement("div", { key: `empty_${item.id}` }, "\xA0")); return /*#__PURE__*/_react.default.createElement(Element, { mutable: true, key: `form_${item.id}`, data: item, controls: controls }); } getSimpleElement(item) { const Element = _formElements.default[item.element]; return /*#__PURE__*/_react.default.createElement(Element, { mutable: true, key: `form_${item.id}`, data: item }); } getCustomElement(item) { const { intl } = this.props; if (!item.component || typeof item.component !== 'function') { item.component = _registry.default.get(item.key); if (!item.component) { console.error(`${item.element} ${intl.formatMessage({ id: 'message.was-not-registered' })}`); } } const inputProps = item.forwardRef ? { handleChange: this.handleChange, defaultValue: this._getDefaultValue(item), ref: c => this.inputs[item.field_name] = c } : {}; const liveAnswers = this._answersWithVariables(); return /*#__PURE__*/_react.default.createElement("div", { key: `form_${item.id}` }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(_customElement.default, (0, _extends2.default)({ mutable: true, read_only: this.props.read_only || item?.readOnly, data: item, resultData: liveAnswers }, inputProps)))); } render() { const displayItems = this.props.display_short ? (this.props.data || []).filter(i => i?.alternateForm === true) : this.props.data || []; const items = displayItems.filter(x => x && !x.parentId).map(item => { switch (item.element) { case 'TextInput': case 'DynamicInput': case 'AmountInput': case 'ArithmeticInput': case 'DocumentSelect': case 'CascadeSelect': case 'DynamicMultiInput': case 'DataGridInput': case 'CustomSelect': case 'CustomDatepicker': case 'TableInput': case 'EmailInput': case 'PhoneNumber': case 'NumberInput': case 'PasswordInput': case 'MultiFileUpload': case 'TextArea': case 'Dropdown': case 'SmartAdaptorDropdown': case 'DatePicker': case 'RadioButtons': case 'RadioButton': case 'Rating': case 'Tags': case 'Range': return this.getInputElement(item); case 'CustomElement': return this.getCustomElement(item); case 'MultiColumnRow': return this.getContainerElement(item, _multiColumn.MultiColumnRow); case 'ThreeColumnRow': return this.getContainerElement(item, _multiColumn.ThreeColumnRow); case 'TwoColumnRow': return this.getContainerElement(item, _multiColumn.TwoColumnRow); case 'FieldSet': return this.getContainerElement(item, _fieldset.FieldSet); case 'Signature': return /*#__PURE__*/_react.default.createElement(Signature, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only || item?.readOnly, mutable: true, data: item, defaultValue: this._getDefaultValue(item) }); case 'Checkboxes': return /*#__PURE__*/_react.default.createElement(Checkboxes, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only, handleChange: this.handleChange, mutable: true, data: item, defaultValue: this._optionsDefaultValue(item) }); case 'Image': return /*#__PURE__*/_react.default.createElement(Image, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, handleChange: this.handleChange, mutable: true, data: item, defaultValue: this._getDefaultValue(item) }); case 'Download': return /*#__PURE__*/_react.default.createElement(Download, { key: `form_${item.id}`, download_path: this.props.download_path, mutable: true, data: item }); case 'Camera': return /*#__PURE__*/_react.default.createElement(Camera, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only || item?.readOnly, mutable: true, data: item, defaultValue: this._getDefaultValue(item) }); case 'FileUpload': return /*#__PURE__*/_react.default.createElement(FileUpload, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only || item?.readOnly, mutable: true, data: item, defaultValue: this._getDefaultValue(item) }); default: return this.getSimpleElement(item); } }); return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_formValidator.default, { emitter: this.emitter }), /*#__PURE__*/_react.default.createElement("div", { className: "react-form-builder-form" }, /*#__PURE__*/_react.default.createElement("form", { encType: "multipart/form-data", ref: c => this.form = c, action: this.props.form_action, onBlur: this.handleBlur, onChange: this.handleChange, onSubmit: this.handleSubmit, method: this.props.form_method }, this.props.authenticity_token && /*#__PURE__*/_react.default.createElement("div", { style: { display: 'none' } }, /*#__PURE__*/_react.default.createElement("input", { name: "utf8", type: "hidden", value: "\u2713" }), /*#__PURE__*/_react.default.createElement("input", { name: "authenticity_token", type: "hidden", value: this.props.authenticity_token }), /*#__PURE__*/_react.default.createElement("input", { name: "task_id", type: "hidden", value: this.props.task_id })), /*#__PURE__*/_react.default.createElement("div", { id: "form-items", className: "" }, items), /*#__PURE__*/_react.default.createElement("div", { className: "mt-6 btn-toolbar" }, !this.props.hide_actions && this.handleRenderSubmit(), !this.props.hide_actions && this.props.back_action && this.handleRenderBack())))); } } ReactForm.defaultProps = { validateForCorrectness: false }; var _default = exports.default = (0, _reactIntl.injectIntl)(ReactForm);