├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── cssPreprocess.js ├── demo ├── app.js └── index.html ├── package.json ├── postcss.config.js ├── src ├── components │ ├── Container.js │ ├── EditElement.js │ ├── FormBuilder.js │ ├── FormField.js │ ├── FormGenerator.js │ ├── Settings.js │ ├── Toolbar.js │ ├── editFields │ │ ├── ColorPicker.js │ │ ├── Editor.js │ │ ├── ImageUploader.js │ │ ├── Input.js │ │ ├── MceEditor.js │ │ ├── Multiple.js │ │ ├── RadioButtons.js │ │ ├── Switch.js │ │ └── Uploader.js │ └── formElements │ │ ├── Checkboxes.js │ │ ├── DownloadFile.js │ │ ├── Editor.js │ │ ├── EditorComponent.js │ │ ├── EmptyComponent.js │ │ ├── File.js │ │ ├── Image.js │ │ ├── InlineToolbar.js │ │ ├── Link.js │ │ ├── LinkInput.js │ │ ├── Pdf.js │ │ ├── RadioButtons.js │ │ ├── Range.js │ │ └── Video.js ├── constants │ └── componentsDefaults.js ├── contexts │ ├── ComponentsContext.js │ ├── EditModalContext.js │ ├── FileUrlContext.js │ └── MceLanguageUrl.js ├── css │ ├── colorPicker.scss │ ├── editor.scss │ ├── editorInput.scss │ ├── editorToolbar.scss │ ├── formBuilder.scss │ ├── image.scss │ ├── imageUploader.scss │ ├── multipleField.scss │ ├── options.scss │ ├── pdf.scss │ ├── range.scss │ ├── sortableRow.scss │ ├── toolbar.scss │ └── video.scss ├── hocs │ ├── withComponentsContext.js │ ├── withData.js │ ├── withElementWrapper.js │ ├── withFieldWrapper.js │ └── withFileUrlContext.js ├── index.js ├── ru.js └── utils │ ├── dnd.js │ ├── editor.js │ ├── files.js │ └── methods.js ├── webpack.config.js ├── webpack ├── common.config.js ├── dev.config.js └── production.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | ["babel-plugin-css-modules-transform", { 9 | "preprocessCss": "./cssPreprocess.js", 10 | "extensions": [".scss"], 11 | "extractCss": "./lib/main.css" 12 | }], 13 | ["import", { "libraryName": "antd", "style": true }] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react"], 4 | "rules": { 5 | "no-await-in-loop": "error", 6 | "no-cond-assign": ["error", "always"], 7 | "no-console": "warn", 8 | "no-constant-condition": "warn", 9 | "no-control-regex": "error", 10 | "no-debugger": "warn", 11 | "no-dupe-args": "error", 12 | "no-dupe-keys": "error", 13 | "no-duplicate-case": "error", 14 | "no-empty": "error", 15 | "no-empty-character-class": "error", 16 | "no-ex-assign": "error", 17 | "no-extra-boolean-cast": "warn", 18 | "no-extra-semi": "error", 19 | "no-func-assign": "error", 20 | "no-inner-declarations": "error", 21 | "no-invalid-regexp": "error", 22 | "no-irregular-whitespace": "error", 23 | "no-obj-calls": "warn", 24 | "no-prototype-builtins": "error", 25 | "no-regex-spaces": "error", 26 | "no-sparse-arrays": "error", 27 | "no-template-curly-in-string": "error", 28 | "no-unexpected-multiline": "warn", 29 | "no-unreachable": "error", 30 | "no-unsafe-finally": "error", 31 | "no-unsafe-negation": "warn", 32 | "use-isnan": "warn", 33 | "valid-typeof": ["warn", { "requireStringLiterals": true }], 34 | "block-spacing": ["warn", "always"], 35 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }], 36 | "camelcase": ["error", { "properties": "never" }], 37 | "comma-spacing": ["error", { "before": false, "after": true }], 38 | "comma-style": ["error", "last"], 39 | "computed-property-spacing": ["error", "never"], 40 | "eol-last": ["error", "always"], 41 | "func-call-spacing": ["error", "never"], 42 | "func-names": "warn", 43 | "indent": ["warn", 4, {"SwitchCase": 1}], 44 | "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 45 | "keyword-spacing": ["error", { 46 | "before": true, 47 | "after": true, 48 | "overrides": { 49 | "return": { "after": true }, 50 | "throw": { "after": true }, 51 | "case": { "after": true } 52 | } 53 | }], 54 | "linebreak-style": ["error", "unix"], 55 | "lines-around-directive": ["error", { 56 | "before": "always", 57 | "after": "always" 58 | }], 59 | "max-len": ["warn", 160, 2, { 60 | "ignoreUrls": true, 61 | "ignoreComments": false, 62 | "ignoreRegExpLiterals": true, 63 | "ignoreStrings": true, 64 | "ignoreTemplateLiterals": true 65 | }], 66 | "new-cap": ["error", { 67 | "newIsCap": true, 68 | "newIsCapExceptions": [], 69 | "capIsNew": false, 70 | "capIsNewExceptions": ["Immutable.Map", "Immutable.Set", "Immutable.List"], 71 | }], 72 | "new-parens": "error", 73 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 4 }], 74 | "no-array-constructor": "error", 75 | "no-bitwise": "error", 76 | "no-continue": "error", 77 | "no-lonely-if": "error", 78 | "no-mixed-operators": ["warn", { 79 | "groups": [ 80 | ["+", "-", "*", "/", "%", "**"], 81 | ["&", "|", "^", "~", "<<", ">>", ">>>"], 82 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="], 83 | ["in", "instanceof"] 84 | ], 85 | "allowSamePrecedence": false 86 | }], 87 | "no-mixed-spaces-and-tabs": "error", 88 | "no-multi-assign": ["error"], 89 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }], 90 | "no-new-object": "error", 91 | "no-plusplus": "error", 92 | "no-restricted-syntax": [ 93 | "error", 94 | "ForInStatement", 95 | "ForOfStatement", 96 | "LabeledStatement", 97 | "WithStatement", 98 | ], 99 | "no-spaced-func": "error", 100 | "no-tabs": "error", 101 | "no-trailing-spaces": "warn", 102 | "no-unneeded-ternary": ["error", { "defaultAssignment": false }], 103 | "object-property-newline": ["error", { 104 | "allowMultiplePropertiesPerLine": true 105 | }], 106 | "one-var": ["error", "never"], 107 | "one-var-declaration-per-line": ["error", "always"], 108 | "operator-assignment": ["error", "always"], 109 | "padded-blocks": ["error", "never"], 110 | "quote-props": ["warn", "consistent"], 111 | "quotes": ["warn", "single", { "avoidEscape": true }], 112 | "semi": ["warn", "always"], 113 | "semi-spacing": ["error", { "before": false, "after": true }], 114 | "space-before-blocks": "error", 115 | "space-before-function-paren": ["error", { 116 | "anonymous": "always", 117 | "named": "never", 118 | "asyncArrow": "always" 119 | }], 120 | "space-in-parens": ["error", "never"], 121 | "space-infix-ops": "warn", 122 | "space-unary-ops": ["warn", { 123 | "words": true, 124 | "nonwords": false, 125 | "overrides": {} 126 | }], 127 | "spaced-comment": ["warn", "always", { 128 | "line": { 129 | "exceptions": ["-", "+"], 130 | "markers": ["=", "!"] 131 | }, 132 | "block": { 133 | "exceptions": ["-", "+"], 134 | "markers": ["=", "!"], 135 | "balanced": false 136 | } 137 | }], 138 | "unicode-bom": ["error", "never"], 139 | 140 | "arrow-spacing": ["warn", { "before": true, "after": true }], 141 | "constructor-super": "error", 142 | "generator-star-spacing": ["error", { "before": false, "after": true }], 143 | "no-class-assign": "error", 144 | "no-const-assign": "error", 145 | "no-dupe-class-members": "error", 146 | "no-new-symbol": "error", 147 | "no-this-before-super": "error", 148 | "no-useless-computed-key": "warn", 149 | "no-useless-constructor": "warn", 150 | "no-useless-rename": ["warn", { 151 | "ignoreDestructuring": false, 152 | "ignoreImport": false, 153 | "ignoreExport": false 154 | }], 155 | "no-var": "warn", 156 | "object-shorthand": ["warn", "always", { 157 | "ignoreConstructors": false, 158 | "avoidQuotes": true 159 | }], 160 | "prefer-arrow-callback": ["warn", { 161 | "allowNamedFunctions": false, 162 | "allowUnboundThis": true 163 | }], 164 | "prefer-const": ["warn", { 165 | "destructuring": "any", 166 | "ignoreReadBeforeAssign": true 167 | }], 168 | "prefer-numeric-literals": "warn", 169 | "prefer-spread": "warn", 170 | "prefer-template": "warn", 171 | "require-yield": "warn", 172 | "rest-spread-spacing": ["warn", "never"], 173 | "symbol-description": "error", 174 | "template-curly-spacing": "warn", 175 | "yield-star-spacing": ["warn", "after"], 176 | 177 | "react/jsx-max-props-per-line": ["warn", { "maximum": 1, "when": "multiline" }], 178 | "react/no-unknown-property": "error", 179 | "react/prefer-es6-class": ["error", "always"], 180 | "react/react-in-jsx-scope": "error", 181 | "react/require-render-return": "error", 182 | "react/jsx-space-before-closing": ["warn", "always"] 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /reports 3 | /dist 4 | /.tmp 5 | /lib 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .babelrc 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | version: 1.0 4 | node_js: stable 5 | 6 | script: 7 | - echo "skipping tests" 8 | 9 | jobs: 10 | include: 11 | - stage: npm release 12 | node_js: stable 13 | before_deploy: 14 | - yarn install 15 | - yarn package 16 | deploy: 17 | skip_cleanup: true 18 | edge: true 19 | provider: npm 20 | email: "webdev@experium.ru" 21 | api_key: "$NPM_API_KEY" 22 | on: 23 | branch: master 24 | - stage: deploy gh pages 25 | before_deploy: 26 | - yarn install 27 | - yarn build 28 | deploy: 29 | provider: pages 30 | skip-cleanup: true 31 | github-token: "$GITHUB_TOKEN" 32 | keep-history: true 33 | local-dir: dist 34 | on: 35 | branch: master 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React editor 2 | 3 | ## `` 4 | 5 | Form editor component 6 | 7 | ### FormBuilder Props 8 | 9 | - `components`: custom form editor [fields](#form-editor-field). 10 | - `getComponents`: map form editor fields. 11 | - `data`: form editor data. 12 | - `showSimple`: form editor correct field values. 13 | - `uploadUrl`: form editor url for file upload. 14 | - `downloadUrl`: form editor url for file download. 15 | - `uploadImages`: form editor upload by url images for options. 16 | - `placeholder`: default text string for new items in radio buttons and checkboxes. 17 | - `onChange`: `onChange` handler will be called when form data will be changed. 18 | - `onCopy`: `onCopy` handler will be called when copy element. 19 | - `onPreviewOpen`: `onPreviewOpen` handler will be called when preview modal will be opened. 20 | - `onPreviewClose`: `onPreviewClose` handler will be called when preview modal will be closed. 21 | 22 | ### Form editor fields 23 | 24 | ```js 25 | const formEditorItems = [ 26 | { 27 | type: 'Editor', 28 | name: 'Текст', 29 | icon: 'font', 30 | renderInfo: props => props.content, 31 | component: Editor, 32 | formComponent: EditorComponent, 33 | props: { 34 | content: '' 35 | }, 36 | staticContent: true, 37 | fields: [ 38 | { type: 'editor', label: 'Текст', prop: 'content' } 39 | ] 40 | } 41 | ]; 42 | ``` 43 | 44 | - `type`: type of item. Must be unique. 45 | - `name`: text in toolbar. 46 | - `icon`: icon in toolbar. Use fontawesome version 4. 47 | - `renderInfo`: function renders data in draggable row. 48 | - `component`: component which will be respresent in draggable row. 49 | - `formComponent`: component which will be respresent in form. 50 | - `props`: field props. 51 | - `staticContent`: `true` if field isn't changable. 52 | - `fields`: [array of fields](#edit-modal-field-types), which will be shown in field edit modal. 53 | 54 | #### Edit modal field types 55 | 56 | - `editor`: text editor field. 57 | - `multiple`: field for multiple items such as radio buttons and checkboxes. 58 | - `input`: input field. 59 | - `switch`: switch field. 60 | 61 | ## `` 62 | 63 | - `values`: form values. 64 | - `data`: form editor data. 65 | - `onSubmit`: `onSubmit` handler will be called when a user submits the form and all validation passes. 66 | -------------------------------------------------------------------------------- /cssPreprocess.js: -------------------------------------------------------------------------------- 1 | var sass = require('node-sass'); 2 | var path = require('path'); 3 | 4 | module.exports = function processSass(data, filename) { 5 | var result; 6 | result = sass.renderSync({ 7 | data: data, 8 | file: filename 9 | }).css; 10 | return result.toString('utf8'); 11 | }; 12 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { FormBuilder, FormGenerator } from '../src/index'; 5 | 6 | const saveState = !!window.location.search; 7 | const isPreview = saveState && window.location.search.includes('preview=1'); 8 | const state = localStorage.getItem('editor:data'); 9 | const uploadImages = false; 10 | 11 | const Builder = () => { 12 | const [copyItemState, setCopyItemState] = useState(null); 13 | 14 | return ( 15 | uploadImages ? '/api/files' : undefined} 17 | downloadUrl={id => `/api/files/${id}/view`} 18 | uploadImages={uploadImages} 19 | withoutUrl={!uploadImages} 20 | placeholder='Тестовый выбор' 21 | submitText='Отправить' 22 | data={saveState && state ? JSON.parse(state) : undefined} 23 | copyItemData={copyItemState} 24 | onChange={data => saveState && localStorage.setItem('editor:data', JSON.stringify(data))} 25 | onCopy={item => setCopyItemState(item) } 26 | mceOnInit={editor => { 27 | editor.target.editorCommands.execCommand('fontSize', false, '17px'); 28 | }} 29 | /> 30 | ); 31 | }; 32 | 33 | ReactDOM.render( 34 | isPreview ? ( 35 | `/api/files/${id}/view`} 40 | onSubmit={result => { 41 | localStorage.setItem('editor:data', JSON.stringify({ 42 | ...JSON.parse(state), 43 | result, 44 | })); 45 | }} 46 | /> 47 | ) : ( 48 | 49 | ), 50 | document.getElementById('root') 51 | ); 52 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Player Builder 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@experium/react-editor", 3 | "main": "./lib/index.js", 4 | "version": "1.3.78", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "start": "webpack-dev-server", 10 | "build": "NODE_ENV=production webpack", 11 | "lint": "eslint src/** -f checkstyle -o reports/eslint.xml", 12 | "package": "babel src --out-dir lib" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "@ant-design/icons": "^4.2.2", 17 | "@babel/plugin-proposal-class-properties": "^7.5.5", 18 | "@tinymce/tinymce-react": "^3.6.1", 19 | "antd": "^4.4.3", 20 | "classnames": "^2.2.6", 21 | "draft-js": "^0.11.7", 22 | "draft-js-export-html": "^1.4.1", 23 | "draft-js-import-html": "^1.4.1", 24 | "final-form": "^4.20.1", 25 | "final-form-arrays": "^3.0.2", 26 | "font-awesome": "^4.7.0", 27 | "prop-types": "^15.7.2", 28 | "ramda": "^0.27.1", 29 | "react": "^16.13.1", 30 | "react-beautiful-dnd": "^13.0.0", 31 | "react-color": "^2.18.1", 32 | "react-dom": "^16.13.1", 33 | "react-final-form": "^6.5.1", 34 | "react-final-form-arrays": "^3.1.2", 35 | "react-medium-image-zoom": "^4.3.1", 36 | "react-onclickoutside": "^6.9.0", 37 | "react-pdf": "^4.2.0", 38 | "react-player": "^2.6.2", 39 | "styled-components": "^5.1.0", 40 | "uniqid": "^4.1.1" 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "^7.6.0", 44 | "@babel/core": "^7.6.0", 45 | "@babel/preset-env": "^7.6.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "babel-eslint": "^10.1.0", 48 | "babel-loader": "^8.1.0", 49 | "babel-plugin-css-modules-transform": "^1.6.2", 50 | "babel-plugin-import": "^1.13.0", 51 | "clean-webpack-plugin": "^3.0.0", 52 | "copy-webpack-plugin": "^5.0.4", 53 | "css-loader": "^4.3.0", 54 | "eslint": "^6.4.0", 55 | "eslint-loader": "^3.0.0", 56 | "eslint-plugin-import": "^2.18.2", 57 | "eslint-plugin-react": "^7.14.3", 58 | "file-loader": "^6.1.0", 59 | "mini-css-extract-plugin": "^0.11.2", 60 | "node-sass": "^4.12.0", 61 | "postcss-loader": "^3.0.0", 62 | "sass-loader": "^8.0.0", 63 | "style-loader": "^1.2.1", 64 | "url-loader": "^4.1.0", 65 | "webpack": "^4.44.2", 66 | "webpack-cli": "^3.3.12", 67 | "webpack-dev-server": "^3.11.0", 68 | "webpack-merge": "^4.2.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/Container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Droppable, Draggable } from 'react-beautiful-dnd'; 4 | import cx from 'classnames'; 5 | import { find, propEq, path } from 'ramda'; 6 | 7 | import styles from '../css/formBuilder.scss'; 8 | import withComponentsContext from '../hocs/withComponentsContext'; 9 | import EmptyComponent from './formElements/EmptyComponent'; 10 | 11 | class Container extends Component { 12 | static propTypes = { 13 | items: PropTypes.array, 14 | reorderItems: PropTypes.func, 15 | editItem: PropTypes.func, 16 | removeItem: PropTypes.func, 17 | copyItem: PropTypes.func, 18 | elements: PropTypes.object, 19 | placeholder: PropTypes.string 20 | }; 21 | 22 | renderItem = (id, dragHandleProps, isDraggingOver) => { 23 | const { removeItem, editItem, copyItem, elements, placeholder, simpleView, editAllItem, components } = this.props; 24 | const item = path([id], elements); 25 | const element = find(propEq('type', item.type), components); 26 | 27 | const Component = element ? element.component : EmptyComponent; 28 | 29 | return ; 41 | } 42 | 43 | getElement = (id) => { 44 | const { elements, components } = this.props; 45 | const item = path([id], elements); 46 | 47 | if (!element) { 48 | return null; 49 | } 50 | 51 | return element; 52 | } 53 | 54 | render() { 55 | const { items } = this.props; 56 | 57 | return
58 |
59 | 60 | { (provided, snapshot) => 61 |
62 | { items.map((id, index) => 63 | 67 | { provided => 68 |
71 | { this.renderItem(id, provided.dragHandleProps, snapshot.isDraggingOver) } 72 |
73 | } 74 |
75 | )} 76 | { provided.placeholder } 77 |
78 | } 79 |
80 |
81 |
; 82 | } 83 | } 84 | 85 | export default withComponentsContext(Container); 86 | -------------------------------------------------------------------------------- /src/components/EditElement.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { find, propEq, path, omit } from 'ramda'; 3 | import { Form, Field } from 'react-final-form'; 4 | import { Form as FormComponent, Button, Popconfirm } from 'antd'; 5 | import arrayMutators from 'final-form-arrays'; 6 | import { FieldArray } from 'react-final-form-arrays'; 7 | import cx from 'classnames'; 8 | import { createGlobalStyle } from 'styled-components'; 9 | 10 | import styles from '../css/editor.scss'; 11 | import formBuilderStyles from '../css/formBuilder.scss'; 12 | import FileUrlContext from '../contexts/FileUrlContext'; 13 | import FormField from './FormField'; 14 | import Input from './editFields/Input'; 15 | import MceEditor from './editFields/MceEditor'; 16 | import Multiple from './editFields/Multiple'; 17 | import Switch from './editFields/Switch'; 18 | import withComponentsContext from '../hocs/withComponentsContext'; 19 | import Uploader from './editFields/Uploader'; 20 | import RadioButtons from './editFields/RadioButtons'; 21 | 22 | const FIELDS = { 23 | editor: MceEditor, 24 | input: Input, 25 | multiple: Multiple, 26 | switch: Switch, 27 | uploader: Uploader, 28 | radiobuttons: RadioButtons 29 | }; 30 | 31 | const GlobalStyle = createGlobalStyle` 32 | .tox-notifications-container { 33 | display: none; 34 | } 35 | .tox-tinymce-inline { 36 | z-index: 1000; 37 | } 38 | `; 39 | 40 | class EditElement extends Component { 41 | renderPreview = item => { 42 | const { components } = this.props; 43 | const options = find(propEq('type', item.type), components); 44 | const { staticContent, formComponent: Component } = options; 45 | 46 | return staticContent ? 47 | 48 | {fileContext => ( 49 | 50 | )} 51 | : 52 | ; 63 | } 64 | 65 | render() { 66 | const { item, placeholder, onSubmit, components, onCancel } = this.props; 67 | const { fields = [], hidePreview } = find(propEq('type', item.type), components); 68 | 69 | return
70 | 71 |
76 |
77 |
78 | 79 |
80 | { fields.map(item => (path(['props', 'cond'], item) ? item.props.cond(values) : true) && FIELDS[item.type] ? 81 | (item.fieldArray ? 82 | : 90 | 98 | ) : null 99 | )} 100 |
101 |
102 | 103 | 106 | 111 | 114 | 115 | 116 |
117 |
118 |
119 | { !hidePreview && 120 |
121 | { this.renderPreview(values) } 122 |
123 | } 124 |
125 | } /> 126 |
; 127 | } 128 | } 129 | 130 | export default withComponentsContext(EditElement); 131 | -------------------------------------------------------------------------------- /src/components/FormBuilder.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, Button } from 'antd'; 4 | import { DragDropContext } from 'react-beautiful-dnd'; 5 | import { contains } from 'ramda'; 6 | import cx from 'classnames'; 7 | import { EyeOutlined, SettingOutlined, CopyOutlined } from '@ant-design/icons'; 8 | 9 | import Toolbar from './Toolbar'; 10 | import Container from './Container'; 11 | import withData from '../hocs/withData'; 12 | import { FormGenerator } from './FormGenerator'; 13 | import { reorder } from '../utils/dnd'; 14 | import styles from '../css/formBuilder.scss'; 15 | import Settings from './Settings'; 16 | import FileUrlContext from '../contexts/FileUrlContext'; 17 | import MceLanguageUrl from '../contexts/MceLanguageUrl'; 18 | 19 | class FormBuilderComponent extends Component { 20 | static propTypes = { 21 | addItem: PropTypes.func, 22 | reorderItems: PropTypes.func, 23 | items: PropTypes.array, 24 | elements: PropTypes.object, 25 | onPreviewOpen: PropTypes.func, 26 | onPreviewClose: PropTypes.func, 27 | placeholder: PropTypes.string 28 | }; 29 | 30 | state = { 31 | preview: false, 32 | showSettings: false 33 | }; 34 | 35 | openPreview = () => { 36 | const { onPreviewOpen } = this.props; 37 | 38 | this.setState({ preview: true }); 39 | onPreviewOpen && onPreviewOpen(); 40 | }; 41 | 42 | closePreview = () => { 43 | const { onPreviewClose } = this.props; 44 | 45 | this.setState({ preview: false }); 46 | onPreviewClose && onPreviewClose(); 47 | }; 48 | 49 | onDragEnd = result => { 50 | if (!result.destination) { 51 | return; 52 | } 53 | 54 | if (contains('field', result.type)) { 55 | const id = result.destination.droppableId; 56 | this.props.editItem(id, 'options', reorder(this.props.elements[id].options, result.source.index, result.destination.index)); 57 | return; 58 | } 59 | 60 | if (result.source.droppableId === 'toolbar') { 61 | this.props.addItem(result.draggableId, result.destination.index); 62 | } else { 63 | this.props.reorderItems(result.source.index, result.destination.index); 64 | } 65 | } 66 | 67 | openSettings = () => this.setState({ showSettings: true }); 68 | 69 | closeSettings = () => this.setState({ showSettings: false }); 70 | 71 | editCommonSettings = settings => { 72 | this.props.editCommonSettings(settings); 73 | this.closeSettings(); 74 | } 75 | 76 | addCopy = () => { 77 | this.props.addCopy(this.props.copyItemData); 78 | } 79 | 80 | render() { 81 | const { 82 | items, 83 | elements, 84 | addItem, 85 | components, 86 | commonSettings, 87 | uploadUrl, 88 | downloadUrl, 89 | uploadImages, 90 | submitText, 91 | mceLanguageUrl, 92 | mceOnInit, 93 | tinymceScriptSrc, 94 | copyItemData, 95 | } = this.props; 96 | 97 | return
98 |
99 |
100 | 101 | 107 | 112 | {!!copyItemData && ( 113 | 119 | )} 120 | 121 |
122 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 143 | 150 | 151 | 158 | 161 | 162 |
163 |
; 164 | } 165 | } 166 | 167 | export const FormBuilder = withData(FormBuilderComponent); 168 | -------------------------------------------------------------------------------- /src/components/FormField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, Field } from 'react-final-form'; 4 | import { Form as FormComponent } from 'antd'; 5 | import { isNil } from 'ramda'; 6 | 7 | import withFileUrlContext from '../hocs/withFileUrlContext'; 8 | 9 | const required = value => !value ? 'Обязательно для заполнения' : undefined; 10 | const incorrect = (value, correct) => value !== correct ? 'Неправильный ответ' : undefined; 11 | 12 | class FormField extends Component { 13 | static propTypes = { 14 | item: PropTypes.object, 15 | options: PropTypes.object, 16 | component: PropTypes.func, 17 | value: PropTypes.any, 18 | id: PropTypes.string, 19 | fieldType: PropTypes.string, 20 | preview: PropTypes.bool, 21 | view: PropTypes.bool 22 | }; 23 | 24 | renderField = () => { 25 | const { id, component: Component, item, options, fieldType, value, view, noCheckCorrect, downloadUrl } = this.props; 26 | const disabled = !isNil(value) || view; 27 | 28 | return disabled ? undefined : ( 32 | (item.required && (options.requiredValidator ? options.requiredValidator(value, item) : required(value))) 33 | || (!noCheckCorrect && item.allowCorrect && ( 34 | options.correctValidator ? options.correctValidator(value, item.correct, item) : incorrect(value, item.correct) 35 | )) 36 | || undefined 37 | )} 38 | fieldType={fieldType} 39 | id={id} 40 | disabled={disabled} 41 | downloadUrl={downloadUrl} 42 | {...item} 43 | allowCorrect={!noCheckCorrect && item.allowCorrect} />; 44 | } 45 | 46 | render() { 47 | const { isField } = this.props; 48 | 49 | return isField ? 50 | this.renderField() : 51 | {}} 54 | subscription={{ submitting: true, submitFailed: true, error: true}} 55 | render={({ handleSubmit }) => 56 | 57 | { this.renderField() } 58 | 59 | } />; 60 | } 61 | } 62 | 63 | export default withFileUrlContext(FormField); 64 | -------------------------------------------------------------------------------- /src/components/FormGenerator.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row, Col, Button, Form as FormComponent } from 'antd'; 4 | import { find, propEq, any, toPairs, concat, filter, addIndex, path, pathOr } from 'ramda'; 5 | import { DragDropContext } from 'react-beautiful-dnd'; 6 | import cx from 'classnames'; 7 | import { Form } from 'react-final-form'; 8 | 9 | import COMPONENTS_DEFAULTS from '../constants/componentsDefaults'; 10 | import FormField from './FormField'; 11 | import FileUrlContext from '../contexts/FileUrlContext'; 12 | import styles from '../css/formBuilder.scss'; 13 | import ComponentsContext from '../contexts/ComponentsContext'; 14 | 15 | export class FormGenerator extends Component { 16 | static propTypes = { 17 | data: PropTypes.object, 18 | onSubmit: PropTypes.func, 19 | renderFooter: PropTypes.func, 20 | preview: PropTypes.bool, 21 | values: PropTypes.object, 22 | components: PropTypes.array, 23 | view: PropTypes.bool 24 | }; 25 | 26 | static defaultProps = { 27 | data: {}, 28 | values: {}, 29 | disable: true, 30 | components: [] 31 | }; 32 | 33 | state = { 34 | page: 0 35 | }; 36 | 37 | onSubmit = (values, ...props) => { 38 | this.props.onSubmit && this.props.onSubmit(values); 39 | } 40 | 41 | getComponents = placeholder => { 42 | return concat(COMPONENTS_DEFAULTS(placeholder), this.props.components); 43 | } 44 | 45 | goBack = () => this.setState(prev => ({ page: prev.page - 1 })); 46 | 47 | goNext = (formProps, values) => { 48 | this.setState(prev => ({ page: prev.page + 1 })); 49 | formProps.reset(values); 50 | } 51 | 52 | renderFooter = footerProps => { 53 | const { renderFooter, staticContent, index, invalid, formProps, formValues, handleSubmit } = footerProps; 54 | const { data: { common = {}, items, elements }, submitText, onHandleSubmit } = this.props; 55 | const { page } = this.state; 56 | 57 | const components = COMPONENTS_DEFAULTS().concat(this.props.components); 58 | const showSubmit = any(([ _, item ]) => !path(['staticContent'], find(propEq('type', item.type), components)), toPairs(elements)); 59 | 60 | return renderFooter ? ( 61 | renderFooter({ 62 | ...footerProps, 63 | showSubmit, 64 | page, 65 | goNext: () => this.goNext(formProps, formValues), 66 | }, this.props) 67 | ) : ( 68 | 69 | { common.pages && page > 0 && 70 | 75 | } 76 | { showSubmit && ((!staticContent && common.everyQuestionSubmit) || (common.pages ? page : index) === items.length - 1) && 77 | 88 | } 89 | { common.pages && page < items.length - 1 && 90 | 95 | } 96 | 97 | ); 98 | } 99 | 100 | renderRow = (id, index, invalid, formProps, formValues, errors, submitFailed, handleSubmit) => { 101 | const { data: { elements = {} }, preview, values, view, disable, noCheckCorrect, placeholder, renderFooter } = this.props; 102 | const item = pathOr({}, [id], elements); 103 | const options = find(propEq('type', item.type), this.getComponents(placeholder)); 104 | 105 | if (!options) { 106 | return null; 107 | } 108 | 109 | const { staticContent, fieldType, formComponent: Component } = options; 110 | 111 | return 115 | 116 | { staticContent ? 117 | 118 | {fileContext => } 119 | : 120 | 131 | } 132 | 133 | { this.renderFooter({ renderFooter, staticContent, index, invalid, formProps, formValues, errors, submitFailed, handleSubmit }) } 134 | ; 135 | } 136 | 137 | hasFields = () => { 138 | const { elements = {} } = this.props.data; 139 | 140 | return any(([, item]) => !find(propEq('type', item.type), this.getComponents(placeholder)).staticContent, toPairs(elements)); 141 | } 142 | 143 | render() { 144 | const { data: { items = [], common = {} }, uploadUrl, downloadUrl, uploadImages, values } = this.props; 145 | const { page } = this.state; 146 | 147 | return
148 | 149 | 154 | 160 | 161 | 162 | { addIndex(filter)((item, index) => common.pages ? index === page : true, items) 163 | .map((row, index) => this.renderRow(row, index, invalid, form, values, errors, submitFailed, handleSubmit)) 164 | } 165 | 166 | 167 | } /> 168 | 169 | 170 |
; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/components/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Form, Field } from 'react-final-form'; 3 | import { Form as FormComponent, Button } from 'antd'; 4 | 5 | import Switch from './editFields/Switch'; 6 | import ColorPicker from './editFields/ColorPicker'; 7 | 8 | export default class Settings extends Component { 9 | render() { 10 | const { settings, onSubmit } = this.props; 11 | 12 | return 16 | 17 | 21 | 25 | 29 | 34 | 35 | } />; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Droppable, Draggable } from 'react-beautiful-dnd'; 4 | import cx from 'classnames'; 5 | 6 | import styles from '../css/toolbar.scss'; 7 | import ComponentsContext from '../contexts/ComponentsContext'; 8 | 9 | export default class Toolbar extends Component { 10 | static propTypes = { 11 | items: PropTypes.array 12 | }; 13 | 14 | renderToolbarList = items => { 15 | return items.map((item, index) => 16 | 20 | { (provided, snapshot) => 21 | 22 |
  • 27 |
    this.props.addItem(item.type)}> 28 | { item.name } 29 |
    30 |
  • 31 | { snapshot.isDragging && 32 |
    33 |
    34 | { item.name } 35 |
    36 |
    37 | } 38 |
    39 | } 40 |
    41 | ); 42 | } 43 | 44 | render() { 45 | return ( 46 |
    47 |

    Панель элементов

    48 | 49 | { provided => 50 |
      51 | { this.renderToolbarList } 52 |
      { provided.placeholder }
      53 |
    54 | } 55 |
    56 |
    57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/editFields/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ChromePicker } from 'react-color'; 3 | 4 | import withFieldWrapper from '../../hocs/withFieldWrapper'; 5 | import styles from '../../css/colorPicker.scss'; 6 | 7 | class ColorPicker extends Component { 8 | state = { 9 | open: false 10 | }; 11 | 12 | onChange = ({ rgb }) => { 13 | this.props.onChange(`rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`); 14 | } 15 | 16 | toggle = () => this.setState(prev => ({ open: !prev.open })); 17 | 18 | close = () => this.setState({ open: false }); 19 | 20 | render() { 21 | const { input: { value }} = this.props; 22 | 23 | return
    24 |
    25 |
    26 |
    27 | { this.state.open && 28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 | } 35 |
    ; 36 | } 37 | } 38 | 39 | export default withFieldWrapper(ColorPicker); 40 | -------------------------------------------------------------------------------- /src/components/editFields/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import EditorComponent from '../formElements/EditorComponent'; 5 | import withFieldWrapper from '../../hocs/withFieldWrapper'; 6 | import styles from '../../css/editor.scss'; 7 | 8 | class Editor extends Component { 9 | onChange = (_, value) => { 10 | this.props.onChange(value); 11 | }; 12 | 13 | render() { 14 | const { short, input: { value }} = this.props; 15 | 16 | return
    17 | 23 |
    ; 24 | } 25 | } 26 | 27 | export default withFieldWrapper(Editor); 28 | -------------------------------------------------------------------------------- /src/components/editFields/ImageUploader.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Upload, Button } from 'antd'; 3 | import cx from 'classnames'; 4 | import { CloseOutlined, FileImageOutlined } from '@ant-design/icons'; 5 | 6 | import styles from '../../css/imageUploader.scss'; 7 | 8 | import withFileUrlContext from '../../hocs/withFileUrlContext'; 9 | import { getUrl } from '../../utils/files'; 10 | 11 | class ImageUploader extends Component { 12 | state = { 13 | error: false, 14 | } 15 | 16 | reader = new FileReader(); 17 | 18 | beforeUpload = file => { 19 | this.reader.readAsDataURL(file); 20 | this.reader.onload = () => this.props.input.onChange({ 21 | name: file.name, 22 | data: this.reader.result 23 | }); 24 | 25 | return false; 26 | } 27 | 28 | onChange = info => { 29 | const { status, response, name } = info.file; 30 | 31 | switch (status) { 32 | case 'done': 33 | this.setState({ error: false }); 34 | this.props.input.onChange({ 35 | name, 36 | id: response.id, 37 | }); 38 | break; 39 | case 'error': 40 | this.setState({ error: true }); 41 | break; 42 | default: 43 | return; 44 | } 45 | } 46 | 47 | remove = () => this.props.input.onChange(null); 48 | 49 | renderStaticUploader = () => ( 50 | 54 |