├── jquery-stub.js ├── examples ├── cra │ ├── .env │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── manifest.json │ │ └── index.html │ ├── .gitignore │ ├── src │ │ ├── app.js │ │ ├── requests.js │ │ ├── index.js │ │ ├── logo.svg │ │ ├── serviceWorker.js │ │ └── demobar.js │ ├── package.json │ └── README.md ├── custom │ ├── jquery-stub.js │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── .babelrc │ ├── requests.js │ ├── webpack.config.js │ ├── index.html │ ├── package.json │ ├── app.js │ └── demobar.js ├── demo │ ├── jquery-stub.js │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── .babelrc │ ├── app.js │ ├── index.html │ ├── webpack.config.js │ ├── package.json │ └── demobar.js ├── next │ ├── server │ │ ├── index.js │ │ ├── api │ │ │ ├── dummyFormData.json │ │ │ ├── formData.js │ │ │ ├── routes.js │ │ │ └── form.js │ │ └── server.js │ ├── .gitignore │ ├── readme.md │ ├── .nodemon.json │ ├── styles │ │ └── site.css │ ├── components │ │ └── requests.js │ ├── pages │ │ ├── index.js │ │ ├── _app.js │ │ └── form.js │ └── package.json ├── mongo │ ├── .gitignore │ ├── server │ │ ├── index.js │ │ ├── api │ │ │ ├── dummyFormData.json │ │ │ ├── routes.js │ │ │ ├── formData.js │ │ │ └── form.js │ │ └── server.js │ ├── readme.md │ ├── .nodemon.json │ ├── next.config.js │ ├── styles │ │ └── site.css │ ├── pages │ │ ├── index.js │ │ ├── _app.js │ │ └── form.js │ ├── components │ │ └── requests.js │ └── package.json └── umd │ ├── form-builder.js │ ├── package.json │ ├── form-generator.js │ └── index.html ├── server ├── views │ ├── footer.ejs │ ├── 404.ejs │ ├── error_header.ejs │ ├── 500.ejs │ └── index.ejs ├── api │ ├── dummyFormData.json │ ├── formData.js │ ├── routes.js │ └── form.js ├── index.js └── server.js ├── .eslintignore ├── src ├── ItemTypes.js ├── fieldset │ ├── index.js │ └── FieldSet.jsx ├── multi-column │ ├── index.js │ ├── grip.jsx │ ├── MultiColumnRow.jsx │ └── dustbin.jsx ├── language-provider │ ├── entries │ │ ├── en-us.js │ │ ├── fa-ir.js │ │ ├── it-it.js │ │ └── vi-vn.js │ ├── IntlMessages.js │ ├── index.js │ └── locales │ │ ├── vi-vn.json │ │ ├── fa-ir.json │ │ ├── en-us.json │ │ └── it-it.json ├── form-elements │ ├── myxss.js │ ├── component-label.jsx │ ├── component-header.jsx │ ├── component-drag-preview.jsx │ ├── header-bar.jsx │ ├── component-drag-handle.jsx │ ├── component-drag-layer.jsx │ ├── custom-element.jsx │ └── date-picker.jsx ├── stores │ ├── requests.js │ ├── registry.js │ └── store.js ├── form-dynamic-edit.jsx ├── toolbar-group-item.jsx ├── functions │ └── index.js ├── toolbar-draggable-item.jsx ├── form-place-holder.jsx ├── UUID.js ├── form-validator.jsx ├── sortable-form-elements.jsx ├── index.jsx ├── sortable-element.jsx └── dynamic-option-list.jsx ├── screenshot.png ├── screenshot2.png ├── screenshot3.png ├── scss ├── variables.scss ├── react-date-picker.scss ├── application.scss ├── react-star-rating.scss └── form-builder-form.scss ├── public ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 └── index.html ├── .babelrc ├── .vscode ├── launch.json └── settings.json ├── .gitignore ├── app.js ├── LICENSE ├── .eslintrc.json ├── index.html ├── webpack.config.js ├── webpack.production.config.js ├── docs ├── form-builder.js ├── index.html └── form-generator.js ├── package.json └── types └── index.d.ts /jquery-stub.js: -------------------------------------------------------------------------------- 1 | module.exports = null; -------------------------------------------------------------------------------- /examples/cra/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /server/views/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/custom/jquery-stub.js: -------------------------------------------------------------------------------- 1 | module.exports = null; -------------------------------------------------------------------------------- /server/api/dummyFormData.json: -------------------------------------------------------------------------------- 1 | { "task_data": "[]" } -------------------------------------------------------------------------------- /examples/demo/jquery-stub.js: -------------------------------------------------------------------------------- 1 | module.exports = null; 2 | -------------------------------------------------------------------------------- /examples/next/server/index.js: -------------------------------------------------------------------------------- 1 | require('./server.js'); 2 | -------------------------------------------------------------------------------- /examples/mongo/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | *.log 4 | -------------------------------------------------------------------------------- /examples/mongo/server/index.js: -------------------------------------------------------------------------------- 1 | require('./server.js'); 2 | -------------------------------------------------------------------------------- /examples/next/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | *.log 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/variables.js 2 | **/webpack.*.js 3 | /docs/*.*.js -------------------------------------------------------------------------------- /examples/mongo/server/api/dummyFormData.json: -------------------------------------------------------------------------------- 1 | { "task_data": "[]" } -------------------------------------------------------------------------------- /examples/next/server/api/dummyFormData.json: -------------------------------------------------------------------------------- 1 | { "task_data": "[]" } -------------------------------------------------------------------------------- /src/ItemTypes.js: -------------------------------------------------------------------------------- 1 | export default { CARD: 'card', BOX: 'box' }; 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/screenshot.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/screenshot2.png -------------------------------------------------------------------------------- /screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/screenshot3.png -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | require('./server.js'); 3 | -------------------------------------------------------------------------------- /src/fieldset/index.js: -------------------------------------------------------------------------------- 1 | import FieldSet from './FieldSet'; 2 | 3 | export { FieldSet }; 4 | -------------------------------------------------------------------------------- /examples/cra/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /scss/variables.scss: -------------------------------------------------------------------------------- 1 | $screen-sm-min: 768px; 2 | $screen-md-min: 992px; 3 | $screen-lg-min: 1200px; -------------------------------------------------------------------------------- /server/views/404.ejs: -------------------------------------------------------------------------------- 1 | <% include error_header %> 2 |

Cannot find <%= url %>

3 | <% include footer %> -------------------------------------------------------------------------------- /examples/mongo/readme.md: -------------------------------------------------------------------------------- 1 | ## Develop 2 | ```bash 3 | $ npm install 4 | $ npm run build 5 | $ npm run dev 6 | ``` -------------------------------------------------------------------------------- /examples/next/readme.md: -------------------------------------------------------------------------------- 1 | ## Develop 2 | ```bash 3 | $ npm install 4 | $ npm run build 5 | $ npm run dev 6 | ``` -------------------------------------------------------------------------------- /public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /examples/cra/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/favicon.ico -------------------------------------------------------------------------------- /examples/cra/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/logo192.png -------------------------------------------------------------------------------- /examples/cra/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/logo512.png -------------------------------------------------------------------------------- /examples/custom/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/custom/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /examples/demo/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/demo/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /server/api/formData.js: -------------------------------------------------------------------------------- 1 | const formData = require('./dummyFormData.json'); 2 | 3 | module.exports = { data: formData, answers: {} }; 4 | -------------------------------------------------------------------------------- /examples/cra/public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /examples/demo/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/demo/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /examples/demo/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/demo/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /examples/demo/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/demo/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /examples/custom/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/custom/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /examples/custom/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/custom/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /examples/custom/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/custom/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /examples/custom/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/custom/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /examples/demo/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/demo/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /examples/next/server/api/formData.js: -------------------------------------------------------------------------------- 1 | const formData = require('./dummyFormData.json'); 2 | 3 | module.exports = { data: formData, answers: {} }; 4 | -------------------------------------------------------------------------------- /server/views/error_header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 5 | 6 | 7 | 8 |

An error occurred!

-------------------------------------------------------------------------------- /examples/cra/public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /examples/cra/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /examples/cra/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /examples/cra/public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiho/react-form-builder/HEAD/examples/cra/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /examples/mongo/.nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["node_modules", ".next"], 4 | "watch": ["server/**/*"], 5 | "ext": "js json" 6 | } -------------------------------------------------------------------------------- /examples/next/.nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["node_modules", ".next"], 4 | "watch": ["server/**/*"], 5 | "ext": "js json" 6 | } -------------------------------------------------------------------------------- /scss/react-date-picker.scss: -------------------------------------------------------------------------------- 1 | @use "../node_modules/react-datepicker/src/stylesheets/datepicker.scss"; 2 | 3 | .react-datepicker-popper { 4 | z-index: 2000; 5 | } -------------------------------------------------------------------------------- /examples/mongo/next.config.js: -------------------------------------------------------------------------------- 1 | const withSass = require('@zeit/next-sass'); 2 | const withCSS = require('@zeit/next-css'); 3 | 4 | module.exports = withCSS(withSass()); 5 | -------------------------------------------------------------------------------- /src/multi-column/index.js: -------------------------------------------------------------------------------- 1 | import { TwoColumnRow, ThreeColumnRow, MultiColumnRow } from './MultiColumnRow'; 2 | 3 | export { TwoColumnRow, ThreeColumnRow, MultiColumnRow }; 4 | -------------------------------------------------------------------------------- /src/language-provider/entries/en-us.js: -------------------------------------------------------------------------------- 1 | import enMessages from '../locales/en-us.json'; 2 | 3 | const EnLang = { 4 | messages: { 5 | ...enMessages, 6 | }, 7 | locale: 'en-US', 8 | }; 9 | export default EnLang; 10 | -------------------------------------------------------------------------------- /src/language-provider/entries/fa-ir.js: -------------------------------------------------------------------------------- 1 | import faMessages from '../locales/fa-ir.json'; 2 | 3 | const FaLang = { 4 | messages: { 5 | ...faMessages, 6 | }, 7 | locale: 'fa-IR', 8 | }; 9 | export default FaLang; 10 | -------------------------------------------------------------------------------- /src/language-provider/entries/it-it.js: -------------------------------------------------------------------------------- 1 | import itMessages from '../locales/it-it.json'; 2 | 3 | const ItLang = { 4 | messages: { 5 | ...itMessages, 6 | }, 7 | locale: 'it-IT', 8 | }; 9 | export default ItLang; 10 | -------------------------------------------------------------------------------- /src/language-provider/entries/vi-vn.js: -------------------------------------------------------------------------------- 1 | import viMessages from '../locales/vi-vn.json'; 2 | 3 | const VnLang = { 4 | messages: { 5 | ...viMessages, 6 | }, 7 | locale: 'vi-VN', 8 | }; 9 | export default VnLang; 10 | -------------------------------------------------------------------------------- /server/views/500.ejs: -------------------------------------------------------------------------------- 1 | <% include error_header %> 2 |

Error: <%= error.message %>

3 | <% if (settings['verbose errors']) { %> 4 |
<%= error.stack %>
5 | <% } else { %> 6 |

An error occurred!

7 | <% } %> 8 | <% include footer %> -------------------------------------------------------------------------------- /examples/mongo/styles/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-weight: 300; 4 | color: #404d5b; 5 | width: 1200px; 6 | margin: 0 auto; 7 | } 8 | .modal { background: rgba(0,0,0, 0.3);} 9 | .modal-content { padding: 30px; max-height: 800px; overflow-y: auto } -------------------------------------------------------------------------------- /examples/next/styles/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-weight: 300; 4 | color: #404d5b; 5 | width: 1200px; 6 | margin: 0 auto; 7 | } 8 | .modal { background: rgba(0,0,0, 0.3);} 9 | .modal-content { padding: 30px; max-height: 500px; overflow-y: auto } -------------------------------------------------------------------------------- /src/language-provider/IntlMessages.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage, injectIntl } from 'react-intl'; 3 | 4 | const InjectMassage = props => ; 5 | export default injectIntl(InjectMassage, { 6 | withRef: false, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/umd/form-builder.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // const e = React.createElement; 3 | const FormBuilder = ReactFormBuilder.ReactFormBuilder; 4 | const domContainer = document.querySelector('#form-builder'); 5 | 6 | ReactDOM.render(e(FormBuilder, { url: '/api/formdata', saveUrl: '/api/formdata' }), domContainer); 7 | -------------------------------------------------------------------------------- /examples/custom/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-json-strings", 9 | "@babel/plugin-syntax-dynamic-import", 10 | "@babel/plugin-syntax-import-meta" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-json-strings", 9 | "@babel/plugin-syntax-dynamic-import", 10 | "@babel/plugin-syntax-import-meta" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-json-strings", 9 | "@babel/plugin-syntax-dynamic-import", 10 | "@babel/plugin-syntax-import-meta", 11 | "@babel/plugin-transform-runtime" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/cra/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/cra/src/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactFormBuilder } from "@threehippies/react-form-builder"; 3 | import * as variables from "./variables"; 4 | 5 | const url = "/api/formdata"; 6 | const saveUrl = "/api/formdata"; 7 | 8 | const App = () => ( 9 | 16 | ); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/form-elements/myxss.js: -------------------------------------------------------------------------------- 1 | import xss from 'xss'; 2 | 3 | const myxss = new xss.FilterXSS({ 4 | whiteList: { 5 | a: ['href', 'title', 'target'], 6 | u: [], 7 | br: [], 8 | b: [], 9 | i: [], 10 | ol: ['style'], 11 | ul: ['style'], 12 | li: [], 13 | p: ['style'], 14 | sub: [], 15 | sup: [], 16 | div: ['style'], 17 | em: [], 18 | strong: [], 19 | span: ['style'], 20 | ins: [], 21 | }, 22 | }); 23 | 24 | export default myxss; 25 | -------------------------------------------------------------------------------- /examples/custom/requests.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const headers = { 4 | Accept: 'application/json', 5 | 'Content-Type': 'application/json; charset=utf-8', 6 | OPTIONS: '', 7 | }; 8 | 9 | export function post(url, data) { 10 | return fetch(url, { 11 | method: 'POST', 12 | headers, 13 | body: JSON.stringify(data), 14 | }).then(response => response); 15 | } 16 | 17 | export function get(url) { 18 | return fetch(url, { 19 | method: 'GET', 20 | headers, 21 | }).then(response => response.json()); 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "pwa-chrome", 10 | "request": "launch", 11 | "name": "Launch Chrome against localhost", 12 | "url": "http://localhost:8080", 13 | "webRoot": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scss/application.scss: -------------------------------------------------------------------------------- 1 | // Modern SCSS file structure using @use and @forward 2 | 3 | // Core variables and mixins 4 | // @use replaces @import and prevents global namespace pollution 5 | @use 'variables' as vars; 6 | 7 | // Component styles with namespacing 8 | // This allows you to reference variables as component.variable 9 | @use 'react-star-rating'; 10 | @use 'react-select'; 11 | @use 'react-bootstrap-slider'; 12 | @use 'react-date-picker'; 13 | @use 'react-draft'; 14 | 15 | // Form builder styles 16 | @use 'form-builder'; 17 | @use 'form-builder-form'; -------------------------------------------------------------------------------- /src/stores/requests.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const headers = { 4 | Accept: 'application/json', 5 | 'Content-Type': 'application/json; charset=utf-8', 6 | OPTIONS: '', 7 | }; 8 | 9 | export function post(url, data) { 10 | return fetch(url, { 11 | method: 'POST', 12 | headers, 13 | body: JSON.stringify(data), 14 | }).then(response => response); 15 | } 16 | 17 | export function get(url) { 18 | return fetch(url, { 19 | method: 'GET', 20 | headers, 21 | }).then(response => response.json()); 22 | } 23 | -------------------------------------------------------------------------------- /examples/cra/src/requests.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const headers = { 4 | Accept: 'application/json', 5 | 'Content-Type': 'application/json; charset=utf-8', 6 | OPTIONS: '', 7 | }; 8 | 9 | export function post(url, data) { 10 | return fetch(url, { 11 | method: 'POST', 12 | headers, 13 | body: JSON.stringify(data), 14 | }).then(response => response); 15 | } 16 | 17 | export function get(url) { 18 | return fetch(url, { 19 | method: 'GET', 20 | headers, 21 | }).then(response => response.json()); 22 | } 23 | -------------------------------------------------------------------------------- /examples/next/components/requests.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const headers = { 4 | Accept: 'application/json', 5 | 'Content-Type': 'application/json; charset=utf-8', 6 | OPTIONS: '', 7 | }; 8 | 9 | export function post(url, data) { 10 | return fetch(url, { 11 | method: 'POST', 12 | headers, 13 | body: JSON.stringify(data), 14 | }).then(response => response); 15 | } 16 | 17 | export function get(url) { 18 | return fetch(url, { 19 | method: 'GET', 20 | headers, 21 | }).then(response => response.json()); 22 | } 23 | -------------------------------------------------------------------------------- /examples/mongo/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormBuilder from '@threehippies/react-form-builder'; 3 | import DemoBar from '../components/demobar'; 4 | 5 | // Form Data 6 | const url = '/api/formdata'; 7 | const saveUrl = '/api/formdata'; 8 | const postUrl = '/api/form'; 9 | 10 | class Index extends React.Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | export default Index; 23 | -------------------------------------------------------------------------------- /examples/next/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormBuilder from '@threehippies/react-form-builder'; 3 | import DemoBar from '../components/demobar'; 4 | 5 | // Form Data 6 | const url = '/api/formdata'; 7 | const saveUrl = '/api/formdata'; 8 | const postUrl = '/api/form'; 9 | 10 | class Index extends React.Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | export default Index; 23 | -------------------------------------------------------------------------------- /examples/umd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfb2-umd", 3 | "version": "0.0.7", 4 | "description": "A complete form builder for react.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "react", 8 | "react-component", 9 | "form", 10 | "builder", 11 | "ui", 12 | "drag", 13 | "drop" 14 | ], 15 | "author": "Kiho Chang", 16 | "devDependencies": { 17 | "live-server": "1.2.1", 18 | "@threehippies/react-form-builder": "^0.20.1" 19 | }, 20 | "scripts": { 21 | "start": "live-server --cors --port=8007 --proxy=/api:http://127.0.0.1:5005/api --watch=./bundle.js,./index.html ." 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/cra/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/form-dynamic-edit.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormElementsEditor = (props) => { 4 | const [ 5 | dynamic, 6 | setDynamic, 7 | ] = React.useState(null); 8 | 9 | React.useEffect(() => { 10 | const loadDynamic = async () => { 11 | const x = await import('./form-elements-edit'); 12 | setDynamic(() => x.default); 13 | }; 14 | loadDynamic(); 15 | }, []); 16 | 17 | if (!dynamic) { 18 | return (
Loading...
); 19 | } 20 | const Component = dynamic; 21 | return ; 22 | }; 23 | 24 | export default FormElementsEditor; 25 | -------------------------------------------------------------------------------- /examples/mongo/components/requests.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | const headers = { 4 | Accept: 'application/json', 5 | 'Content-Type': 'application/json; charset=utf-8', 6 | OPTIONS: '', 7 | }; 8 | 9 | export function post(url, data) { 10 | return fetch(url, { 11 | method: 'POST', 12 | headers, 13 | body: JSON.stringify(data), 14 | }).then(response => response); 15 | } 16 | 17 | export function get(url) { 18 | return fetch(url, { 19 | method: 'GET', 20 | headers, 21 | }).then(response => { 22 | const r = response.json(); 23 | return r; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "extensions.ignoreRecommendations": true, 6 | "files.insertFinalNewline": true, 7 | "files.trimTrailingWhitespace": true, 8 | "eslint.format.enable": true, 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "typescriptreact" 14 | ], 15 | "prettier.documentSelectors": ["**/*.{ts,tsx,js,jsx}"], 16 | "prettier.singleQuote": true, 17 | "files.eol": "\n", 18 | "javascript.preferences.quoteStyle": "single", 19 | "typescript.preferences.quoteStyle": "single", 20 | "prettier.quoteProps": "consistent" 21 | } 22 | -------------------------------------------------------------------------------- /examples/next/server/api/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable no-var */ 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | var express = require('express'); 5 | // var handleForm = require('./form'); 6 | var formData = require('./formData'); 7 | 8 | var app = express(); 9 | 10 | app.route('/formdata/') 11 | .get((req, res) => { 12 | // console.log('get formdata: ', formData.data); 13 | res.send(formData.data.task_data); 14 | }) 15 | .post((req, res) => { 16 | formData.data = req.body; 17 | // console.log('post formdata: ', formData.data); 18 | res.status(200).send(); 19 | }); 20 | 21 | module.exports = app; 22 | -------------------------------------------------------------------------------- /examples/demo/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { ReactFormBuilder } from '@threehippies/react-form-builder'; 4 | import DemoBar from './demobar'; 5 | import * as variables from './variables'; 6 | 7 | import '@threehippies/react-form-builder/dist/app.css'; 8 | 9 | const app = ReactDOM.createRoot(document.getElementById('form-builder')); 10 | app.render( 11 | 12 | 17 | 18 | ); 19 | 20 | const demo = ReactDOM.createRoot(document.getElementById('demo-bar')); 21 | demo.render(); 22 | -------------------------------------------------------------------------------- /examples/cra/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './app'; 5 | import DemoBar from './demobar'; 6 | import * as serviceWorker from './serviceWorker'; 7 | import * as variables from './variables'; 8 | 9 | const root = createRoot(document.getElementById('form-builder')); 10 | root.render(); 11 | 12 | const demoBarRoot = createRoot(document.getElementById('demo-bar')); 13 | demoBarRoot.render(); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /src/form-elements/component-label.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import myxss from './myxss'; 3 | 4 | const ComponentLabel = (props) => { 5 | const hasRequiredLabel = 6 | props.data.hasOwnProperty('required') && 7 | props.data.required === true && 8 | !props.read_only; 9 | const labelText = myxss.process(props.data.label); 10 | if (!labelText) { 11 | return null; 12 | } 13 | return ( 14 | 20 | ); 21 | }; 22 | 23 | export default ComponentLabel; 24 | -------------------------------------------------------------------------------- /examples/next/pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | // stylings 4 | import '@threehippies/react-form-builder/dist/app.css'; 5 | import '../styles/site.css'; 6 | 7 | function MyApp({ Component, pageProps }) { 8 | return ( 9 | <> 10 | 11 | 15 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default MyApp; 28 | -------------------------------------------------------------------------------- /src/toolbar-group-item.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | import React, { useState } from 'react'; 6 | 7 | function ToolbarGroupItem(props) { 8 | const { name, group, renderItem } = props; 9 | 10 | const [show, setShow] = useState(false); 11 | 12 | function onClick() { 13 | setShow(!show); 14 | } 15 | 16 | const classShow = 'collapse' + (show ? ' show' : ''); 17 | return ( 18 |
  • 19 |
    20 | 23 |
    24 |
      25 | { group.map(renderItem) } 26 |
    27 |
    28 |
    29 |
  • 30 | ); 31 | } 32 | 33 | export default ToolbarGroupItem; 34 | -------------------------------------------------------------------------------- /src/language-provider/index.js: -------------------------------------------------------------------------------- 1 | import enLang from './entries/en-us'; 2 | import itLang from './entries/it-it'; 3 | import vnLang from './entries/vi-vn'; 4 | import faLang from './entries/fa-ir'; 5 | 6 | export const AppLanguages = [ 7 | { 8 | languageId: 'vietnamese', 9 | locale: 'vi', 10 | name: 'Vietnamese', 11 | icon: 'vn', 12 | }, 13 | { 14 | languageId: 'english', 15 | locale: 'en', 16 | name: 'English', 17 | icon: 'us', 18 | }, 19 | { 20 | languageId: 'italian', 21 | locale: 'it', 22 | name: 'Italiano', 23 | icon: 'it', 24 | }, 25 | { 26 | languageId: 'persian', 27 | locale: 'ir', 28 | name: 'farsi', 29 | icon: 'ir', 30 | }, 31 | ]; 32 | 33 | const AppLocale = { 34 | en: enLang, 35 | vi: vnLang, 36 | it: itLang, 37 | fa: faLang, 38 | }; 39 | 40 | export default AppLocale; 41 | -------------------------------------------------------------------------------- /src/form-elements/component-header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderBar from './header-bar'; 3 | 4 | const ComponentHeader = (props) => { 5 | if (props.mutable) { 6 | return null; 7 | } 8 | return ( 9 |
    10 | {props.data.pageBreakBefore && ( 11 |
    Page Break
    12 | )} 13 | 25 |
    26 | ); 27 | }; 28 | 29 | export default ComponentHeader; 30 | -------------------------------------------------------------------------------- /src/functions/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as PkgTextAreaAutosize from 'react-textarea-autosize'; 3 | import * as DraftJs from 'draft-js'; 4 | import * as draftToHtml from 'draftjs-to-html'; 5 | import { Editor } from 'react-draft-wysiwyg'; 6 | import ID from '../UUID'; 7 | 8 | const generateUUID = () => ID.uuid(); 9 | 10 | const TextAreaAutosize = (props) => ; 11 | 12 | function groupBy(list, keyGetter) { 13 | const map = new Map(); 14 | list.forEach((item) => { 15 | const key = keyGetter(item); 16 | const collection = map.get(key); 17 | if (!collection) { 18 | map.set(key, [item]); 19 | } else { 20 | collection.push(item); 21 | } 22 | }); 23 | return map; 24 | } 25 | 26 | export { 27 | generateUUID, TextAreaAutosize, DraftJs, draftToHtml, Editor, groupBy, 28 | }; 29 | -------------------------------------------------------------------------------- /examples/cra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfb-cra", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^18.3.1", 7 | "react-dom": "^18.3.1", 8 | "@threehippies/react-form-builder": "^0.20.1", 9 | "react-scripts": "^4.0.3" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | }, 32 | "devDependencies": { 33 | "jquery": "^3.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | .idea/ 6 | *.iml 7 | .DS_Store 8 | 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | dist/ 44 | lib/ 45 | public/app.* 46 | public/uploads/ 47 | public/temp 48 | 49 | # bundles 50 | bundle.js 51 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import DemoBar from './demobar'; 4 | // eslint-disable-next-line no-unused-vars 5 | import FormBuilder, { Registry } from './src/index'; 6 | import * as variables from './variables'; 7 | 8 | // Add our stylesheets for the demo. 9 | require('./scss/application.scss'); 10 | 11 | const url = '/api/formdata'; 12 | const saveUrl = '/api/formdata'; 13 | 14 | const App = () => ( 15 | 23 | ); 24 | 25 | const root = createRoot(document.getElementById('form-builder')); 26 | root.render(); 27 | 28 | const demoBarRoot = createRoot(document.getElementById('demo-bar')); 29 | demoBarRoot.render(); 30 | -------------------------------------------------------------------------------- /server/api/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable no-var */ 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | var express = require('express'); 5 | var formData = require('./formData'); 6 | 7 | var optionsData = [ 8 | { text: 'Text 1', value: '1' }, 9 | { text: 'Text 2', value: '2' }, 10 | { text: 'Text 3', value: '3' }, 11 | ]; 12 | 13 | var app = express(); 14 | 15 | app.route('/formdata/') 16 | .get((req, res) => { 17 | // console.log('get formdata: ', formData.data); 18 | res.send(formData.data.task_data); 19 | }) 20 | .post((req, res) => { 21 | formData.data = req.body; 22 | // console.log('post formdata: ', formData.data); 23 | res.status(200).send(); 24 | }); 25 | app.route('/optionsdata/') 26 | .get((req, res) => { 27 | res.send(optionsData); 28 | }); 29 | 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form-next", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon server/index.js", 9 | "build": "next build", 10 | "start": "node server/index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@threehippies/react-form-builder": "^0.20.1", 17 | "react-draft-wysiwyg": "1.13.2", 18 | "body-parser": "^1.18.3", 19 | "classnames": "^2.2.5", 20 | "jquery": "^3.3.1", 21 | "es6-promise": "^4.2.4", 22 | "express": "^4.16.4", 23 | "multer": "^1.4.1", 24 | "isomorphic-fetch": "^2.2.1", 25 | "next": "^12.3.1", 26 | "next-routes": "^1.4.2", 27 | "sass": "^1.53.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^2.0.20" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/mongo/server/api/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable no-var */ 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | var express = require('express'); 5 | var formData = require('./formData'); 6 | 7 | var app = express(); 8 | 9 | function fixFormData(data) { 10 | return (!data || data === '[]' || data.length === 0) ? [] : data; 11 | } 12 | 13 | app.route('/formdata/') 14 | .get(async (req, res) => { 15 | let data = fixFormData(formData.data.task_data); 16 | // console.log('get formdata: ', data); 17 | if (!data.length) { 18 | data = await formData.load(req.db); 19 | formData.task_data = data; 20 | } 21 | res.send(data); 22 | }) 23 | .post(async (req, res) => { 24 | formData.data = req.body; 25 | const data = fixFormData(formData.data.task_data); 26 | await formData.save(req.db, data); 27 | res.status(200).send(); 28 | }); 29 | 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /src/toolbar-draggable-item.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | import React from 'react'; 6 | import { useDrag } from 'react-dnd'; 7 | import ItemTypes from './ItemTypes'; 8 | import ID from './UUID'; 9 | 10 | const ToolbarItem = ({ data, onCreate, onClick }) => { 11 | // Setup drag functionality using the useDrag hook 12 | const [{ isDragging }, drag] = useDrag({ 13 | type: ItemTypes.CARD, 14 | item: () => ({ 15 | id: ID.uuid(), 16 | index: -1, 17 | data: data, 18 | onCreate: onCreate, 19 | }), 20 | collect: (monitor) => ({ 21 | isDragging: monitor.isDragging(), 22 | }), 23 | }); 24 | 25 | // Apply slight opacity while dragging for better UX 26 | const opacity = isDragging ? 0.5 : 1; 27 | 28 | return ( 29 |
  • 34 | 35 | {data.name} 36 |
  • 37 | ); 38 | }; 39 | 40 | export default ToolbarItem; -------------------------------------------------------------------------------- /examples/custom/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './app.js', 5 | 6 | output: { 7 | path: path.resolve('.'), 8 | filename: 'bundle.js', 9 | library: 'ReactFormBuilder', 10 | libraryTarget: 'umd' 11 | }, 12 | 13 | externals: { 14 | 'bootstrap': 'bootstrap' 15 | }, 16 | 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.scss', '.css', '.json'], 19 | alias: { 20 | "jquery": path.join(__dirname, "./jquery-stub.js") 21 | } 22 | }, 23 | 24 | module: { 25 | rules: [ 26 | { 27 | exclude: /node_modules/, 28 | test: /\.js|.jsx?$/, 29 | use: [ 30 | { loader: 'babel-loader' } 31 | ] 32 | }, { 33 | test: /\.css$/, 34 | use: [ 35 | { 36 | loader: 'style-loader' 37 | }, 38 | { 39 | loader: 'css-loader' 40 | }, 41 | ] 42 | } 43 | ] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /scss/react-star-rating.scss: -------------------------------------------------------------------------------- 1 | .rating-container, .react-star-rating__root{ 2 | vertical-align:middle;display:inline-block 3 | } 4 | .rating-container .rating-stars:before,.rating-container:before{content:attr(data-content)}.react-star-rating__star{width:25px}.react-star-rating__star #star-flat{fill:#C6C6C6}.react-star-rating__star #star-flat:hover{fill:#FFA91B}.react-star-rating__root{font-size:2em}.react-star-rating__root.rating-editing:hover{cursor:pointer}.rating-container{position:relative;color:#e3e3e3;overflow:hidden}.rating-container .rating-stars{position:absolute;left:0;top:0;white-space:nowrap;overflow:hidden;color:#F5A71B;-webkit-transition:all .01s;-moz-transition:all .01s;transition:all .01s;-webkit-mask-image:-webkit-gradient(linear,left top,left bottom,from(#FDBD47),to(#F5A71B))}.react-rating-caption{font-size:1.25em;vertical-align:middle;margin-right:.5em}.rating-disabled .rating-container:hover{cursor:not-allowed}.react-star-rating__size--sm{font-size:1em}.react-star-rating__size--md{font-size:2em}.react-star-rating__size--lg{font-size:2.5em} -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb-base"], 4 | "plugins": [ 5 | "react" 6 | ], 7 | "globals": { 8 | "window": true, 9 | "document": true, 10 | "navigator": true 11 | }, 12 | "rules": { 13 | "max-len": [1, 180, 2, {"ignoreComments": true}], 14 | "semi": [2, "always"], 15 | "comma-dangle": "warn", 16 | "camelcase": "warn", 17 | "prefer-destructuring": "warn", 18 | "no-param-reassign": "warn", 19 | "linebreak-style": 0, 20 | "operator-linebreak": "off", 21 | "no-underscore-dangle": "off", 22 | "class-methods-use-this": "off", 23 | "arrow-parens": "off", 24 | "indent": "off", 25 | "no-plusplus": "off", 26 | "no-return-assign": "off", 27 | "object-curly-newline": "off", 28 | "no-prototype-builtins": "warn", 29 | "react/jsx-uses-vars": [2], 30 | "react/jsx-uses-react": 1 31 | }, 32 | "settings": { 33 | "import/resolver": { 34 | "node": { 35 | "extensions": [".js", ".jsx"] 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /examples/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Form Builder Example 5 | 6 | 8 | 27 | 28 | 29 | 30 | 34 |
    35 |
    36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './app.js', 5 | 6 | devtool: 'source-map', 7 | 8 | output: { 9 | path: path.resolve('.'), 10 | filename: 'bundle.js', 11 | library: 'ReactFormBuilder', 12 | libraryTarget: 'umd' 13 | }, 14 | 15 | externals: { 16 | 'bootstrap': 'bootstrap' 17 | }, 18 | 19 | resolve: { 20 | extensions: ['.js', '.jsx', '.scss', '.css', '.json'], 21 | alias: { 22 | "jquery": path.join(__dirname, "./jquery-stub.js") 23 | } 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { 29 | exclude: /node_modules/, 30 | test: /\.js|.jsx?$/, 31 | use: [ 32 | { loader: 'babel-loader' } 33 | ] 34 | }, { 35 | test: /\.css$/, 36 | use: [ 37 | { 38 | loader: 'style-loader' 39 | }, 40 | { 41 | loader: 'css-loader' 42 | }, 43 | ] 44 | } 45 | ] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Form Builder Example 5 | 6 | 8 | 27 | 28 | 29 | 30 | 34 |
    35 |
    36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/form-place-holder.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { injectIntl } from 'react-intl'; 4 | 5 | const PLACE_HOLDER = 'form-place-holder'; 6 | const PLACE_HOLDER_HIDDEN = 'form-place-holder-hidden'; 7 | 8 | class PlaceHolder extends React.Component { 9 | render() { 10 | const { intl } = this.props; 11 | const placeHolderClass = this.props.show ? PLACE_HOLDER : PLACE_HOLDER_HIDDEN; 12 | // eslint-disable-next-line no-nested-ternary 13 | const placeHolder = this.props.show ? 14 | (this.props.text === 'Dropzone' ? intl.formatMessage({ id: 'drop-zone' }) : this.props.text) : ''; 15 | 16 | return ( 17 |
    18 |
    {placeHolder}
    19 |
    20 | ); 21 | } 22 | } 23 | 24 | export default injectIntl(PlaceHolder); 25 | PlaceHolder.propTypes = { 26 | text: PropTypes.string, 27 | show: PropTypes.bool, 28 | }; 29 | 30 | PlaceHolder.defaultProps = { 31 | text: 'Dropzone', 32 | show: false, 33 | }; 34 | -------------------------------------------------------------------------------- /examples/mongo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form-next", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon server/index.js", 9 | "build": "next build", 10 | "start": "node server/index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@zeit/next-css": "^1.0.1", 17 | "@zeit/next-sass": "^1.0.1", 18 | "body-parser": "^1.18.3", 19 | "classnames": "^2.2.5", 20 | "co": "^4.6.0", 21 | "es6-promise": "^4.2.4", 22 | "express": "^4.16.4", 23 | "isomorphic-fetch": "^2.2.1", 24 | "jquery": "^3.3.1", 25 | "mongodb": "^3.2.3", 26 | "multer": "^1.4.1", 27 | "next": "^8.0.3", 28 | "next-routes": "^1.4.2", 29 | "node-sass": "^4.11.0", 30 | "react": "^16.8.6", 31 | "react-dom": "^16.8.6", 32 | "@threehippies/react-form-builder": "^0.20.1", 33 | "react-draft-wysiwyg": "1.13.2" 34 | }, 35 | "devDependencies": { 36 | "nodemon": "^1.18.10" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Form Builder Example 5 | 6 | 8 | 9 | 28 | 29 | 30 | 31 | 35 |
    36 |
    37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/multi-column/grip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DragSource } from 'react-dnd'; 3 | import ItemTypes from '../ItemTypes'; 4 | 5 | const style = { 6 | // display: 'inline-block', 7 | // border: '1px dashed gray', 8 | // padding: '0.5rem 1rem', 9 | // backgroundColor: 'white', 10 | cursor: 'move', 11 | }; 12 | 13 | const gripSource = { 14 | beginDrag(props) { 15 | const { 16 | data, index, onDestroy, setAsChild, getDataById, 17 | } = props; 18 | return { 19 | itemType: ItemTypes.BOX, 20 | index: data.parentId ? -1 : index, 21 | parentIndex: data.parentIndex, 22 | id: data.id, 23 | col: data.col, 24 | onDestroy, 25 | setAsChild, 26 | getDataById, 27 | data, 28 | }; 29 | }, 30 | }; 31 | 32 | const Grip = ({ connectDragSource }) => connectDragSource( 33 |
    , 34 | ); 35 | 36 | export default DragSource( 37 | ItemTypes.BOX, 38 | gripSource, 39 | (connect) => ({ 40 | connectDragSource: connect.dragSource(), 41 | }), 42 | )(Grip); 43 | -------------------------------------------------------------------------------- /src/form-elements/component-drag-preview.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | /* eslint-disable import/prefer-default-export */ 3 | /* eslint-disable no-unused-vars */ 4 | 5 | import React, { useState, useEffect } from 'react'; 6 | 7 | const boxStyles = { 8 | border: '1px dashed gray', 9 | padding: '0.5rem 1rem', 10 | cursor: 'move', 11 | }; 12 | 13 | const styles = { 14 | display: 'inline-block', 15 | transform: 'rotate(-7deg)', 16 | WebkitTransform: 'rotate(-7deg)', 17 | }; 18 | 19 | const Box = ({ title, color }) => { 20 | const backgroundColor = color ? '#059862' : 'white'; 21 | return
    {title}
    ; 22 | }; 23 | 24 | export const BoxDragPreview = ({ item }) => { 25 | const [tickTock, setTickTock] = useState(false); 26 | 27 | const text = item.data.content 28 | ? item.data.content 29 | : item.data.label 30 | ? item.data.label 31 | : item.data.text; 32 | const isLongText = text.length > 20; 33 | const previewText = isLongText ? `${text.slice(0, 20)}...` : text; 34 | 35 | return ( 36 |
    37 | 38 |
    39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/stores/registry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | function regg() { 3 | const registry = {}; 4 | 5 | const self = { 6 | register, 7 | list, 8 | get, 9 | }; 10 | 11 | /** 12 | * Registers a entry and make it read only 13 | * @param {String} name To get the entry by 14 | * @param {Object} entry What you want to register 15 | */ 16 | function register(name, entry) { 17 | if (!name) { 18 | throw new Error('You must provide a valid name for this entry.'); 19 | } 20 | 21 | if (registry[name] !== undefined) { 22 | throw new Error(`'${name}' already registered`); 23 | } 24 | 25 | if (!entry) { 26 | throw new Error(`You must provide something to register as '${name}'`); 27 | } 28 | 29 | registry[name] = entry; 30 | 31 | return self; 32 | } 33 | 34 | function get(name) { 35 | // eslint-disable-next-line no-prototype-builtins 36 | if (!registry.hasOwnProperty(name)) { 37 | console.error(`No such entry '${name}'`); 38 | } 39 | return registry[name]; 40 | } 41 | 42 | function list() { 43 | return Object.keys(registry); 44 | } 45 | 46 | return self; 47 | } 48 | 49 | module.exports = regg(); 50 | -------------------------------------------------------------------------------- /examples/mongo/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App, { Container } from 'next/app'; 3 | import Head from 'next/head'; 4 | 5 | // stylings 6 | import '@threehippies/react-form-builder/dist/app.css'; 7 | import '../styles/site.css'; 8 | 9 | class MyApp extends App { 10 | static async getInitialProps({ Component, ctx }) { 11 | let pageProps = {}; 12 | 13 | if (Component.getInitialProps) { 14 | pageProps = await Component.getInitialProps(ctx); 15 | } 16 | 17 | return { pageProps }; 18 | } 19 | 20 | render() { 21 | const { Component, pageProps } = this.props; 22 | 23 | return ( 24 | 25 | 26 | 30 | 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default MyApp; 44 | -------------------------------------------------------------------------------- /src/form-elements/header-bar.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | import React from 'react'; 6 | // import Grip from '../multi-column/grip'; 7 | import DragHandle from './component-drag-handle'; 8 | 9 | export default class HeaderBar extends React.Component { 10 | render() { 11 | return ( 12 |
    13 | {this.props.data.text} 14 |
    15 | {this.props.data.element !== 'LineBreak' && ( 16 |
    23 | 24 |
    25 | )} 26 |
    30 | 31 |
    32 | 33 | 39 |
    40 |
    41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/form-elements/component-drag-handle.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDrag } from 'react-dnd'; 3 | import { getEmptyImage } from 'react-dnd-html5-backend'; 4 | import ItemTypes from '../ItemTypes'; 5 | 6 | const style = { 7 | cursor: 'move', 8 | }; 9 | 10 | const DragHandle = (props) => { 11 | const { data, index, onDestroy, setAsChild, getDataById } = props; 12 | 13 | const [, dragRef, dragPreviewRef] = useDrag({ 14 | type: ItemTypes.BOX, 15 | item: () => ({ 16 | itemType: ItemTypes.BOX, 17 | index: data.parentId ? -1 : index, 18 | parentIndex: data.parentIndex, 19 | id: data.id, 20 | col: data.col, 21 | onDestroy, 22 | setAsChild, 23 | getDataById, 24 | data, 25 | }), 26 | collect: (monitor) => ({ 27 | isDragging: monitor.isDragging(), 28 | }), 29 | }); 30 | 31 | // Use empty image as drag preview 32 | useEffect(() => { 33 | dragPreviewRef(getEmptyImage(), { 34 | // IE fallback: specify that we'd rather screenshot the node 35 | // when it already knows it's being dragged so we can hide it with CSS. 36 | captureDraggingState: true, 37 | }); 38 | }, [dragPreviewRef]); 39 | 40 | return ( 41 |
    42 | 43 |
    44 | ); 45 | }; 46 | 47 | export default DragHandle; 48 | -------------------------------------------------------------------------------- /examples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfb2-example", 3 | "version": "0.1.0", 4 | "description": "A complete form builder for react.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "react", 8 | "react-component", 9 | "form", 10 | "builder", 11 | "ui", 12 | "drag", 13 | "drop" 14 | ], 15 | "author": "Jason Kadrmas", 16 | "dependencies": { 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "@threehippies/react-form-builder": "^0.20.1" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.0.0", 23 | "@babel/core": "^7.0.0", 24 | "@babel/plugin-proposal-class-properties": "^7.0.0", 25 | "@babel/plugin-proposal-json-strings": "^7.0.0", 26 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 27 | "@babel/plugin-syntax-import-meta": "^7.0.0", 28 | "@babel/preset-env": "^7.0.0", 29 | "@babel/preset-react": "^7.0.0", 30 | "@babel/runtime-corejs2": "^7.0.0", 31 | "babel-eslint": "^10.0.1", 32 | "babel-loader": "^8.0.0", 33 | "css-loader": "^0.28.10", 34 | "live-server": "1.2.1", 35 | "style-loader": "^0.20.2", 36 | "uglifyjs-webpack-plugin": "^1.3.0", 37 | "webpack": "^4.0.0", 38 | "webpack-cli": "^3.1.0", 39 | "webpack-dev-server": "^3.1.5" 40 | }, 41 | "scripts": { 42 | "build": "webpack", 43 | "start": "live-server --cors --port=8007 --proxy=/api:http://127.0.0.1:5005/api --watch=./bundle.js,./index.html ." 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/next/server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable object-shorthand */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable func-names */ 4 | /* eslint-disable prefer-arrow-callback */ 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | /* eslint-disable no-var */ 7 | const express = require('express'); 8 | const next = require('next'); 9 | const bodyParser = require('body-parser'); 10 | const api = require('./api/routes'); 11 | const handleForm = require('./api/form'); 12 | const formData = require('./api/formData'); 13 | 14 | const PORT = process.env.PORT || 3000; 15 | const dev = process.env.NODE_DEV !== 'production'; // true false 16 | const nextApp = next({ dev }); 17 | const handle = nextApp.getRequestHandler(); // part of next config 18 | 19 | global.navigator = { 20 | userAgent: 'node.js', 21 | }; 22 | 23 | nextApp.prepare().then(() => { 24 | // express code here 25 | const app = express(); 26 | 27 | app.use(bodyParser.json()); 28 | app.use(bodyParser.urlencoded({ extended: true })); 29 | app.use('/api/', api); 30 | 31 | app.route('/api/form/') 32 | .get((req, res) => { 33 | res.send(formData.answers); 34 | }) 35 | .post(handleForm); 36 | 37 | app.get('*', (req, res) => { 38 | return handle(req, res); // for all the react stuff 39 | }); 40 | 41 | app.listen(PORT, err => { 42 | if (err) throw err; 43 | console.log(`ready at http://localhost:${PORT}`); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/custom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Form Builder Example 4 | 8 | 14 | 19 | 44 | 45 | 46 | 47 | 51 |
    52 |
    53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfb2-example-custom", 3 | "version": "0.1.0", 4 | "description": "A complete form builder for react.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "react", 8 | "react-component", 9 | "form", 10 | "builder", 11 | "ui", 12 | "drag", 13 | "drop" 14 | ], 15 | "author": "Kiho Chang", 16 | "dependencies": { 17 | "isomorphic-fetch": "^2.2.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "@threehippies/react-form-builder": "^0.20.1" 21 | }, 22 | "devDependencies": { 23 | "@babel/cli": "^7.0.0", 24 | "@babel/core": "^7.0.0", 25 | "@babel/plugin-proposal-class-properties": "^7.0.0", 26 | "@babel/plugin-proposal-json-strings": "^7.0.0", 27 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 28 | "@babel/plugin-syntax-import-meta": "^7.0.0", 29 | "@babel/preset-env": "^7.0.0", 30 | "@babel/preset-react": "^7.0.0", 31 | "@babel/runtime-corejs2": "^7.0.0", 32 | "babel-eslint": "^10.0.1", 33 | "babel-loader": "^8.0.0", 34 | "css-loader": "^0.28.10", 35 | "live-server": "1.2.1", 36 | "style-loader": "^0.20.2", 37 | "uglifyjs-webpack-plugin": "^1.3.0", 38 | "webpack": "^4.0.0", 39 | "webpack-cli": "^3.1.0", 40 | "webpack-dev-server": "^3.1.5" 41 | }, 42 | "scripts": { 43 | "build": "webpack", 44 | "build:dev": "webpack --mode development", 45 | "start": "live-server --cors --port=8007 --proxy=/api:http://127.0.0.1:5005/api --watch=./bundle.js,./index.html ." 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/mongo/server/api/formData.js: -------------------------------------------------------------------------------- 1 | const formData = require('./dummyFormData.json'); 2 | 3 | async function saveFormData(db, doc) { 4 | const o = await db.collection('form').findOne({ id: doc.id }); 5 | if (o != null) { 6 | // console.log('update: ', doc); 7 | // eslint-disable-next-line no-param-reassign 8 | delete doc._id; 9 | db.collection('form').findOneAndUpdate({ _id: o._id }, 10 | { $set: doc }, 11 | { upsert: false }, 12 | (err) => { 13 | if (err) console.log('findOneAndUpdate error: ', err); 14 | }); 15 | } else { 16 | // console.log('insert: ', doc); 17 | db.collection('form').insertOne(doc); 18 | } 19 | } 20 | 21 | async function loadFormData(db) { 22 | return db.collection('form').find().toArray(); 23 | } 24 | 25 | async function deleteFormData(db, doc) { 26 | db.collection('form').deleteOne({ id: doc.id }); 27 | } 28 | 29 | function findRemovedItem(currentArray, previousArray) { 30 | return previousArray.filter(x => { 31 | const z = currentArray.find(y => y.id === x.id); 32 | return !currentArray.includes(z); 33 | }); 34 | } 35 | 36 | async function save(db, taskData) { 37 | const oldData = await loadFormData(db); 38 | const missing = findRemovedItem(taskData, oldData); 39 | if (missing.length) { 40 | missing.forEach(doc => deleteFormData(db, doc)); 41 | } else { 42 | taskData.forEach(doc => saveFormData(db, doc)); 43 | } 44 | } 45 | 46 | module.exports = { 47 | data: formData, 48 | answers: {}, 49 | save, 50 | load: loadFormData, 51 | }; 52 | -------------------------------------------------------------------------------- /src/form-elements/component-drag-layer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDragLayer } from 'react-dnd'; 3 | import ItemTypes from '../ItemTypes'; 4 | import { BoxDragPreview } from './component-drag-preview'; 5 | 6 | const layerStyles = { 7 | position: 'fixed', 8 | pointerEvents: 'none', 9 | zIndex: 100, 10 | left: 0, 11 | top: 0, 12 | width: '100%', 13 | height: '100%', 14 | }; 15 | 16 | function getItemStyles(initialOffset, currentOffset) { 17 | if (!initialOffset || !currentOffset) { 18 | return { 19 | display: 'none', 20 | }; 21 | } 22 | 23 | const { x, y } = currentOffset; 24 | const transform = `translate(${x}px, ${y}px)`; 25 | 26 | return { 27 | transform, 28 | WebkitTransform: transform, 29 | }; 30 | } 31 | 32 | const CustomDragLayer = () => { 33 | const { itemType, isDragging, item, initialOffset, currentOffset } = 34 | useDragLayer((monitor) => ({ 35 | item: monitor.getItem(), 36 | itemType: monitor.getItemType(), 37 | initialOffset: monitor.getInitialSourceClientOffset(), 38 | currentOffset: monitor.getSourceClientOffset(), 39 | isDragging: monitor.isDragging(), 40 | })); 41 | 42 | function renderItem() { 43 | switch (itemType) { 44 | case ItemTypes.BOX: 45 | return ; 46 | default: 47 | return null; 48 | } 49 | } 50 | 51 | if (!isDragging) { 52 | return null; 53 | } 54 | 55 | return ( 56 |
    57 |
    58 | {renderItem()} 59 |
    60 |
    61 | ); 62 | }; 63 | 64 | export default CustomDragLayer; 65 | -------------------------------------------------------------------------------- /scss/form-builder-form.scss: -------------------------------------------------------------------------------- 1 | input[type="date"]:before { 2 | content: attr(placeholder); 3 | color: #aaa; 4 | margin-right: 0.5em; 5 | } 6 | 7 | input[type="date"] { 8 | width: 200px; 9 | } 10 | .validation-error { 11 | position: fixed; 12 | width: 100%; 13 | top: 0; 14 | left: 0; 15 | margin: 0; 16 | z-index: 99999999; 17 | ul { 18 | width: auto; 19 | } 20 | .dismiss-modal-button { 21 | margin-top: 10px; 22 | } 23 | } 24 | .react-form-builder-form { 25 | position: relative; 26 | 27 | .rfb-item.alwaysbreak { page-break-before: always; } 28 | .rfb-item.nobreak:before { clear:both; } 29 | .rfb-item.nobreak { page-break-inside: avoid; } 30 | 31 | .rfb-item { 32 | padding: 10px 0; 33 | position: relative; 34 | 35 | img { 36 | max-width: 100%; 37 | } 38 | label { 39 | font-weight: normal; 40 | } 41 | 42 | .bold { 43 | font-weight: bold; 44 | } 45 | .italic { 46 | font-style: italic; 47 | } 48 | .form-label { 49 | display: block !important; 50 | } 51 | 52 | .form-group { 53 | .option-inline { 54 | display: inline-block !important; 55 | margin-right: 10px; 56 | } 57 | 58 | a { 59 | cursor: pointer; 60 | } 61 | input[type='date'] { 62 | height: 42px; 63 | } 64 | .m-signature-pad { 65 | position: relative; 66 | width: auto; 67 | } 68 | .react-star-rating { 69 | display: block; 70 | } 71 | .checkbox-label, .radio-label { 72 | font-weight: normal; 73 | display: block; 74 | } 75 | .label-required { 76 | margin-left: 5px; 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/form-elements/custom-element.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ComponentHeader from './component-header'; 3 | import ComponentLabel from './component-label'; 4 | 5 | class CustomElement extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.inputField = React.createRef(); 9 | } 10 | 11 | render() { 12 | const { bare } = this.props.data; 13 | const props = {}; 14 | props.name = this.props.data.field_name; 15 | props.defaultValue = this.props.defaultValue; 16 | 17 | if (this.props.mutable && this.props.data.forwardRef) { 18 | props.ref = this.inputField; 19 | } 20 | 21 | if (this.props.read_only) { 22 | props.disabled = 'disabled'; 23 | } 24 | 25 | // Return if component is invalid. 26 | if (!this.props.data.component) return null; 27 | const Element = this.props.data.component; 28 | 29 | let baseClasses = 'SortableItem rfb-item'; 30 | if (this.props.data.pageBreakBefore) { 31 | baseClasses += ' alwaysbreak'; 32 | } 33 | 34 | return ( 35 |
    36 | 37 | {bare ? ( 38 | 43 | ) : ( 44 |
    45 | 46 | 51 |
    52 | )} 53 |
    54 | ); 55 | } 56 | } 57 | 58 | CustomElement.propTypes = {}; 59 | 60 | export default CustomElement; 61 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | 4 | module.exports = { 5 | entry: "./app.js", 6 | devtool: "source-map", 7 | output: { 8 | path: path.resolve("./public"), 9 | filename: "app.js", 10 | }, 11 | resolve: { 12 | extensions: [".js", ".jsx", ".scss", ".css", ".json"], 13 | alias: { 14 | jquery: path.join(__dirname, "./jquery-stub.js"), 15 | }, 16 | }, 17 | plugins: [ 18 | // 19 | ], 20 | 21 | module: { 22 | rules: [ 23 | { 24 | exclude: /node_modules/, 25 | test: /\.js$|.jsx?$/, 26 | use: [{ loader: "babel-loader" }], 27 | }, 28 | { 29 | test: /\.scss$/, 30 | use: [ 31 | { 32 | loader: "style-loader", 33 | }, 34 | { 35 | loader: "css-loader", 36 | }, 37 | { 38 | loader: "sass-loader", 39 | options: { 40 | sassOptions: { 41 | includePaths: ["./node_modules"], 42 | }, 43 | }, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | devServer: { 50 | port: 8080, 51 | host: "localhost", 52 | historyApiFallback: true, 53 | headers: { 54 | "Access-Control-Allow-Origin": "*", 55 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", 56 | "Access-Control-Allow-Headers": 57 | "X-Requested-With, content-type, Authorization", 58 | }, 59 | watchFiles: { 60 | paths: ["**/*"], 61 | options: { 62 | aggregateTimeout: 300, 63 | polling: 1000, 64 | }, 65 | }, 66 | static: { 67 | directory: "./public", 68 | }, 69 | open: true, 70 | proxy: [ 71 | { 72 | context: ["/api/*"], 73 | target: "http://127.0.0.1:5005", 74 | }, 75 | ], 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /src/UUID.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Private array of chars to use 3 | var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 4 | var ID = {}; 5 | ID.uuid = function (len, radix) { 6 | var chars = CHARS, uuid = [], i; 7 | radix = radix || chars.length; 8 | 9 | if (len) { 10 | // Compact form 11 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; 12 | } else { 13 | // rfc4122, version 4 form 14 | var r; 15 | 16 | // rfc4122 requires these characters 17 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; 18 | uuid[14] = '4'; 19 | 20 | // Fill in random data. At i==19 set the high bits of clock sequence as 21 | // per rfc4122, sec. 4.1.5 22 | for (i = 0; i < 36; i++) { 23 | if (!uuid[i]) { 24 | r = 0 | Math.random()*16; 25 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 26 | } 27 | } 28 | } 29 | 30 | return uuid.join(''); 31 | }; 32 | 33 | // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance 34 | // by minimizing calls to random() 35 | ID.uuidFast = function() { 36 | var chars = CHARS, uuid = new Array(36), rnd=0, r; 37 | for (var i = 0; i < 36; i++) { 38 | if (i==8 || i==13 || i==18 || i==23) { 39 | uuid[i] = '-'; 40 | } else if (i==14) { 41 | uuid[i] = '4'; 42 | } else { 43 | if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; 44 | r = rnd & 0xf; 45 | rnd = rnd >> 4; 46 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; 47 | } 48 | } 49 | return uuid.join(''); 50 | }; 51 | 52 | // A more compact, but less performant, RFC4122v4 solution: 53 | ID.uuidCompact = function() { 54 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 55 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 56 | return v.toString(16); 57 | }); 58 | }; 59 | module.exports = ID; -------------------------------------------------------------------------------- /src/form-validator.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | import React from 'react'; 6 | import xss from 'xss'; 7 | import IntlMessages from './language-provider/IntlMessages'; 8 | 9 | const myxss = new xss.FilterXSS({ 10 | whiteList: { 11 | u: [], 12 | br: [], 13 | b: [], 14 | i: [], 15 | ol: ['style'], 16 | ul: ['style'], 17 | li: [], 18 | p: ['style'], 19 | sub: [], 20 | sup: [], 21 | div: ['style'], 22 | em: [], 23 | strong: [], 24 | span: ['style'], 25 | }, 26 | }); 27 | 28 | export default class FormValidator extends React.Component { 29 | constructor(props) { 30 | super(props); 31 | this.state = { 32 | errors: [], 33 | }; 34 | } 35 | 36 | componentDidMount() { 37 | this.subscription = this.props.emitter.addListener('formValidation', (errors) => { 38 | this.setState({ errors }); 39 | }); 40 | } 41 | 42 | componentWillUnmount() { 43 | this.subscription.remove(); 44 | } 45 | 46 | dismissModal(e) { 47 | e.preventDefault(); 48 | this.setState({ errors: [] }); 49 | } 50 | 51 | render() { 52 | const errors = this.state.errors.map((error, index) =>
  • ); 53 | 54 | return ( 55 |
    56 | { this.state.errors.length > 0 && 57 |
    58 |
    59 | 60 |
      61 | {errors} 62 |
    63 |
    64 |
    65 | 66 |
    67 |
    68 | } 69 |
    70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/index.jsx', 6 | 7 | output: { 8 | path: path.resolve('./dist'), 9 | filename: 'app.js', 10 | library: 'ReactFormBuilder', 11 | libraryTarget: 'umd', 12 | umdNamedDefine: true, 13 | }, 14 | 15 | externals: { 16 | //don't bundle the 'react' npm package with our bundle.js 17 | //but get it from a global 'React' variable 18 | 'react': { 19 | 'commonjs': 'react', 20 | 'commonjs2': 'react', 21 | 'amd': 'react', 22 | 'root': 'React' 23 | }, 24 | 'react-dom': { 25 | 'commonjs': 'react-dom', 26 | 'commonjs2': 'react-dom', 27 | 'amd': 'react-dom', 28 | 'root': 'ReactDOM' 29 | }, 30 | // 'react-datepicker': 'react-datepicker', 31 | // 'classnames': 'classnames', 32 | // 'jquery': 'jquery', 33 | 'bootstrap': 'bootstrap' 34 | }, 35 | 36 | resolve: { 37 | extensions: ['./mjs', '.js', '.jsx', '.scss', '.css', '.json'], 38 | alias: { 39 | "jquery": path.join(__dirname, "./jquery-stub.js") 40 | } 41 | }, 42 | 43 | module: { 44 | rules: [ 45 | { 46 | exclude: /node_modules/, 47 | test: /\.js$|.jsx?$/, 48 | use: [ 49 | { loader: 'babel-loader' } 50 | ] 51 | }, 52 | { 53 | test: /\.scss$/, 54 | use: [ 55 | { 56 | loader: 'style-loader' 57 | }, 58 | { 59 | loader: 'css-loader' 60 | }, 61 | { 62 | loader: 'sass-loader', options: { 63 | sassOptions: { 64 | includePaths: ['./node_modules'], 65 | }, 66 | } 67 | } 68 | ] 69 | }, 70 | ] 71 | }, 72 | performance: { 73 | hints: false 74 | } 75 | }; -------------------------------------------------------------------------------- /examples/mongo/server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | /* eslint-disable arrow-body-style */ 3 | /* eslint-disable object-shorthand */ 4 | /* eslint-disable no-unused-vars */ 5 | /* eslint-disable func-names */ 6 | /* eslint-disable prefer-arrow-callback */ 7 | /* eslint-disable import/no-extraneous-dependencies */ 8 | /* eslint-disable no-var */ 9 | const express = require('express'); 10 | const next = require('next'); 11 | const { MongoClient } = require('mongodb'); 12 | 13 | const bodyParser = require('body-parser'); 14 | const api = require('./api/routes'); 15 | const handleForm = require('./api/form'); 16 | const formData = require('./api/formData'); 17 | 18 | const PORT = process.env.PORT || 3000; 19 | const dev = process.env.NODE_DEV !== 'production'; // true false 20 | const nextApp = next({ dev }); 21 | const handle = nextApp.getRequestHandler(); // part of next config 22 | 23 | const MONGO_URL = 'mongodb://localhost:27017'; 24 | const dbName = 'test'; 25 | 26 | // Fix crash with react-datepicker 27 | global.navigator = { 28 | userAgent: 'node.js', 29 | }; 30 | 31 | MongoClient.connect(MONGO_URL, (err, client) => { 32 | console.log('Connected successfully to server', MONGO_URL); 33 | const db = client.db(dbName); 34 | nextApp.prepare().then(() => { 35 | // express code here 36 | const app = express(); 37 | app.use(bodyParser.json()); 38 | app.use(bodyParser.urlencoded({ extended: true })); 39 | 40 | app.use((req, res, next) => { 41 | // Also expose the MongoDB database handle so Next.js can access it. 42 | req.db = db; 43 | next(); 44 | }); 45 | 46 | app.use('/api/', api); 47 | 48 | app.route('/api/form/') 49 | .get((req, res) => { 50 | res.send(formData.answers); 51 | }) 52 | .post(handleForm); 53 | 54 | app.get('*', (req, res) => 55 | // for all the react stuff 56 | // eslint-disable-next-line implicit-arrow-linebreak 57 | handle(req, res)); 58 | 59 | app.listen(PORT, err1 => { 60 | if (err1) throw err1; 61 | console.log(`ready at http://localhost:${PORT}`); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /examples/next/server/api/form.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-disable consistent-return */ 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | /* eslint-disable no-var */ 5 | var multer = require('multer'); 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var formData = require('./formData'); 9 | 10 | var extensions = ['.png', '.gif', '.jpg', '.jpeg']; 11 | var handleError = (err, res) => { 12 | res 13 | .status(500) 14 | .contentType('text/plain') 15 | .end('Oops! Something went wrong!'); 16 | }; 17 | var tempPath = path.join(__dirname, '../../public/temp'); 18 | var upload = multer({ dest: tempPath }).any(); 19 | 20 | var handleUpload = (req, res) => { 21 | upload(req, res, (error) => { 22 | if (error instanceof multer.MulterError) { 23 | // A Multer error occurred when uploading. 24 | console.log('multer.MulterError', error); 25 | } else if (error) { 26 | // An unknown error occurred when uploading. 27 | console.log('multer.MulterError', error); 28 | } else { 29 | formData.answers = req.body; 30 | const file = req.files && req.files.length ? req.files[0] : null; 31 | if (!file) { 32 | res.status(201).send(); 33 | return; 34 | } 35 | 36 | // TODO - handle multiple files 37 | const tempFilePath = file.path; 38 | const fieldname = file.fieldname; 39 | const targetPath = path.join(__dirname, '../../public/uploads'); 40 | const extn = path.extname(file.originalname).toLowerCase(); 41 | if (extensions.indexOf(extn) > -1) { 42 | const targetFilePath = path.join(targetPath, `${fieldname}${extn}`); 43 | fs.rename(tempFilePath, targetFilePath, err => { 44 | if (err) return handleError(err, res); 45 | formData.answers[fieldname] = `/uploads/${fieldname}${extn}`; 46 | res.status(201).send(); 47 | }); 48 | } else { 49 | fs.unlink(tempPath, err => { 50 | if (err) return handleError(err, res); 51 | res 52 | .status(403) 53 | .contentType('text/plain') 54 | .end('File type is not allowed!'); 55 | }); 56 | } 57 | } 58 | }); 59 | }; 60 | 61 | module.exports = handleUpload; 62 | -------------------------------------------------------------------------------- /docs/form-builder.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // const e = React.createElement; 3 | const FormBuilder = ReactFormBuilder.default.ReactFormBuilder; 4 | const domContainer = document.querySelector('#form-builder'); 5 | function guid() { 6 | function _p8(s) { 7 | const p = (`${Math.random().toString(16)}000000000`).substr(2, 8); 8 | return s ? `_${p.substr(0, 4)}_${p.substr(4, 4)}` : p; 9 | } 10 | return _p8() + _p8(true) + _p8(true) + _p8(); 11 | } 12 | 13 | let cid = window.localStorage.getItem('cid'); 14 | if (!cid) { 15 | cid = guid(); 16 | window.localStorage.setItem('cid', cid); 17 | } 18 | 19 | const url = `https://safe-springs-35306.herokuapp.com/api/formdata?cid=${cid}`; 20 | const saveUrl = `https://safe-springs-35306.herokuapp.com/api/formdata?cid=${cid}`; 21 | const saveAlways = false; 22 | 23 | ReactDOM.render(e(FormBuilder, { url, saveUrl, saveAlways }), domContainer); 24 | 25 | let backdropElement; 26 | const classBackdrop = 'modal-backdrop'; 27 | 28 | function showBackdrop() { 29 | if (!backdropElement) { 30 | backdropElement = document.createElement('div'); 31 | backdropElement.className = `${classBackdrop} show`; 32 | document.body.appendChild(backdropElement); 33 | } 34 | } 35 | 36 | function destroyBackdrop() { 37 | if (backdropElement) { 38 | backdropElement.parentNode.removeChild(backdropElement); 39 | } 40 | } 41 | 42 | let show = false; 43 | 44 | function clearMessage() { 45 | destroyBackdrop(); 46 | toastr.clear(); 47 | show = false; 48 | } 49 | 50 | function errorMessage() { 51 | show = false; 52 | toastr.error('Service Not Available.', 'Back-End', { timeOut: 30000 }); 53 | } 54 | 55 | const headers = { 56 | Accept: 'application/json', 57 | 'Content-Type': 'application/json; charset=utf-8', 58 | OPTIONS: '', 59 | }; 60 | 61 | function checkBackEnd() { 62 | show = false; 63 | setTimeout(() => { 64 | if (show) showBackdrop(); 65 | }, 300); 66 | setTimeout(() => { 67 | if (show) toastr.warning('Loading.... Please Wait.'); 68 | }, 1000); 69 | fetch(url, { 70 | method: 'GET', 71 | headers, 72 | }) 73 | .then(clearMessage) 74 | .catch(errorMessage); 75 | } 76 | 77 | checkBackEnd(); 78 | -------------------------------------------------------------------------------- /server/api/form.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-disable consistent-return */ 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | /* eslint-disable no-var */ 5 | var multer = require('multer'); 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var formData = require('./formData'); 9 | 10 | var extensions = ['.png', '.gif', '.jpg', '.jpeg']; 11 | var handleError = (err, res) => { 12 | console.log(err); 13 | res 14 | .status(500) 15 | .contentType('text/plain') 16 | .end('Oops! Something went wrong!'); 17 | }; 18 | var tempPath = path.join(__dirname, '../../public/temp'); 19 | var upload = multer({ dest: tempPath }).any(); 20 | 21 | var handleUpload = (req, res) => { 22 | upload(req, res, (error) => { 23 | if (error instanceof multer.MulterError) { 24 | // A Multer error occurred when uploading. 25 | console.log('multer.MulterError', error); 26 | } else if (error) { 27 | // An unknown error occurred when uploading. 28 | console.log('multer.MulterError', error); 29 | } else { 30 | formData.answers = req.body; 31 | const file = req.files[0]; 32 | if (!file) { 33 | res.redirect('/api/form'); 34 | return; 35 | } 36 | 37 | // TODO - handle multiple files 38 | const tempFilePath = file.path; 39 | const fieldname = file.fieldname; 40 | const targetPath = path.join(__dirname, '../../public/uploads'); 41 | const extn = path.extname(file.originalname).toLowerCase(); 42 | if (extensions.indexOf(extn) > -1) { 43 | const targetFilePath = path.join(targetPath, `${fieldname}${extn}`); 44 | fs.rename(tempFilePath, targetFilePath, err => { 45 | if (err) return handleError(err, res); 46 | formData.answers[fieldname] = `/uploads/${fieldname}${extn}`; 47 | res.redirect('/api/form'); 48 | }); 49 | } else { 50 | console.log('File type is not allowed!'); 51 | fs.unlink(tempPath, err => { 52 | if (err) return handleError(err, res); 53 | res 54 | .status(403) 55 | .contentType('text/plain') 56 | .end('File type is not allowed!'); 57 | }); 58 | } 59 | } 60 | }); 61 | }; 62 | 63 | module.exports = handleUpload; 64 | -------------------------------------------------------------------------------- /src/sortable-form-elements.jsx: -------------------------------------------------------------------------------- 1 | import SortableElement from './sortable-element'; 2 | import PlaceHolder from './form-place-holder'; 3 | import BaseFormElements from './form-elements'; 4 | import { TwoColumnRow, ThreeColumnRow, MultiColumnRow } from './multi-column'; 5 | import { FieldSet } from './fieldset'; 6 | import CustomElement from './form-elements/custom-element'; 7 | 8 | const { 9 | Header, Paragraph, Label, LineBreak, TextInput, EmailInput, PhoneNumber, NumberInput, TextArea, Dropdown, Checkboxes, 10 | DatePicker, RadioButtons, Image, Rating, Tags, Signature, HyperLink, Download, Camera, Range, FileUpload, 11 | } = BaseFormElements; 12 | 13 | const FormElements = {}; 14 | 15 | FormElements.Header = SortableElement(Header); 16 | FormElements.Paragraph = SortableElement(Paragraph); 17 | FormElements.Label = SortableElement(Label); 18 | FormElements.LineBreak = SortableElement(LineBreak); 19 | FormElements.TextInput = SortableElement(TextInput); 20 | FormElements.EmailInput = SortableElement(EmailInput); 21 | FormElements.PhoneNumber = SortableElement(PhoneNumber); 22 | FormElements.NumberInput = SortableElement(NumberInput); 23 | FormElements.TextArea = SortableElement(TextArea); 24 | FormElements.Dropdown = SortableElement(Dropdown); 25 | FormElements.Signature = SortableElement(Signature); 26 | FormElements.Checkboxes = SortableElement(Checkboxes); 27 | FormElements.DatePicker = SortableElement(DatePicker); 28 | FormElements.RadioButtons = SortableElement(RadioButtons); 29 | FormElements.Image = SortableElement(Image); 30 | FormElements.Rating = SortableElement(Rating); 31 | FormElements.Tags = SortableElement(Tags); 32 | FormElements.HyperLink = SortableElement(HyperLink); 33 | FormElements.Download = SortableElement(Download); 34 | FormElements.Camera = SortableElement(Camera); 35 | FormElements.FileUpload = SortableElement(FileUpload); 36 | FormElements.Range = SortableElement(Range); 37 | FormElements.PlaceHolder = SortableElement(PlaceHolder); 38 | FormElements.FieldSet = SortableElement(FieldSet); 39 | FormElements.TwoColumnRow = SortableElement(TwoColumnRow); 40 | FormElements.ThreeColumnRow = SortableElement(ThreeColumnRow); 41 | FormElements.MultiColumnRow = SortableElement(MultiColumnRow); 42 | FormElements.CustomElement = SortableElement(CustomElement); 43 | 44 | export default FormElements; 45 | -------------------------------------------------------------------------------- /examples/mongo/server/api/form.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-disable consistent-return */ 3 | /* eslint-disable import/no-extraneous-dependencies */ 4 | /* eslint-disable no-var */ 5 | var multer = require('multer'); 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var formData = require('./formData'); 9 | 10 | var extensions = ['.png', '.gif', '.jpg', '.jpeg']; 11 | 12 | var handleError = (err, res) => { 13 | res 14 | .status(500) 15 | .contentType('text/plain') 16 | .end('Oops! Something went wrong!'); 17 | }; 18 | 19 | var tempPath = path.join(__dirname, '../../public/temp'); 20 | var upload = multer({ dest: tempPath }).any(); 21 | 22 | module.exports = (req, res) => { 23 | upload(req, res, (error) => { 24 | if (error instanceof multer.MulterError) { 25 | // A Multer error occurred when uploading. 26 | console.log('multer.MulterError', error); 27 | } else if (error) { 28 | // An unknown error occurred when uploading. 29 | console.log('multer.MulterError', error); 30 | } else { 31 | formData.answers = req.body; 32 | saveAnswers(req.db, req.body); 33 | const file = req.files && req.files.length ? req.files[0] : null; 34 | if (!file) { 35 | res.status(201).send(); 36 | return; 37 | } 38 | 39 | // TODO - handle multiple files 40 | const tempFilePath = file.path; 41 | const fieldname = file.fieldname; 42 | const targetPath = path.join(__dirname, '../../public/uploads'); 43 | const extn = path.extname(file.originalname).toLowerCase(); 44 | if (extensions.indexOf(extn) > -1) { 45 | const targetFilePath = path.join(targetPath, `${fieldname}${extn}`); 46 | fs.rename(tempFilePath, targetFilePath, err => { 47 | if (err) return handleError(err, res); 48 | formData.answers[fieldname] = `/uploads/${fieldname}${extn}`; 49 | res.status(201).send(); 50 | }); 51 | } else { 52 | fs.unlink(tempPath, err => { 53 | if (err) return handleError(err, res); 54 | res 55 | .status(403) 56 | .contentType('text/plain') 57 | .end('File type is not allowed!'); 58 | }); 59 | } 60 | } 61 | }); 62 | }; 63 | 64 | function saveAnswers(db, answers) { 65 | if (answers && answers.length) { 66 | db.collection('answers').insertMany(answers); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/umd/form-generator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable prefer-destructuring */ 3 | /* eslint-disable no-undef */ 4 | // const e = React.createElement; 5 | const e = React.createElement; 6 | 7 | const ReactFormGenerator = ReactFormBuilder.ReactFormGenerator; 8 | const ElementStore = ReactFormBuilder.ElementStore; 9 | const formContainer = document.querySelector('#form-generator'); 10 | 11 | function setClass(element, name, remove) { 12 | if (typeof element === 'string') { 13 | element = document.querySelector(element); 14 | } 15 | if (remove) { 16 | element.classList.remove(name); 17 | } else { 18 | element.classList.add(name); 19 | } 20 | } 21 | 22 | class FormGenerator extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | data: [], 27 | previewVisible: false, 28 | }; 29 | 30 | this.showPreview = this.showPreview.bind(this); 31 | this.closePreview = this.closePreview.bind(this); 32 | this._onUpdate = this._onChange.bind(this); 33 | } 34 | 35 | componentDidMount() { 36 | ElementStore.subscribe(state => this._onUpdate(state.data)); 37 | document.querySelector('#button-preview') 38 | .addEventListener('click', this.showPreview); 39 | document.querySelector('#button-close') 40 | .addEventListener('click', this.closePreview); 41 | } 42 | 43 | showPreview() { 44 | this.setState({ 45 | previewVisible: true, 46 | }); 47 | setClass('#preview-dialog', 'show', false); 48 | setClass('#preview-dialog', 'd-block', false); 49 | } 50 | 51 | closePreview() { 52 | this.setState({ 53 | previewVisible: false, 54 | }); 55 | setClass('#preview-dialog', 'show', true); 56 | setClass('#preview-dialog', 'd-block', true); 57 | } 58 | 59 | _onChange(data) { 60 | this.setState({ 61 | data, 62 | }); 63 | } 64 | 65 | render() { 66 | const previewVisible = this.state.previewVisible; 67 | if (!previewVisible) { 68 | return null; 69 | } 70 | return e( 71 | ReactFormGenerator, { 72 | download_path: '', 73 | back_action: '/', 74 | back_name: 'Back', 75 | answer_data: {}, 76 | action_name: 'Save', 77 | form_action: '/', 78 | form_method: 'POST', 79 | variables: this.props.variables, 80 | data: this.state.data, 81 | }, 82 | ); 83 | } 84 | } 85 | 86 | ReactDOM.render(e(FormGenerator), formContainer); 87 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable object-shorthand */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable func-names */ 4 | /* eslint-disable prefer-arrow-callback */ 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | /* eslint-disable no-var */ 7 | var express = require('express'); 8 | var bodyParser = require('body-parser'); 9 | var path = require('path'); 10 | var api = require('./api/routes'); 11 | var handleForm = require('./api/form'); 12 | var formData = require('./api/formData'); 13 | 14 | var mode = process.env.NODE_ENV || 'development'; 15 | const isProduction = mode === 'production'; 16 | 17 | var app = express(); 18 | 19 | // set the view engine to ejs 20 | app.set('views', path.join(__dirname, '/views')); 21 | app.set('view engine', 'ejs'); 22 | // app.engine('ejs', require('ejs').renderFile); 23 | 24 | app.set('port', (process.env.PORT || isProduction ? 8080 : 5005)); 25 | 26 | app.use(bodyParser.json()); 27 | app.use(bodyParser.urlencoded({ 28 | extended: true, 29 | })); 30 | 31 | if (isProduction) { 32 | app.use(express.static(`${__dirname}/../dist`)); 33 | } 34 | 35 | app.use('/api/', api); 36 | 37 | function fixLabelLink(data) { 38 | const task_data = data.task_data.map(x => ({ ...x })); 39 | for (let i = 0; i < task_data.length; i++) { 40 | if (data.task_data[i].label) { 41 | task_data[i].label = task_data[i].label.replace(/"/g, '\\"'); 42 | } 43 | if (data.task_data[i].component) { 44 | task_data[i].component = null; 45 | } 46 | } 47 | return { task_data }; 48 | } 49 | 50 | app.route('/api/form/') 51 | .get((req, res) => { 52 | const data = fixLabelLink(formData.data); 53 | // console.log('get form: ', data); 54 | // console.log('get form answers: ', formData.answers); 55 | res.render('index', { 56 | data: JSON.stringify(data), 57 | answers: JSON.stringify(formData.answers), 58 | }); 59 | }) 60 | .post(handleForm); 61 | 62 | // console.log('NODE_ENV', process.env.NODE_ENV, `${__dirname}/../dist`); 63 | 64 | // 404 catch-all handler (middleware) 65 | app.use(function (req, res) { 66 | res.status(404); 67 | res.render('404'); 68 | }); 69 | 70 | // 500 error handler (middleware) 71 | app.use(function (err, req, res, next) { 72 | res.status(500); 73 | res.render('500', { error: err }); 74 | }); 75 | 76 | app.listen(app.get('port'), function () { 77 | console.log( 78 | `Express started on http://localhost:${app.get( 79 | 'port', 80 | )}; press Ctrl-C to terminate.`, 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /examples/cra/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/cra/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 16 | 17 | 18 | 22 | 23 | 32 | React App 33 | 44 | 45 | 46 | 47 | 48 |
    49 |
    50 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Form Builder - UMD 7 | 8 | 10 | 11 | 12 | 31 | 32 | 33 | 34 | 38 | 39 |
    40 | 41 |

    React Form Builder 2

    42 |
    43 | 44 | 56 |
    57 |
    58 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/umd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Form Builder - UMD 6 | 10 | 16 | 21 | 38 | 39 | 40 | 41 | 45 | 46 |
    47 |

    Preview

    48 | 55 | 74 |
    75 |
    76 | 77 | 78 | 79 | 83 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/form-generator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable prefer-destructuring */ 3 | /* eslint-disable no-undef */ 4 | const e = React.createElement; 5 | 6 | const ReactFormGenerator = ReactFormBuilder.default.ReactFormGenerator; 7 | const ElementStore = ReactFormBuilder.default.ElementStore; 8 | const formContainer = document.querySelector('#form-generator'); 9 | 10 | function setClass(element, name, remove) { 11 | if (typeof element === 'string') { 12 | element = document.querySelector(element); 13 | } 14 | if (remove) { 15 | element.classList.remove(name); 16 | } else { 17 | element.classList.add(name); 18 | } 19 | } 20 | 21 | class FormGenerator extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | data: [], 26 | previewVisible: false, 27 | }; 28 | 29 | this.showPreview = this.showPreview.bind(this); 30 | this.closePreview = this.closePreview.bind(this); 31 | this._onUpdate = this._onChange.bind(this); 32 | } 33 | 34 | componentDidMount() { 35 | ElementStore.subscribe(state => this._onUpdate(state.data)); 36 | document.querySelector('#button-preview') 37 | .addEventListener('click', this.showPreview); 38 | document.querySelector('#button-close') 39 | .addEventListener('click', this.closePreview); 40 | } 41 | 42 | saveFormData() { 43 | ElementStore.dispatch('post'); 44 | } 45 | 46 | showPreview() { 47 | this.saveFormData(); 48 | this.setState({ 49 | previewVisible: true, 50 | }); 51 | setClass('#preview-dialog', 'show', false); 52 | setClass('#preview-dialog', 'd-block', false); 53 | } 54 | 55 | closePreview() { 56 | this.setState({ 57 | previewVisible: false, 58 | }); 59 | setClass('#preview-dialog', 'show', true); 60 | setClass('#preview-dialog', 'd-block', true); 61 | } 62 | 63 | _onChange(data) { 64 | this.setState({ 65 | data, 66 | }); 67 | } 68 | 69 | render() { 70 | const previewVisible = this.state.previewVisible; 71 | if (!previewVisible) { 72 | return null; 73 | } 74 | const cid = window.localStorage.getItem('cid'); 75 | const query = cid ? `?cid=${cid}` : ''; 76 | const postUrl = `https://safe-springs-35306.herokuapp.com/api/form${query}`; 77 | return e( 78 | ReactFormGenerator, { 79 | download_path: '', 80 | back_action: '/react-form-builder/index.html', 81 | back_name: 'Back', 82 | answer_data: {}, 83 | action_name: 'Save', 84 | form_action: postUrl, 85 | form_method: 'POST', 86 | variables: this.props.variables, 87 | data: this.state.data, 88 | }, 89 | ); 90 | } 91 | } 92 | 93 | ReactDOM.render(e(FormGenerator), formContainer); 94 | -------------------------------------------------------------------------------- /src/language-provider/locales/vi-vn.json: -------------------------------------------------------------------------------- 1 | { 2 | "display-label": "Nhãn hiển thị", 3 | "choose-file": "Chọn tệp tin", 4 | "text-to-display": "Chữ hiển thị", 5 | "link-to": "Liên kết tới", 6 | "center": "Giữa", 7 | "width": "Độ rộng", 8 | "height": "Chiều cao", 9 | "required": "Bắt buộc nhập", 10 | "read-only": "Chỉ đọc", 11 | "default-to-today": "Mặc định hôm nay", 12 | "show-time-select": "Hiển thị chọn thời gian", 13 | "show-time-select-only": "Chỉ hiển thị thời gian", 14 | "show-time-input": "Hiển thị đầu vào thời gian", 15 | "display-horizontal": "Hiển thị theo chiều ngang", 16 | "variable-key": "Khóa tham chiếu", 17 | "variable-key-desc": "Tùy chọn này cung cấp từ khóa dùng để thay thế nội dung khi biểu mẫu hoạt động", 18 | "print-options": "Tùy chọn in ấn", 19 | "page-break-before-elements": "Phân trang trước thành phần điểu khiển", 20 | "alternate-signature-page": "Trang thay thế / ký", 21 | "display-on-alternate-signature-page": "Hiển thị trên trang thay thế / ký", 22 | "step": "Bước nhảy", 23 | "min": "Nhỏ nhất", 24 | "max": "Lớn nhất", 25 | "default-selected": "Mặc Định Chọn", 26 | "text-style": "Kiểu Chữ", 27 | "bold": "Đậm", 28 | "italic": "Nghiêng", 29 | "description": "Mô tả", 30 | "correct-answer": "Lựa chọn đúng", 31 | "populate-options-from-api": "Lấy dữ liệu từ API", 32 | "populate": "Thực hiện", 33 | 34 | "options" : "Tùy chọn", 35 | "value" : "Giá trị", 36 | "correct" : "Đúng", 37 | 38 | "dismiss" : "Bỏ qua", 39 | 40 | "place-holder-option-1" : "Lựa chọn 1", 41 | "place-holder-option-2" : "Lựa chọn 2", 42 | "place-holder-option-3" : "Lựa chọn 3", 43 | "place-holder-tag-1" : "Lựa chọn 1", 44 | "place-holder-tag-2" : "Lựa chọn 2", 45 | "place-holder-tag-3" : "Lựa chọn 3", 46 | 47 | "toolbox" : "Công cụ", 48 | "header-text" : "Tiêu đề", 49 | "label" : "Nhãn", 50 | "paragraph" : "Đoạn văn", 51 | "line-break" : "Đường kẻ dòng", 52 | "dropdown" : "Danh sách chọn", 53 | "tags": "Thẻ", 54 | "checkboxes" : "Hộp chọn", 55 | "multiple-choice":"Nhiều lựa chọn", 56 | "text-input":"Nhập chữ", 57 | "number-input":"Nhập số", 58 | "fieldset":"bộ trường", 59 | "multi-line-input":"Nhập nhiều dòng", 60 | "two-columns-row":"Dòng có hai cột", 61 | "three-columns-row":"Dòng có ba cột", 62 | "four-columns-row":"Dòng có bốn cột", 63 | "image":"Liên kết ảnh", 64 | "rating":"Đánh giá", 65 | "date":"Ngày", 66 | "signature":"Chữ kí", 67 | "website":"Trang web", 68 | "file-attachment":"Tệp đính kèm", 69 | "range":"Khoảng", 70 | "camera":"Tải ảnh", 71 | "place-holder-text": "Nội dung...", 72 | "place-holder-label": "Nhãn", 73 | "place-holder-website-link": "Đường dẫn tới website...", 74 | "place-holder-file-name": "Tên tệp...", 75 | "easy": "Easy", 76 | "difficult": "Difficult", 77 | 78 | "drop-zone": "Khu vực kéo thả", 79 | 80 | "message.is-required": "không được bỏ trống", 81 | "message.was-answered incorrectly": "đã trả lời sai", 82 | "message.was-not-registered": "chưa đăng ký" 83 | 84 | } -------------------------------------------------------------------------------- /examples/cra/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
    10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
    13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
    18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
    23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
    26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /examples/mongo/pages/form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactFormGenerator } from '@threehippies/react-form-builder'; 3 | import { get } from '../components/requests'; 4 | 5 | export default class Demobar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | // console.log(`Demobar: `, props); 9 | this.state = { 10 | data: props.data, 11 | answers: props.answers, 12 | roPreviewVisible: props.roPreviewVisible, 13 | }; 14 | } 15 | 16 | showRoPreview() { 17 | this.setState({ 18 | roPreviewVisible: true, 19 | }); 20 | } 21 | 22 | closePreview() { 23 | this.setState({ 24 | roPreviewVisible: false, 25 | }); 26 | } 27 | 28 | render() { 29 | const { answers, data } = this.state; 30 | 31 | let roModalClass = 'modal ro-modal'; 32 | if (this.state.roPreviewVisible) { 33 | roModalClass += ' show d-block'; 34 | } 35 | 36 | return ( 37 |
    38 |

    Preview

    39 | 46 | {this.state.roPreviewVisible && ( 47 |
    48 |
    49 |
    50 |
    51 | 64 |
    65 |
    66 | 74 |
    75 |
    76 |
    77 |
    78 | )} 79 |
    80 | ); 81 | } 82 | } 83 | 84 | // eslint-disable-next-line func-names 85 | Demobar.getInitialProps = async function ({ req }) { 86 | const hostUrl = `${req.protocol}://${req.headers.host}`; 87 | const url = `${hostUrl}/api/formdata`; 88 | const getUrl = `${hostUrl}/api/form`; 89 | const answers = await get(getUrl); 90 | const data = await get(url); 91 | return { 92 | data, 93 | answers, 94 | roPreviewVisible: true, 95 | }; 96 | }; 97 | -------------------------------------------------------------------------------- /src/multi-column/MultiColumnRow.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import React from 'react'; 3 | 4 | import ComponentHeader from '../form-elements/component-header'; 5 | import ComponentLabel from '../form-elements/component-label'; 6 | import Dustbin from './dustbin'; 7 | import ItemTypes from '../ItemTypes'; 8 | 9 | const accepts = [ItemTypes.BOX, ItemTypes.CARD]; 10 | 11 | class MultiColumnRowBase extends React.Component { 12 | render() { 13 | const { 14 | controls, data, editModeOn, getDataById, setAsChild, removeChild, seq, className, index, 15 | } = this.props; 16 | const { childItems, pageBreakBefore } = data; 17 | let baseClasses = 'SortableItem rfb-item'; 18 | if (pageBreakBefore) { baseClasses += ' alwaysbreak'; } 19 | 20 | return ( 21 |
    22 | 23 |
    24 | 25 |
    26 | {childItems.map((x, i) => ( 27 |
    { 28 | controls ? controls[i] : 29 | removeChild(data, i)} 38 | getDataById={getDataById} 39 | setAsChild={setAsChild} 40 | seq={seq} 41 | />} 42 |
    ))} 43 |
    44 |
    45 |
    46 | ); 47 | } 48 | } 49 | 50 | const TwoColumnRow = ({ data, class_name, ...rest }) => { 51 | const className = class_name || 'col-md-6'; 52 | if (!data.childItems) { 53 | // eslint-disable-next-line no-param-reassign 54 | data.childItems = [null, null]; data.isContainer = true; 55 | } 56 | return ( 57 | 58 | ); 59 | }; 60 | 61 | const ThreeColumnRow = ({ data, class_name, ...rest }) => { 62 | const className = class_name || 'col-md-4'; 63 | if (!data.childItems) { 64 | // eslint-disable-next-line no-param-reassign 65 | data.childItems = [null, null, null]; data.isContainer = true; 66 | } 67 | return ( 68 | 69 | ); 70 | }; 71 | 72 | const MultiColumnRow = ({ data, ...rest }) => { 73 | const colCount = data.col_count || 4; 74 | const className = data.class_name || (colCount === 4 ? 'col-md-3' : 'col'); 75 | if (!data.childItems) { 76 | // eslint-disable-next-line no-param-reassign 77 | data.childItems = Array.from({length: colCount}, (v, i) => null); 78 | data.isContainer = true; 79 | } 80 | return ; 81 | }; 82 | 83 | export { TwoColumnRow, ThreeColumnRow, MultiColumnRow }; -------------------------------------------------------------------------------- /examples/next/pages/form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactFormGenerator } from '@threehippies/react-form-builder'; 3 | import { get } from '../components/requests'; 4 | 5 | export default class Demobar extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | // console.log(`Demobar: `, props); 9 | this.state = { 10 | data: props.data, 11 | answers: props.answers, 12 | roPreviewVisible: props.roPreviewVisible, 13 | }; 14 | } 15 | 16 | showRoPreview() { 17 | this.setState({ 18 | roPreviewVisible: true, 19 | }); 20 | } 21 | 22 | closePreview() { 23 | this.setState({ 24 | roPreviewVisible: false, 25 | }); 26 | } 27 | 28 | render() { 29 | const { answers, data } = this.state; 30 | 31 | let roModalClass = 'modal ro-modal'; 32 | if (this.state.roPreviewVisible) { 33 | roModalClass += ' show d-block'; 34 | } 35 | 36 | return ( 37 |
    38 |

    Preview

    39 | 46 | {this.state.roPreviewVisible && ( 47 |
    48 |
    49 |
    50 |
    51 | 64 |
    65 |
    66 | 74 |
    75 |
    76 |
    77 |
    78 | )} 79 |
    80 | ); 81 | } 82 | } 83 | 84 | // eslint-disable-next-line func-names 85 | Demobar.getInitialProps = async function ({ req }) { 86 | const protocol = req.headers.referer.split('://')[0]; 87 | const hostUrl = `${protocol}://${req.headers.host}`; 88 | const url = `${hostUrl}/api/formdata`; 89 | const getUrl = `${hostUrl}/api/form`; 90 | const answers = await get(getUrl); 91 | const data = await get(url); 92 | return { 93 | data, 94 | answers, 95 | roPreviewVisible: true, 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /src/fieldset/FieldSet.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import React, { useEffect, useState } from "react"; 3 | 4 | import ComponentHeader from "../form-elements/component-header"; 5 | import ComponentLabel from "../form-elements/component-label"; 6 | import FieldsetDustbin from '../multi-column/dustbin'; 7 | import ItemTypes from "../ItemTypes"; 8 | 9 | const accepts = [ItemTypes.BOX, ItemTypes.CARD]; 10 | 11 | export default function FieldSetBase(props) { 12 | 13 | const [childData, setChildData] = useState({}); 14 | const [childItems, setChildItems] = useState(null); 15 | 16 | useEffect(() => { 17 | const { data, class_name, ...rest } = props; 18 | setChildData(data); 19 | let count=1; 20 | createChild(count, data); 21 | 22 | }, [props]); 23 | 24 | 25 | const addNewChild=()=>{ 26 | let data=props.data; 27 | let colCount=data.childItems.length+1; 28 | let oldChilds=data.childItems; 29 | data.childItems = Array.from({ length: colCount }, (v, i) => {return oldChilds[i]?oldChilds[i]:null}); 30 | 31 | setChildItems( data.childItems) 32 | } 33 | 34 | const onDropSuccess=(droppedIndex)=>{ 35 | const totalChild=childItems?childItems.length:0; 36 | const isLastChild = totalChild===droppedIndex+1 ; 37 | 38 | if(isLastChild) 39 | { 40 | addNewChild() 41 | } 42 | } 43 | 44 | const createChild = (count, data) => { 45 | const colCount = count; 46 | const className = data.class_name || "col-md-12"; 47 | if (!data.childItems) { 48 | // eslint-disable-next-line no-param-reassign 49 | data.childItems = Array.from({ length: colCount }, (v, i) => null); 50 | data.isContainer = true; 51 | } 52 | setChildItems(data.childItems); 53 | }; 54 | const { 55 | controls, 56 | editModeOn, 57 | getDataById, 58 | setAsChild, 59 | removeChild, 60 | seq, 61 | className, 62 | index, 63 | } = props; 64 | const { pageBreakBefore } = childData; 65 | let baseClasses = "SortableItem rfb-item"; 66 | if (pageBreakBefore) { 67 | baseClasses += " alwaysbreak"; 68 | } 69 | 70 | return ( 71 |
    72 | 73 |
    74 | 75 |
    76 | { 77 | childItems?.map((x, i) => ( 78 |
    79 | {controls ? ( 80 | controls[i] 81 | ) : ( 82 | onDropSuccess(i)} 90 | parentIndex={index} 91 | editModeOn={editModeOn} 92 | _onDestroy={() => removeChild(childData, i)} 93 | getDataById={getDataById} 94 | setAsChild={setAsChild} 95 | seq={seq} 96 | rowNo={i} 97 | /> 98 | )} 99 |
    100 | ))} 101 |
    102 |
    103 |
    104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/language-provider/locales/fa-ir.json: -------------------------------------------------------------------------------- 1 | { 2 | "display-label": "برچسب نمایش", 3 | "choose-file": "انتخاب فایل", 4 | "choose-file-type": "انتخاب نوع فایل", 5 | "select-file-type": "انتخاب نوع فایل", 6 | "text-to-display": "متن برای نمایش", 7 | "link-to": "لینک به", 8 | "center": "مرکز", 9 | "width": "عرض", 10 | "height": "ارتفاع", 11 | "required": "الزامی", 12 | "read-only": "فقط خواندنی", 13 | "default-to-today": "پیش‌فرض به امروز", 14 | "show-time-select": "نمایش انتخاب زمان", 15 | "show-time-select-only": "فقط نمایش انتخاب زمان", 16 | "show-time-input": "نمایش ورودی زمان", 17 | "display-horizontal": "نمایش افقی", 18 | "variable-key": "کلید متغیر", 19 | "variable-key-desc": "این عنصر را به کلیدی تبدیل می‌کند که می‌تواند با یک مقدار زمان اجرا جایگزین شود", 20 | "print-options": "گزینه‌های پرینت", 21 | "page-break-before-elements": "شکست صفحه قبل از عنصر", 22 | "alternate-signature-page": "صفحه امضا/جایگزین", 23 | "display-on-alternate-signature-page": "نمایش در صفحه امضا/جایگزین", 24 | "step": "گام", 25 | "min": "حداقل", 26 | "max": "حداکثر", 27 | "default-selected": "پیش‌فرض انتخاب شده", 28 | "text-style": "سبک متن", 29 | "bold": "پررنگ", 30 | "italic": "ایتالیک", 31 | "description": "توضیحات", 32 | "correct-answer": "پاسخ صحیح", 33 | "populate-options-from-api": "پر کردن گزینه‌ها از API", 34 | "populate": "پر کردن", 35 | "pdf": "PDF", 36 | "word": "Word", 37 | "excel": "Excel", 38 | "ppt": "PPT", 39 | 40 | "options": "گزینه‌ها", 41 | "value": "مقدار", 42 | "correct": "صحیح", 43 | 44 | "dismiss": "رد کردن", 45 | 46 | "place-holder-option-1": "گزینه جایگزین 1", 47 | "place-holder-option-2": "گزینه جایگزین 2", 48 | "place-holder-option-3": "گزینه جایگزین 3", 49 | "place-holder-tag-1": "برچسب جایگزین 1", 50 | "place-holder-tag-2": "برچسب جایگزین 2", 51 | "place-holder-tag-3": "برچسب جایگزین 3", 52 | 53 | "toolbox": "جعبه ابزار", 54 | "header-text": "متن سرصفحه", 55 | "label": "برچسب", 56 | "paragraph": "پاراگراف", 57 | "line-break": "شکست خط", 58 | "dropdown": "منوی کشویی", 59 | "tags": "برچسب‌ها", 60 | "checkboxes": "چک‌ باکس‌ها", 61 | "multiple-choice": "چند گزینه‌ای", 62 | "text-input": "ورودی متن", 63 | "email-input": "ایمیل", 64 | "phone-input": "شماره تلفن", 65 | "number-input": "ورودی عدد", 66 | "multi-line-input": "ورودی چند خطی", 67 | "fieldset": "مجموعه فیلد", 68 | "two-columns-row": "ردیف دو ستونی", 69 | "three-columns-row": "ردیف سه ستونی", 70 | "four-columns-row": "ردیف چهار ستونی", 71 | "five-columns-row": "ردیف پنج ستونی", 72 | "six-columns-row": "ردیف شش ستونی", 73 | "image": "تصویر", 74 | "rating": "رتبه‌بندی", 75 | "date": "تاریخ", 76 | "signature": "امضا", 77 | "website": "وب‌سایت", 78 | "file-attachment": "پیوست فایل", 79 | "range": "محدوده", 80 | "camera": "دوربین", 81 | "file-upload": "بارگذاری فایل", 82 | "place-holder-text": "متن جایگزین...", 83 | "place-holder-label": "برچسب جایگزین", 84 | "place-holder-website-link": "لینک وب‌سایت جایگزین...", 85 | "place-holder-file-name": "نام فایل جایگزین...", 86 | "place-holder-email": "ایمیل", 87 | "place-holder-phone-number": "شماره تلفن", 88 | "easy": "آسان", 89 | "difficult": "دشوار", 90 | "drop-zone": "منطقه رها کردن", 91 | 92 | "message.is-required": "الزامی است", 93 | "message.was-answered-incorrectly": "به اشتباه پاسخ داده شد", 94 | "message.was-not-registered": "ثبت نشد", 95 | "message.invalid-email": "فیلد نیاز به آدرس ایمیل معتبر دارد", 96 | "message.invalid-phone-number": "فیلد نیاز به شماره تلفن معتبر دارد" 97 | } 98 | -------------------------------------------------------------------------------- /src/stores/store.js: -------------------------------------------------------------------------------- 1 | import Store from 'beedle'; 2 | import { get, post } from './requests'; 3 | 4 | let _saveUrl; 5 | let _onPost; 6 | let _onLoad; 7 | 8 | const store = new Store({ 9 | actions: { 10 | setData(context, data, saveData) { 11 | context.commit('setData', data); 12 | if (saveData) this.save(data); 13 | }, 14 | 15 | load(context, { loadUrl, saveUrl, data, saveAlways }) { 16 | _saveUrl = saveUrl; 17 | const saveA = saveAlways || saveAlways === undefined; 18 | context.commit('setSaveAlways', saveA); 19 | if (_onLoad) { 20 | _onLoad().then(x => { 21 | if (data && data.length > 0 && x.length === 0) { 22 | data.forEach(y => x.push(y)); 23 | } 24 | this.setData(context, x); 25 | }); 26 | } else if (loadUrl) { 27 | get(loadUrl).then(x => { 28 | if (data && data.length > 0 && x.length === 0) { 29 | data.forEach(y => x.push(y)); 30 | } 31 | this.setData(context, x); 32 | }); 33 | } else { 34 | this.setData(context, data); 35 | } 36 | }, 37 | 38 | create(context, element) { 39 | const { data, saveAlways } = context.state; 40 | data.push(element); 41 | this.setData(context, data, saveAlways); 42 | }, 43 | 44 | delete(context, element) { 45 | const { data, saveAlways } = context.state; 46 | data.splice(data.indexOf(element), 1); 47 | this.setData(context, data, saveAlways); 48 | }, 49 | 50 | deleteLastItem(context) { 51 | const { lastItem } = context.state; 52 | if (lastItem) { 53 | this.delete(context, lastItem); 54 | context.commit('setLastItem', null); 55 | } 56 | }, 57 | 58 | resetLastItem(context) { 59 | const { lastItem } = context.state; 60 | if (lastItem) { 61 | context.commit('setLastItem', null); 62 | // console.log('resetLastItem'); 63 | } 64 | }, 65 | 66 | post(context) { 67 | const { data } = context.state; 68 | this.setData(context, data, true); 69 | }, 70 | 71 | updateOrder(context, elements) { 72 | const { saveAlways } = context.state; 73 | const newData = elements.filter(x => x && !x.parentId); 74 | elements.filter(x => x && x.parentId).forEach(x => newData.push(x)); 75 | this.setData(context, newData, saveAlways); 76 | }, 77 | 78 | insertItem(context, item) { 79 | // console.log('insertItem', item); 80 | context.commit('setLastItem', item.isContainer ? null : item); 81 | }, 82 | 83 | save(data) { 84 | if (_onPost) { 85 | _onPost({ task_data: data }); 86 | } else if (_saveUrl) { 87 | post(_saveUrl, { task_data: data }); 88 | } 89 | }, 90 | }, 91 | 92 | mutations: { 93 | setData(state, payload) { 94 | // eslint-disable-next-line no-param-reassign 95 | state.data = payload; 96 | return state; 97 | }, 98 | setSaveAlways(state, payload) { 99 | // eslint-disable-next-line no-param-reassign 100 | state.saveAlways = payload; 101 | return state; 102 | }, 103 | setLastItem(state, payload) { 104 | // eslint-disable-next-line no-param-reassign 105 | state.lastItem = payload; 106 | // console.log('setLastItem', payload); 107 | return state; 108 | }, 109 | }, 110 | 111 | initialState: { 112 | data: [], 113 | saveAlways: true, 114 | lastItem: null, 115 | }, 116 | }); 117 | 118 | store.setExternalHandler = (onLoad, onPost) => { 119 | _onLoad = onLoad; 120 | _onPost = onPost; 121 | }; 122 | 123 | export default store; 124 | -------------------------------------------------------------------------------- /src/language-provider/locales/en-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "display-label": "Display Label", 3 | "choose-file": "Choose file", 4 | "choose-file-type": "Choose file type", 5 | "select-file-type": "Select file type", 6 | "text-to-display": "Text to display", 7 | "link-to": "Link to", 8 | "center": "Center", 9 | "width": "Width", 10 | "height": "Height", 11 | "required": "Required", 12 | "read-only": "Read only", 13 | "default-to-today": "Default to Today", 14 | "show-time-select": "Show Time Select", 15 | "show-time-select-only": "Show Time Select Only", 16 | "show-time-input": "Show Time Input", 17 | "display-horizontal": "Display horizontal", 18 | "variable-key": "Variable Key", 19 | "variable-key-desc": "This will give the element a key that can be used to replace the content with a runtime value", 20 | "print-options": "Print options", 21 | "page-break-before-elements": "Page Break Before Element", 22 | "alternate-signature-page": "Alternate/Signature Page", 23 | "display-on-alternate-signature-page": "Display on alternate/signature Page", 24 | "step": "Step", 25 | "min": "Min", 26 | "max": "Max", 27 | "default-selected": "Default Selected", 28 | "text-style": "Text Style", 29 | "bold": "Bold", 30 | "italic": "Italic", 31 | "description": "Description", 32 | "correct-answer" : "Correct Answer", 33 | "populate-options-from-api" : "Populate Options from API", 34 | "populate" : "Populate", 35 | "pdf" : "PDF", 36 | "word" : "Word", 37 | "excel" : "Excel", 38 | "ppt" : "PPT", 39 | 40 | "options" : "Options", 41 | "value" : "Value", 42 | "correct" : "Correct", 43 | 44 | "dismiss" : "Dismiss", 45 | 46 | "place-holder-option-1" : "Place holder option 1", 47 | "place-holder-option-2" : "Place holder option 2", 48 | "place-holder-option-3" : "Place holder option 3", 49 | "place-holder-tag-1" : "Place holder option 1", 50 | "place-holder-tag-2" : "Place holder option 2", 51 | "place-holder-tag-3" : "Place holder option 3", 52 | 53 | "toolbox" : "Toolbox", 54 | "header-text" : "Header Text", 55 | "label" : "Label", 56 | "paragraph" : "Paragraph", 57 | "line-break" : "Line Break", 58 | "dropdown" : "Dropdown", 59 | "tags": "Tags", 60 | "checkboxes" : "Checkboxes", 61 | "multiple-choice":"Multiple Choice", 62 | "text-input":"Text Input", 63 | "email-input":"Email", 64 | "phone-input":"Phone Number", 65 | "number-input":"Number Input", 66 | "multi-line-input":"Multi-line Input", 67 | "fieldset":"Fieldset", 68 | "two-columns-row":"Two Column Row", 69 | "three-columns-row":"Three Columns Row", 70 | "four-columns-row":"Four Columns Row", 71 | "five-columns-row":"Five Columns Row", 72 | "six-columns-row":"Six Columns Row", 73 | "image":"Image", 74 | "rating":"Rating", 75 | "date":"Date", 76 | "signature":"Signature", 77 | "website":"Website", 78 | "file-attachment":"File Attachment", 79 | "range":"Range", 80 | "camera":"Camera", 81 | "file-upload":"File Upload", 82 | "place-holder-text": "Placeholder text...", 83 | "place-holder-label": "Placeholder label", 84 | "place-holder-website-link": "Placeholder website Link...", 85 | "place-holder-file-name": "Placeholder file name...", 86 | "place-holder-email": "E-Mail", 87 | "place-holder-phone-number": "Phone Number", 88 | "easy": "Easy", 89 | "difficult": "Difficult", 90 | "drop-zone": "Dropzone", 91 | 92 | "message.is-required": "is required", 93 | "message.was-answered-incorrectly": "was answered incorrectly", 94 | "message.was-not-registered": "was not registered", 95 | "message.invalid-email": "field requires valid email address", 96 | "message.invalid-phone-number": "field requires a valid phone number" 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | } -------------------------------------------------------------------------------- /src/language-provider/locales/it-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "display-label": "Visualizza etichetta", 3 | "choose-file": "Scegli file", 4 | "choose-file-type": "Scegli tipo di file", 5 | "select-file-type": "Scegli tipo di file", 6 | "text-to-display": "Testo visualizzato", 7 | "link-to": "Link to", 8 | "center": "Centro", 9 | "width": "Larghezza", 10 | "height": "Altezza", 11 | "required": "Obbligatorio", 12 | "read-only": "Sola lettura", 13 | "default-to-today": "Default oggi", 14 | "show-time-select": "Visualizza selettore orario", 15 | "show-time-select-only": "Visualizza solo orario", 16 | "show-time-input": "Visualizza input orario", 17 | "display-horizontal": "Visualizza orizzontale", 18 | "variable-key": "Chiave variabile", 19 | "variable-key-desc": "Ciò fornirà all'elemento una chiave che può essere utilizzata per sostituire il contenuto con un valore di runtime", 20 | "print-options": "Opzioni di stampa", 21 | "page-break-before-elements": "Interruzione di pagina prima dell'elemento", 22 | "alternate-signature-page": "Pagina alternativa/firma", 23 | "display-on-alternate-signature-page": "Visualizza su pagina alternativa/firma", 24 | "step": "Step", 25 | "min": "Min", 26 | "max": "Max", 27 | "default-selected": "Selezionato di default", 28 | "text-style": "Stile testo", 29 | "bold": "Grassetto", 30 | "italic": "Corsivo", 31 | "description": "Descrizione", 32 | "correct-answer" : "Risposta corretta", 33 | "populate-options-from-api" : "Popola opzione da API", 34 | "populate" : "Popola", 35 | "pdf" : "PDF", 36 | "word" : "Word", 37 | "excel" : "Excel", 38 | "ppt" : "PPT", 39 | 40 | "options" : "Opzioni", 41 | "value" : "Valore", 42 | "correct" : "Corretto", 43 | 44 | "dismiss" : "Scarta", 45 | 46 | "place-holder-option-1" : "Segnaposto opzione 1", 47 | "place-holder-option-2" : "Segnaposto opzione 2", 48 | "place-holder-option-3" : "Segnaposto opzione 3", 49 | "place-holder-tag-1" : "Segnaposto opzione 1", 50 | "place-holder-tag-2" : "Segnaposto opzione 2", 51 | "place-holder-tag-3" : "Segnaposto opzione 3", 52 | 53 | "toolbox" : "Toolbox", 54 | "header-text" : "Testo Header", 55 | "label" : "Etichetta", 56 | "paragraph" : "Paragrafo", 57 | "line-break" : "Linea Interruzione", 58 | "dropdown" : "Dropdown", 59 | "tags": "Tags", 60 | "checkboxes" : "Checkboxes", 61 | "multiple-choice":"Scelta multipla", 62 | "text-input":"Input testo", 63 | "email-input":"Email", 64 | "phone-input":"Numero di telefono", 65 | "number-input":"Input numerico", 66 | "multi-line-input":"Input multi linea", 67 | "fieldset":"Set di campi", 68 | "two-columns-row":"Layout 2 colonne", 69 | "three-columns-row":"Layout 3 colonne", 70 | "four-columns-row":"Layout 4 colonne", 71 | "five-columns-row":"Layout 5 colonne", 72 | "six-columns-row":"Layout 6 colonne", 73 | "image":"Immagine", 74 | "rating":"Valutazione", 75 | "date":"Data", 76 | "signature":"Firma", 77 | "website":"Sito web", 78 | "file-attachment":"Allegato file", 79 | "range":"Range", 80 | "camera":"Camera", 81 | "file-upload":"Upload file", 82 | "place-holder-text": "Testo segnaposto...", 83 | "place-holder-label": "Etichetta segnaposto", 84 | "place-holder-website-link": "Segnaposto link sito...", 85 | "place-holder-file-name": "Segnaposto nome file...", 86 | "place-holder-email": "E-Mail", 87 | "place-holder-phone-number": "Numero di telefono", 88 | "easy": "Facile", 89 | "difficult": "Difficile", 90 | "drop-zone": "Dropzone", 91 | 92 | "message.is-required": "è obbligatorio", 93 | "message.was-answered-incorrectly": "è stato risposto in modo errato", 94 | "message.was-not-registered": "non è stato registrato", 95 | "message.invalid-email": "il campo richiede un indirizzo email valido", 96 | "message.invalid-phone-number": "campo richiede un numero di telefono valido" 97 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form-builder2", 3 | "version": "0.20.3", 4 | "description": "A complete form builder for react.", 5 | "main": "lib/index.js", 6 | "types": "types/index.d.ts", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/Kiho/react-form-builder.git" 11 | }, 12 | "files": [ 13 | "lib", 14 | "dist", 15 | "types" 16 | ], 17 | "keywords": [ 18 | "react", 19 | "react-component", 20 | "form", 21 | "builder", 22 | "ui", 23 | "drag", 24 | "drop" 25 | ], 26 | "engines": { 27 | "node": ">=18.0.0" 28 | }, 29 | "author": "Kiho Chang", 30 | "dependencies": { 31 | "beedle": "^0.8.1", 32 | "classnames": "^2.2.6", 33 | "date-fns": "^2.16.1", 34 | "draft-js": "^0.11.7", 35 | "draftjs-to-html": "^0.8.4", 36 | "es6-promise": "^4.2.8", 37 | "fbemitter": "^3.0.0", 38 | "file-saver": "^2.0.5", 39 | "immutability-helper": "^3.1.1", 40 | "isomorphic-fetch": "^3.0.0", 41 | "prop-types": "^15.7.2", 42 | "react-bootstrap-slider": "^3.0.0", 43 | "react-datepicker": "^8.3.0", 44 | "react-dnd": "^16.0.1", 45 | "react-dnd-html5-backend": "^16.0.1", 46 | "react-draft-wysiwyg": "^1.15.0", 47 | "react-intl": "^7.1.11", 48 | "react-select": "^5.10.1", 49 | "react-signature-canvas": "^1.1.0-alpha.2", 50 | "react-textarea-autosize": "^8.5.9", 51 | "xss": "^1.0.8" 52 | }, 53 | "peerDependencies": { 54 | "react": ">=18.3.1", 55 | "react-dom": ">=18.3.1" 56 | }, 57 | "devDependencies": { 58 | "@babel/cli": "^7.12.10", 59 | "@babel/core": "^7.12.10", 60 | "@babel/plugin-proposal-class-properties": "^7.12.1", 61 | "@babel/plugin-proposal-json-strings": "^7.12.1", 62 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 63 | "@babel/plugin-syntax-import-meta": "^7.10.4", 64 | "@babel/plugin-transform-runtime": "^7.17.10", 65 | "@babel/preset-env": "^7.12.11", 66 | "@babel/preset-react": "^7.14.5", 67 | "@babel/runtime-corejs2": "^7.12.5", 68 | "add": "^2.0.6", 69 | "babel-eslint": "^10.1.0", 70 | "babel-loader": "^8.2.2", 71 | "copyfiles": "^2.4.1", 72 | "css-loader": "^3.6.0", 73 | "ejs": "^2.7.4", 74 | "eslint": "^6.8.0", 75 | "eslint-config-airbnb": "^18.2.1", 76 | "eslint-plugin-import": "^2.22.1", 77 | "eslint-plugin-jsx-a11y": "^6.4.1", 78 | "eslint-plugin-react": "^7.22.0", 79 | "express": "^4.17.1", 80 | "multer": "^1.4.2", 81 | "react": "^18.3.1", 82 | "react-dom": "^18.3.1", 83 | "rimraf": "^3.0.2", 84 | "sass": "^1.88.0", 85 | "sass-loader": "^16.0.5", 86 | "style-loader": "^4.0.0", 87 | "webpack": "^5.99.8", 88 | "webpack-cli": "^6.0.1", 89 | "webpack-dev-server": "^5.2.1" 90 | }, 91 | "scripts": { 92 | "build": "webpack --mode production --config webpack.production.config.js", 93 | "build:dev": "webpack --mode development", 94 | "build:umd": "webpack --mode development --config webpack.production.config.js", 95 | "build:style": "sass ./scss/application.scss dist/app.css --style compressed", 96 | "build:lib": "npm run transpile && npm run build:style", 97 | "build:dist": "npm run build && npm run copy:dist", 98 | "clean": "rimraf dist", 99 | "copy:dist": "copyfiles -f \"./dist/*\" \"./public/dist\"", 100 | "prepublishOnly": "npm run clean && NODE_OPTIONS=--openssl-legacy-provider npm run build:lib && NODE_OPTIONS=--openssl-legacy-provider npm run build", 101 | "watch": "webpack --watch", 102 | "start": "NODE_OPTIONS=--openssl-legacy-provider && webpack-dev-server --hot --mode development", 103 | "serve:api": "node server/index.js", 104 | "pretranspile": "rimraf lib", 105 | "transpile": "babel --out-dir lib src --copy-files" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | import React from 'react'; 6 | import { DndProvider } from 'react-dnd'; 7 | import { HTML5Backend } from 'react-dnd-html5-backend'; 8 | import { IntlProvider } from 'react-intl'; 9 | import Preview from './preview'; 10 | import Toolbar from './toolbar'; 11 | import FormGenerator from './form'; 12 | import store from './stores/store'; 13 | import Registry from './stores/registry'; 14 | import AppLocale from './language-provider'; 15 | 16 | class ReactFormBuilder extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | editMode: false, 22 | editElement: null, 23 | }; 24 | this.editModeOn = this.editModeOn.bind(this); 25 | } 26 | 27 | editModeOn(data, e) { 28 | e.preventDefault(); 29 | e.stopPropagation(); 30 | if (this.state.editMode) { 31 | this.setState({ editMode: !this.state.editMode, editElement: null }); 32 | } else { 33 | this.setState({ editMode: !this.state.editMode, editElement: data }); 34 | } 35 | } 36 | 37 | manualEditModeOff() { 38 | if (this.state.editMode) { 39 | this.setState({ 40 | editMode: false, 41 | editElement: null, 42 | }); 43 | } 44 | } 45 | 46 | render() { 47 | const toolbarProps = { 48 | showDescription: this.props.show_description, 49 | }; 50 | 51 | const language = this.props.locale ? this.props.locale : 'en'; 52 | const currentAppLocale = AppLocale[language]; 53 | if (this.props.toolbarItems) { toolbarProps.items = this.props.toolbarItems; } 54 | return ( 55 | 56 | 59 |
    60 | {/*
    61 |

    62 | It is easy to implement a sortable interface with React DnD. Just make 63 | the same component both a drag source and a drop target, and reorder 64 | the data in the hover handler. 65 |

    66 | 67 |
    */} 68 |
    69 |
    70 | 88 | 89 |
    90 |
    91 |
    92 |
    93 |
    94 | ); 95 | } 96 | } 97 | 98 | function ReactFormGenerator(props) { 99 | const language = props.locale ? props.locale : 'en'; 100 | const currentAppLocale = AppLocale[language]; 101 | return ( 102 | 105 | 106 | 107 | ); 108 | } 109 | 110 | const FormBuilders = {}; 111 | FormBuilders.ReactFormBuilder = ReactFormBuilder; 112 | FormBuilders.ReactFormGenerator = ReactFormGenerator; 113 | FormBuilders.ElementStore = store; 114 | FormBuilders.Registry = Registry; 115 | 116 | export default FormBuilders; 117 | 118 | export { 119 | ReactFormBuilder, ReactFormGenerator, store as ElementStore, Registry, 120 | }; 121 | -------------------------------------------------------------------------------- /src/multi-column/dustbin.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useDrop } from 'react-dnd'; 3 | import FormElements from '../form-elements'; 4 | import ItemTypes from '../ItemTypes'; 5 | 6 | import CustomElement from '../form-elements/custom-element'; 7 | import Registry from '../stores/registry'; 8 | import store from '../stores/store'; 9 | 10 | function getCustomElement(item, props) { 11 | if (!item.component || typeof item.component !== 'function') { 12 | item.component = Registry.get(item.key); 13 | if (!item.component) { 14 | console.error(`${item.element} was not registered`); 15 | } 16 | } 17 | return ( 18 | 24 | ); 25 | } 26 | 27 | function getElement(item, props) { 28 | if (!item) return null; 29 | if (item.custom) { 30 | return getCustomElement(item, props); 31 | } 32 | const Element = FormElements[item.element || item.key]; 33 | return ; 34 | } 35 | 36 | function getStyle(backgroundColor) { 37 | return { 38 | border: '1px solid rgba(0,0,0,0.2)', 39 | minHeight: '2rem', 40 | minWidth: '7rem', 41 | width: '100%', 42 | backgroundColor, 43 | padding: 0, 44 | float: 'left', 45 | }; 46 | } 47 | 48 | function isContainer(item) { 49 | if (item.itemType !== ItemTypes.CARD) { 50 | const { data } = item; 51 | if (data) { 52 | if (data.isContainer) { 53 | return true; 54 | } 55 | if (data.field_name) { 56 | return data.field_name.indexOf('_col_row') > -1; 57 | } 58 | } 59 | } 60 | return false; 61 | } 62 | 63 | const Dustbin = ({ 64 | onDropSuccess, 65 | seq, 66 | parentIndex, 67 | items, 68 | col, 69 | getDataById, 70 | accepts, 71 | data, 72 | setAsChild, 73 | ...rest 74 | }) => { 75 | const dropRef = useRef(null); 76 | const item = getDataById(items[col]); 77 | 78 | const [{ isOver, canDrop, draggedItem }, drop] = useDrop({ 79 | accept: accepts, 80 | collect: (monitor) => ({ 81 | isOver: monitor.isOver(), 82 | canDrop: monitor.canDrop(), 83 | draggedItem: monitor.getItem(), 84 | }), 85 | drop: (droppedItem) => { 86 | // Do nothing when moving the box inside the same column 87 | if (col === droppedItem.col && items[col] === droppedItem.id) return; 88 | 89 | // Do not allow replace component other than both items in same multi column row 90 | if (droppedItem.col === undefined && items[col]) { 91 | store.dispatch('resetLastItem'); 92 | return; 93 | } 94 | 95 | if (!isContainer(droppedItem)) { 96 | console.log("Item dropped", droppedItem); 97 | 98 | const isBusy = !!items[col]; 99 | 100 | if (droppedItem.data) { 101 | const isNew = !droppedItem.data.id; 102 | const itemData = isNew ? droppedItem.onCreate(droppedItem.data) : droppedItem.data; 103 | 104 | if (typeof setAsChild === 'function') { 105 | setAsChild(data, itemData, col, isBusy); 106 | } 107 | 108 | onDropSuccess && onDropSuccess(); 109 | store.dispatch('deleteLastItem'); 110 | } 111 | } 112 | }, 113 | canDrop: (item) => { 114 | // Add any custom logic for when an item can be dropped 115 | return true; 116 | }, 117 | }); 118 | 119 | const element = getElement(item, rest); 120 | const sameCard = draggedItem ? draggedItem.index === parentIndex : false; 121 | 122 | let backgroundColor = 'rgba(0, 0, 0, .03)'; 123 | 124 | if (!sameCard && isOver && canDrop && draggedItem && !draggedItem.data?.isContainer) { 125 | backgroundColor = '#F7F589'; 126 | } 127 | 128 | // Connect the drop ref to the DOM element 129 | drop(dropRef); 130 | 131 | return ( 132 |
    133 | {!element && Drop your element here} 134 | {element} 135 |
    136 | ); 137 | }; 138 | 139 | export default Dustbin; -------------------------------------------------------------------------------- /examples/custom/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable implicit-arrow-linebreak */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { 5 | ReactFormBuilder, 6 | ElementStore, 7 | Registry, 8 | } from '@threehippies/react-form-builder'; 9 | import DemoBar from './demobar'; 10 | import * as variables from './variables'; 11 | import { get, post } from './requests'; 12 | 13 | const getUrl = (cid) => 14 | `https://safe-springs-35306.herokuapp.com/api/formdata?cid=${cid}`; 15 | 16 | const TestComponent = () =>

    Hello

    ; 17 | 18 | const MyInput = React.forwardRef((props, ref) => { 19 | const { name, defaultValue, disabled } = props; 20 | return ( 21 | 27 | ); 28 | }); 29 | 30 | Registry.register('MyInput', MyInput); 31 | Registry.register('TestComponent', TestComponent); 32 | 33 | const items = [ 34 | { 35 | key: 'Header', 36 | }, 37 | { 38 | key: 'TextInput', 39 | }, 40 | { 41 | key: 'TextArea', 42 | }, 43 | { 44 | key: 'RadioButtons', 45 | }, 46 | { 47 | key: 'Checkboxes', 48 | }, 49 | { 50 | key: 'Image', 51 | }, 52 | { 53 | group_name: 'Multi Column Row', 54 | key: 'TwoColumnRow', 55 | }, 56 | { 57 | group_name: 'Multi Column Row', 58 | key: 'ThreeColumnRow', 59 | }, 60 | { 61 | group_name: 'Multi Column Row', 62 | key: 'FourColumnRow', 63 | element: 'MultiColumnRow', 64 | }, 65 | { 66 | group_name: 'Multi Column Row', 67 | key: 'FiveColumnRow', 68 | element: 'MultiColumnRow', 69 | }, 70 | { 71 | group_name: 'Multi Column Row', 72 | key: 'SixColumnRow', 73 | element: 'MultiColumnRow', 74 | }, 75 | { 76 | group_name: 'Custom Element', 77 | key: 'TestComponent', 78 | element: 'CustomElement', 79 | component: TestComponent, 80 | type: 'custom', 81 | field_name: 'test_component', 82 | name: 'Something You Want', 83 | icon: 'fa fa-cog', 84 | static: true, 85 | props: { test: 'test_comp' }, 86 | label: 'Label Test', 87 | }, 88 | { 89 | group_name: 'Custom Element', 90 | key: 'MyInput', 91 | element: 'CustomElement', 92 | component: MyInput, 93 | type: 'custom', 94 | forwardRef: true, 95 | bare: true, 96 | field_name: 'my_input_', 97 | name: 'My Input', 98 | icon: 'fa fa-cog', 99 | props: { test: 'test_input' }, 100 | label: 'Label Input', 101 | }, 102 | ]; 103 | 104 | class App extends React.Component { 105 | constructor(props) { 106 | super(props); 107 | this.state = { formId: '1' }; 108 | this.formId = this.state.formId; 109 | this.handleChange = this.handleChange.bind(this); 110 | } 111 | 112 | formId; 113 | 114 | handleChange(event) { 115 | this.formId = event.target.value; 116 | const url = getUrl(this.formId); 117 | console.log('handleChange', url); 118 | ElementStore.dispatch('load', { loadUrl: url }); 119 | this.setState({ formId: this.formId }); 120 | } 121 | 122 | onLoad = () => { 123 | const url = getUrl(this.formId); 124 | console.log('onLoad', url); 125 | return get(url); 126 | }; 127 | 128 | onPost = (data) => { 129 | const saveUrl = getUrl(this.formId); 130 | console.log('onPost', saveUrl, data); 131 | post(saveUrl, data); 132 | }; 133 | 134 | render() { 135 | return ( 136 |
    137 | 138 | 146 |
    147 | 152 | , 153 |
    154 | ); 155 | } 156 | } 157 | 158 | ReactDOM.render(, document.getElementById('form-builder')); 159 | 160 | ReactDOM.render( 161 | , 162 | document.getElementById('demo-bar') 163 | ); 164 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import * as React from 'react'; 3 | 4 | type BaseElement = { 5 | id: string; 6 | element: 7 | | "Header Text" 8 | | "Label" 9 | | "Paragraph" 10 | | "Line Break" 11 | | "Dropdown" 12 | | "Tags" 13 | | "Checkboxes" 14 | | "Multiple Choice" 15 | | "Text Input" 16 | | "Number Input" 17 | | "Multi-line Input" 18 | | "Two Column Row" 19 | | "Three Column Row" 20 | | "Multi Column Row" 21 | | "Image" 22 | | "Rating" 23 | | "Date" 24 | | "Signature" 25 | | "Web site" 26 | | "Fieldset" 27 | | "File Attachment" 28 | | "Range" 29 | | "Camera"; 30 | showDescription?: boolean; 31 | required: boolean; 32 | canHaveAlternateForm: boolean; 33 | canHaveDisplayHorizontal: boolean; 34 | canHaveOptionCorrect: boolean; 35 | canHaveOptionValue: boolean; 36 | canHavePageBreakBefore: boolean; 37 | canPopulateFromApi: boolean; 38 | text: string; 39 | }; 40 | export type StaticElement = { 41 | bold: boolean; 42 | content: string; 43 | inline?: boolean; 44 | italic: boolean; 45 | static: true; 46 | }; 47 | export type FormBuilderInput = { 48 | canHaveAnswer?: true; 49 | field_name: string; 50 | label: string; 51 | }; 52 | export type Option = { 53 | key: string; 54 | label?: string; 55 | text: string; 56 | value: string; 57 | }; 58 | export type SelectableElement = { 59 | options: Option[]; 60 | } & FormBuilderInput; 61 | export type ImageElement = { 62 | field_name: string; 63 | src: string; 64 | }; 65 | export type DateElement = { 66 | dateFormat: string; 67 | defaultToday: boolean; 68 | readOnly: boolean; 69 | showTimeSelect: boolean; 70 | showTimeSelectOnly: boolean; 71 | showTimeInput: boolean; 72 | timeFormat: string; 73 | } & FormBuilderInput; 74 | export type RangeElement = { 75 | max_label: string; 76 | max_value: number; 77 | min_label: string; 78 | min_value: number; 79 | } & FormBuilderInput; 80 | export type FileElement = { 81 | _href: string; 82 | file_path: string; 83 | field_name: string; 84 | } & StaticElement; 85 | export type WebsiteElement = { 86 | href: string; 87 | } & StaticElement; 88 | export type SignatureElement = { 89 | readOnly: boolean; 90 | } & FormBuilderInput; 91 | export type TaskData = BaseElement & 92 | (| StaticElement 93 | | FormBuilderInput 94 | | SelectableElement 95 | | ImageElement 96 | | DateElement 97 | | RangeElement 98 | | WebsiteElement 99 | | FileElement 100 | | SignatureElement 101 | // eslint-disable-next-line no-use-before-define 102 | | FormBuilderLayout 103 | ); 104 | export type FormBuilderLayout = { 105 | isContainer: true; 106 | childItems: TaskData[]; 107 | field_name: string; 108 | }; 109 | export type FormBuilderPostData = { 110 | task_data: TaskData[]; 111 | }; 112 | 113 | export type ToolbarItem = { 114 | key: string; 115 | name: string; 116 | static: boolean; 117 | icon: string; 118 | content: string; 119 | }; 120 | 121 | export interface FormBuilderProps { 122 | toolbarItems?: ToolbarItem[]; 123 | files?: any[]; 124 | locale?: string; 125 | url?: string; 126 | showCorrectColumn?: boolean; 127 | show_description?: boolean; 128 | onLoad?: () => Promise; 129 | onPost?: (data: FormBuilderPostData) => void; 130 | saveUrl?: string; 131 | saveAlways?: boolean; 132 | editMode?: boolean; 133 | renderEditForm?: (props: BaseElement) => React.ReactNode; 134 | } 135 | 136 | export class ReactFormBuilder extends React.Component {} 137 | 138 | export interface FormGeneratorOnSubmitParams { 139 | id: number; 140 | name: string; 141 | custom_name: string; 142 | value: string | string[]; 143 | } 144 | 145 | export interface FormGeneratorProps { 146 | form_action: string; 147 | form_method: string; 148 | action_name?: string; 149 | onBlur?: (info: FormGeneratorOnSubmitParams[]) => void; 150 | onSubmit?: (info: FormGeneratorOnSubmitParams[]) => void; 151 | onChange?: (info: FormGeneratorOnSubmitParams[]) => void; 152 | data: any[]; 153 | back_action?: string; 154 | back_name?: string; 155 | locale?: string; 156 | task_id?: number; 157 | answer_data?: any[]; 158 | authenticity_token?: string; 159 | hide_actions?: boolean; 160 | skip_validations?: boolean; 161 | option_key_value?: 'key' | 'value'; 162 | download_path?: string; 163 | display_short?: boolean; 164 | read_only?: boolean; 165 | // eslint-disable-next-line no-undef 166 | variables?: Record; 167 | submitButton?: JSX.Element; 168 | } 169 | 170 | export class ReactFormGenerator extends React.Component {} 171 | 172 | export type ActionType = "load" | "updateOrder" | "delete"; 173 | 174 | export class ElementStore { 175 | static dispatch: (type: ActionType, data: any) => void; 176 | } 177 | 178 | export class Registry { 179 | static register: (name: string, component: React.ReactNode) => void; 180 | 181 | static list: () => string[]; 182 | 183 | static get: (name: string) => React.ReactNode; 184 | } 185 | -------------------------------------------------------------------------------- /examples/cra/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/sortable-element.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useDrag, useDrop } from 'react-dnd'; 4 | import ItemTypes from './ItemTypes'; 5 | 6 | const style = { 7 | border: '1px dashed gray', 8 | padding: '0.5rem 1rem', 9 | marginBottom: '.5rem', 10 | backgroundColor: 'white', 11 | cursor: 'pointer', 12 | }; 13 | 14 | // Modern approach using a custom hook for DnD logic 15 | const useDragAndDrop = (props) => { 16 | const ref = useRef(null); 17 | 18 | // Setup drag 19 | const [{ isDragging }, drag, preview] = useDrag({ 20 | type: ItemTypes.CARD, 21 | item: () => ({ 22 | itemType: ItemTypes.CARD, 23 | id: props.id, 24 | index: props.index, 25 | }), 26 | collect: (monitor) => ({ 27 | isDragging: monitor.isDragging(), 28 | }), 29 | }); 30 | 31 | // Setup drop 32 | const [, drop] = useDrop({ 33 | accept: [ItemTypes.CARD, ItemTypes.BOX], 34 | drop: (item) => { 35 | // Don't handle drops if we're a container and this is a card drop 36 | if ((props.data && props.data.isContainer) || item.itemType === ItemTypes.CARD) { 37 | return; 38 | } 39 | 40 | const hoverIndex = props.index; 41 | const dragIndex = item.index; 42 | 43 | // Handle box drops 44 | if (item.data && typeof item.setAsChild === 'function') { 45 | if (dragIndex === -1) { 46 | props.insertCard(item, hoverIndex, item.id); 47 | } 48 | } 49 | }, 50 | hover: (item, monitor) => { 51 | // Don't replace items being dragged from box with index -1 52 | if (item.itemType === ItemTypes.BOX && item.index === -1) return; 53 | 54 | // Don't replace multi-column component unless both drag & hover are multi-column 55 | if (props.data?.isContainer && !item.data?.isContainer) return; 56 | 57 | const dragIndex = item.index; 58 | const hoverIndex = props.index; 59 | 60 | // Don't replace items with themselves 61 | if (dragIndex === hoverIndex) { 62 | return; 63 | } 64 | 65 | // Handle new items being created 66 | if (dragIndex === -1) { 67 | if (props.data && props.data.isContainer) { 68 | return; 69 | } 70 | 71 | item.index = hoverIndex; 72 | props.insertCard(item.onCreate(item.data), hoverIndex); 73 | return; 74 | } 75 | 76 | // Skip if no ref available 77 | if (!ref.current) { 78 | return; 79 | } 80 | 81 | // Determine rectangle on screen 82 | const hoverBoundingRect = ref.current.getBoundingClientRect(); 83 | 84 | // Get vertical middle 85 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 86 | 87 | // Determine mouse position 88 | const clientOffset = monitor.getClientOffset(); 89 | if (!clientOffset) return; 90 | 91 | // Get pixels to the top 92 | const hoverClientY = clientOffset.y - hoverBoundingRect.top; 93 | 94 | // Only perform the move when the mouse has crossed half of the items height 95 | // When dragging downwards, only move when the cursor is below 50% 96 | // When dragging upwards, only move when the cursor is above 50% 97 | 98 | // Dragging downwards 99 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 100 | return; 101 | } 102 | 103 | // Dragging upwards 104 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 105 | return; 106 | } 107 | 108 | // Time to actually perform the action 109 | props.moveCard(dragIndex, hoverIndex); 110 | 111 | // Note: we're mutating the monitor item here! 112 | // Generally it's better to avoid mutations, 113 | // but it's good here for the sake of performance 114 | // to avoid expensive index searches. 115 | item.index = hoverIndex; 116 | }, 117 | }); 118 | 119 | // Connect the drag and drop refs to the same element 120 | return { 121 | ref: (node) => { 122 | ref.current = node; 123 | drop(node); 124 | drag(node); 125 | }, 126 | previewRef: preview, 127 | isDragging, 128 | }; 129 | }; 130 | 131 | // Modern approach using a functional component wrapper instead of HOC 132 | const DraggableCard = (props) => { 133 | const { 134 | index, 135 | id, 136 | moveCard, 137 | seq = -1, 138 | ...restProps 139 | } = props; 140 | 141 | const { ref, previewRef, isDragging } = useDragAndDrop(props); 142 | const opacity = isDragging ? 0 : 1; 143 | 144 | // Use the ComposedComponent passed in props 145 | const ComposedComponent = props.component; 146 | 147 | return ( 148 |
    149 |
    150 | 158 |
    159 |
    160 | ); 161 | }; 162 | 163 | DraggableCard.propTypes = { 164 | component: PropTypes.elementType.isRequired, 165 | index: PropTypes.number.isRequired, 166 | isDragging: PropTypes.bool, 167 | id: PropTypes.any.isRequired, 168 | moveCard: PropTypes.func.isRequired, 169 | seq: PropTypes.number, 170 | }; 171 | 172 | DraggableCard.defaultProps = { 173 | seq: -1, 174 | }; 175 | 176 | // This replaces the HOC pattern with a component that takes the component as a prop 177 | export default function createDraggableCard(ComposedComponent) { 178 | return (props) => ; 179 | } -------------------------------------------------------------------------------- /src/dynamic-option-list.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | 5 | import React from 'react'; 6 | import ID from './UUID'; 7 | import IntlMessages from './language-provider/IntlMessages'; 8 | 9 | export default class DynamicOptionList extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | element: this.props.element, 14 | data: this.props.data, 15 | dirty: false, 16 | }; 17 | } 18 | 19 | _setValue(text) { 20 | return text.replace(/[^A-Z0-9]+/ig, '_').toLowerCase(); 21 | } 22 | 23 | editOption(option_index, e) { 24 | const this_element = this.state.element; 25 | const val = (this_element.options[option_index].value !== this._setValue(this_element.options[option_index].text)) ? this_element.options[option_index].value : this._setValue(e.target.value); 26 | 27 | this_element.options[option_index].text = e.target.value; 28 | this_element.options[option_index].value = val; 29 | this.setState({ 30 | element: this_element, 31 | dirty: true, 32 | }); 33 | } 34 | 35 | editValue(option_index, e) { 36 | const this_element = this.state.element; 37 | const val = (e.target.value === '') ? this._setValue(this_element.options[option_index].text) : e.target.value; 38 | this_element.options[option_index].value = val; 39 | this.setState({ 40 | element: this_element, 41 | dirty: true, 42 | }); 43 | } 44 | 45 | // eslint-disable-next-line no-unused-vars 46 | editOptionCorrect(option_index, e) { 47 | const this_element = this.state.element; 48 | if (this_element.options[option_index].hasOwnProperty('correct')) { 49 | delete (this_element.options[option_index].correct); 50 | } else { 51 | this_element.options[option_index].correct = true; 52 | } 53 | this.setState({ element: this_element }); 54 | this.props.updateElement.call(this.props.preview, this_element); 55 | } 56 | 57 | updateOption() { 58 | const this_element = this.state.element; 59 | // to prevent ajax calls with no change 60 | if (this.state.dirty) { 61 | this.props.updateElement.call(this.props.preview, this_element); 62 | this.setState({ dirty: false }); 63 | } 64 | } 65 | 66 | addOption(index) { 67 | const this_element = this.state.element; 68 | const groupKey = this_element.element.toLowerCase(); 69 | this_element.options.splice(index + 1, 0, { value: '', text: '', key: `${groupKey}_option_${ID.uuid()}` }); 70 | this.props.updateElement.call(this.props.preview, this_element); 71 | } 72 | 73 | removeOption(index) { 74 | const this_element = this.state.element; 75 | this_element.options.splice(index, 1); 76 | this.props.updateElement.call(this.props.preview, this_element); 77 | } 78 | 79 | render() { 80 | if (this.state.dirty) { 81 | this.state.element.dirty = true; 82 | } 83 | return ( 84 |
    85 |
      86 |
    • 87 |
      88 |
      89 | { this.props.canHaveOptionValue && 90 |
      } 91 | { this.props.canHaveOptionValue && this.props.canHaveOptionCorrect && 92 |
      } 93 |
      94 |
    • 95 | { 96 | this.props.element.options.map((option, index) => { 97 | const this_key = `edit_${option.key}`; 98 | const val = (option.value !== this._setValue(option.text)) ? option.value : ''; 99 | return ( 100 |
    • 101 |
      102 |
      103 | 104 |
      105 | { this.props.canHaveOptionValue && 106 |
      107 | 108 |
      } 109 | { this.props.canHaveOptionValue && this.props.canHaveOptionCorrect && 110 |
      111 | 112 |
      } 113 |
      114 |
      115 | 116 | { index > 0 117 | && 118 | } 119 |
      120 |
      121 |
      122 |
    • 123 | ); 124 | }) 125 | } 126 |
    127 |
    128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /examples/cra/src/demobar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ReactFormGenerator, 4 | ElementStore, 5 | } from '@threehippies/react-form-builder'; 6 | 7 | export default class Demobar extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | data: [], 12 | previewVisible: false, 13 | shortPreviewVisible: false, 14 | roPreviewVisible: false, 15 | }; 16 | 17 | this._onUpdate = this._onChange.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | ElementStore.subscribe((state) => this._onUpdate(state.data)); 22 | } 23 | 24 | showPreview() { 25 | this.setState({ 26 | previewVisible: true, 27 | }); 28 | } 29 | 30 | showShortPreview() { 31 | this.setState({ 32 | shortPreviewVisible: true, 33 | }); 34 | } 35 | 36 | showRoPreview() { 37 | this.setState({ 38 | roPreviewVisible: true, 39 | }); 40 | } 41 | 42 | closePreview() { 43 | this.setState({ 44 | previewVisible: false, 45 | shortPreviewVisible: false, 46 | roPreviewVisible: false, 47 | }); 48 | } 49 | 50 | _onChange(data) { 51 | this.setState({ 52 | data, 53 | }); 54 | } 55 | 56 | _onSubmit(data) { 57 | console.log('onSubmit', data); 58 | // Place code to post json data to server here 59 | } 60 | 61 | render() { 62 | let modalClass = 'modal'; 63 | if (this.state.previewVisible) { 64 | modalClass += ' show d-block'; 65 | } 66 | 67 | let shortModalClass = 'modal short-modal'; 68 | if (this.state.shortPreviewVisible) { 69 | shortModalClass += ' show d-block'; 70 | } 71 | 72 | let roModalClass = 'modal ro-modal'; 73 | if (this.state.roPreviewVisible) { 74 | roModalClass += ' show d-block'; 75 | } 76 | 77 | return ( 78 |
    79 |

    Preview

    80 | 87 | 94 | 101 | 102 | {this.state.previewVisible && ( 103 |
    104 |
    105 |
    106 | 118 | 119 |
    120 | 128 |
    129 |
    130 |
    131 |
    132 | )} 133 | 134 | {this.state.roPreviewVisible && ( 135 |
    136 |
    137 |
    138 | 151 | 152 |
    153 | 161 |
    162 |
    163 |
    164 |
    165 | )} 166 | 167 | {this.state.shortPreviewVisible && ( 168 |
    169 |
    170 |
    171 | 182 | 183 |
    184 | 192 |
    193 |
    194 |
    195 |
    196 | )} 197 |
    198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /examples/custom/demobar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ReactFormGenerator, 4 | ElementStore, 5 | } from '@threehippies/react-form-builder'; 6 | 7 | export default class Demobar extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | data: [], 12 | previewVisible: false, 13 | shortPreviewVisible: false, 14 | roPreviewVisible: false, 15 | }; 16 | 17 | this._onUpdate = this._onChange.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | ElementStore.subscribe((state) => this._onUpdate(state.data)); 22 | } 23 | 24 | showPreview() { 25 | this.setState({ 26 | previewVisible: true, 27 | }); 28 | } 29 | 30 | showShortPreview() { 31 | this.setState({ 32 | shortPreviewVisible: true, 33 | }); 34 | } 35 | 36 | showRoPreview() { 37 | this.setState({ 38 | roPreviewVisible: true, 39 | }); 40 | } 41 | 42 | closePreview() { 43 | this.setState({ 44 | previewVisible: false, 45 | shortPreviewVisible: false, 46 | roPreviewVisible: false, 47 | }); 48 | } 49 | 50 | _onChange(data) { 51 | this.setState({ 52 | data, 53 | }); 54 | } 55 | 56 | _onSubmit(data) { 57 | console.log('onSubmit', data); 58 | // Place code to post json data to server here 59 | } 60 | 61 | render() { 62 | let modalClass = 'modal'; 63 | if (this.state.previewVisible) { 64 | modalClass += ' show d-block'; 65 | } 66 | 67 | let shortModalClass = 'modal short-modal'; 68 | if (this.state.shortPreviewVisible) { 69 | shortModalClass += ' show d-block'; 70 | } 71 | 72 | let roModalClass = 'modal ro-modal'; 73 | if (this.state.roPreviewVisible) { 74 | roModalClass += ' show d-block'; 75 | } 76 | 77 | return ( 78 |
    79 |

    Preview

    80 | 87 | 94 | 101 | 102 | {this.state.previewVisible && ( 103 |
    104 |
    105 |
    106 | 118 | 119 |
    120 | 128 |
    129 |
    130 |
    131 |
    132 | )} 133 | 134 | {this.state.roPreviewVisible && ( 135 |
    136 |
    137 |
    138 | 151 | 152 |
    153 | 161 |
    162 |
    163 |
    164 |
    165 | )} 166 | 167 | {this.state.shortPreviewVisible && ( 168 |
    169 |
    170 |
    171 | 182 | 183 |
    184 | 192 |
    193 |
    194 |
    195 |
    196 | )} 197 |
    198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /examples/demo/demobar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ReactFormGenerator, 4 | ElementStore, 5 | } from '@threehippies/react-form-builder'; 6 | 7 | export default class Demobar extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | data: [], 12 | previewVisible: false, 13 | shortPreviewVisible: false, 14 | roPreviewVisible: false, 15 | }; 16 | 17 | this._onUpdate = this._onChange.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | ElementStore.subscribe((state) => this._onUpdate(state.data)); 22 | } 23 | 24 | showPreview() { 25 | this.setState({ 26 | previewVisible: true, 27 | }); 28 | } 29 | 30 | showShortPreview() { 31 | this.setState({ 32 | shortPreviewVisible: true, 33 | }); 34 | } 35 | 36 | showRoPreview() { 37 | this.setState({ 38 | roPreviewVisible: true, 39 | }); 40 | } 41 | 42 | closePreview() { 43 | this.setState({ 44 | previewVisible: false, 45 | shortPreviewVisible: false, 46 | roPreviewVisible: false, 47 | }); 48 | } 49 | 50 | _onChange(data) { 51 | this.setState({ 52 | data, 53 | }); 54 | } 55 | 56 | render() { 57 | let modalClass = 'modal'; 58 | if (this.state.previewVisible) { 59 | modalClass += ' show d-block'; 60 | } 61 | 62 | let shortModalClass = 'modal short-modal'; 63 | if (this.state.shortPreviewVisible) { 64 | shortModalClass += ' show d-block'; 65 | } 66 | 67 | let roModalClass = 'modal ro-modal'; 68 | if (this.state.roPreviewVisible) { 69 | roModalClass += ' show d-block'; 70 | } 71 | 72 | return ( 73 |
    74 |

    Preview

    75 | 82 | 89 | 96 | 97 | {this.state.previewVisible && ( 98 |
    99 |
    100 |
    101 | 112 | 113 |
    114 | 122 |
    123 |
    124 |
    125 |
    126 | )} 127 | 128 | {this.state.roPreviewVisible && ( 129 |
    130 |
    131 |
    132 | 145 | 146 |
    147 | 155 |
    156 |
    157 |
    158 |
    159 | )} 160 | 161 | {this.state.shortPreviewVisible && ( 162 |
    163 |
    164 |
    165 | 176 | 177 |
    178 | 186 |
    187 |
    188 |
    189 |
    190 | )} 191 |
    192 | ); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Form Builder 7 | 8 | 10 | 11 | 30 | 31 | 32 | 33 | 37 | 38 |
    39 |

    Preview

    40 | 41 | 53 |
    54 |
    55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/form-elements/date-picker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { format, parse, parseISO } from 'date-fns'; 3 | import ReactDatePicker from 'react-datepicker'; 4 | import ComponentHeader from './component-header'; 5 | import ComponentLabel from './component-label'; 6 | 7 | class DatePicker extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.inputField = React.createRef(); 11 | 12 | const { formatMask } = DatePicker.updateFormat(props, null); 13 | this.state = DatePicker.updateDateTime(props, { formatMask }, formatMask); 14 | } 15 | 16 | // formatMask = ''; 17 | 18 | handleChange = (dt) => { 19 | let placeholder; 20 | const { formatMask } = this.state; 21 | if (dt && dt.target) { 22 | placeholder = 23 | dt && dt.target && dt.target.value === '' 24 | ? formatMask.toLowerCase() 25 | : ''; 26 | const formattedDate = dt.target.value 27 | ? format(parseISO(dt.target.value), formatMask) 28 | : ''; 29 | this.setState({ 30 | value: formattedDate, 31 | internalValue: formattedDate, 32 | placeholder, 33 | }); 34 | } else { 35 | this.setState({ 36 | value: dt ? format(dt, formatMask) : '', 37 | internalValue: dt, 38 | placeholder, 39 | }); 40 | } 41 | }; 42 | 43 | static updateFormat(props, oldFormatMask) { 44 | const { showTimeSelect, showTimeSelectOnly, showTimeInput } = props.data; 45 | const dateFormat = 46 | showTimeSelect && showTimeSelectOnly ? '' : props.data.dateFormat; 47 | const timeFormat = 48 | showTimeSelect || showTimeInput ? props.data.timeFormat : ''; 49 | const formatMask = `${dateFormat} ${timeFormat}`.trim(); 50 | const updated = formatMask !== oldFormatMask; 51 | 52 | return { updated, formatMask }; 53 | } 54 | 55 | static updateDateTime(props, state, formatMask) { 56 | let value; 57 | let internalValue; 58 | const { defaultToday } = props.data; 59 | if ( 60 | defaultToday && 61 | (props.defaultValue === '' || props.defaultValue === undefined) 62 | ) { 63 | value = format(new Date(), formatMask); 64 | internalValue = new Date(); 65 | } else { 66 | value = props.defaultValue; 67 | 68 | if (value === '' || value === undefined) { 69 | internalValue = undefined; 70 | } else { 71 | internalValue = parse(value, state.formatMask, new Date()); 72 | } 73 | } 74 | return { 75 | value, 76 | internalValue, 77 | placeholder: formatMask.toLowerCase(), 78 | defaultToday, 79 | formatMask: state.formatMask, 80 | }; 81 | } 82 | 83 | // componentWillReceiveProps(props) { 84 | // const formatUpdated = this.updateFormat(props); 85 | // if ((props.data.defaultToday !== !this.state.defaultToday) || formatUpdated) { 86 | // const state = this.updateDateTime(props, this.formatMask); 87 | // this.setState(state); 88 | // } 89 | // } 90 | 91 | static getDerivedStateFromProps(props, state) { 92 | const { updated, formatMask } = DatePicker.updateFormat( 93 | props, 94 | state.formatMask 95 | ); 96 | if (props.data.defaultToday !== state.defaultToday || updated) { 97 | const newState = DatePicker.updateDateTime(props, state, formatMask); 98 | return newState; 99 | } 100 | return null; 101 | } 102 | 103 | render() { 104 | const { showTimeSelect, showTimeSelectOnly, showTimeInput } = 105 | this.props.data; 106 | const props = {}; 107 | props.type = 'date'; 108 | props.className = 'form-control'; 109 | props.name = this.props.data.field_name; 110 | const readOnly = this.props.data.readOnly || this.props.read_only; 111 | const iOS = 112 | /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 113 | const placeholderText = this.state.formatMask.toLowerCase(); 114 | 115 | if (this.props.mutable) { 116 | props.defaultValue = this.props.defaultValue; 117 | props.ref = this.inputField; 118 | } 119 | 120 | let baseClasses = 'SortableItem rfb-item'; 121 | if (this.props.data.pageBreakBefore) { 122 | baseClasses += ' alwaysbreak'; 123 | } 124 | 125 | return ( 126 |
    127 | 128 |
    129 | 130 |
    131 | {readOnly && ( 132 | 141 | )} 142 | {iOS && !readOnly && ( 143 | 152 | )} 153 | {!iOS && !readOnly && ( 154 | 170 | )} 171 |
    172 |
    173 |
    174 | ); 175 | } 176 | } 177 | 178 | export default DatePicker; 179 | --------------------------------------------------------------------------------