├── .babelrc
├── .editorconfig
├── .eslintrc.yaml
├── .gitignore
├── .npmignore
├── .storybook
├── addons.js
├── config.js
└── webpack.config.js
├── .travis.yml
├── README.md
├── ROADMAP.md
├── package.json
├── src
├── Address
│ └── index.jsx
├── BaseButton
│ ├── index.jsx
│ └── styled
│ │ └── button.jsx
├── BaseInput
│ ├── index.jsx
│ └── styled
│ │ ├── input.js
│ │ └── validation-icon.js
├── CheckboxField
│ ├── index.jsx
│ └── styled
│ │ ├── checkbox.js
│ │ ├── fieldset.js
│ │ ├── indicator.js
│ │ ├── inner-label.js
│ │ ├── label.js
│ │ └── wrapper.js
├── Converse
│ ├── formula.js
│ └── index.jsx
├── Date
│ ├── index.jsx
│ ├── labelTranslations.js
│ └── styled
│ │ ├── day-picker.js
│ │ ├── overlay-wrapper.js
│ │ ├── overlay.js
│ │ ├── validation-icon.js
│ │ └── wrapper.js
├── EmailField
│ ├── index.jsx
│ └── styled
│ │ └── email.js
├── FetchForm
│ └── index.jsx
├── Fieldset
│ ├── index.jsx
│ └── styled
│ │ ├── fieldset-inner.js
│ │ ├── fieldset.js
│ │ ├── legend.js
│ │ └── wrapper.js
├── FormRow
│ └── index.jsx
├── Hidden
│ ├── index.jsx
│ └── styled
│ │ └── hidden.js
├── IBAN
│ └── index.jsx
├── Input
│ ├── index.jsx
│ └── styled
│ │ ├── inner-wrapper.js
│ │ ├── input.js
│ │ ├── prefix.js
│ │ ├── suffix.js
│ │ ├── text-content.js
│ │ ├── validation-icon.js
│ │ └── wrapper.js
├── LookUp
│ ├── index.jsx
│ └── styled
│ │ └── validation-message.js
├── Number
│ └── index.jsx
├── Observables
│ ├── Field.jsx
│ ├── Form.js
│ └── Forms.js
├── ParagraphField
│ └── index.jsx
├── Parser
│ ├── index.js
│ └── template.js
├── PhoneField
│ ├── index.jsx
│ └── styled
│ │ └── phone.js
├── RadioField
│ ├── index.jsx
│ └── styled
│ │ ├── indicator.js
│ │ ├── radio-label.js
│ │ ├── radio.js
│ │ └── wrapper.js
├── RangeField
│ ├── index.jsx
│ └── styled
│ │ ├── range-value-wrapper.js
│ │ ├── range-value.js
│ │ ├── range.js
│ │ ├── validation-icon.js
│ │ └── wrapper.js
├── Relation
│ └── index.jsx
├── RuleHint
│ ├── index.jsx
│ └── styled
│ │ └── validation-message.js
├── SelectField
│ ├── index.jsx
│ └── styled
│ │ ├── validation-icon.js
│ │ └── wrapper.js
├── SubmitButton
│ └── index.jsx
├── TextArea
│ ├── index.jsx
│ └── styled
│ │ ├── text-area.js
│ │ └── validation-icon.js
├── Webform
│ ├── ThankYouMessage
│ │ ├── index.jsx
│ │ └── styled
│ │ │ └── message.js
│ ├── conditionals
│ │ ├── __tests__
│ │ │ └── formatCondtionals.test.js
│ │ └── index.js
│ ├── index.jsx
│ ├── rules.js
│ └── styled
│ │ ├── element.js
│ │ ├── form-title.js
│ │ ├── list-item.js
│ │ ├── list.js
│ │ ├── stdcss.0.0.7.js
│ │ └── webform.js
├── WebformElement
│ ├── index.jsx
│ └── styled
│ │ ├── form-row.js
│ │ ├── label.js
│ │ ├── required-marker.js
│ │ ├── text-content.js
│ │ └── validation-message.js
├── WebformUtils.js
├── Wizard
│ ├── WizardProgress
│ │ ├── index.jsx
│ │ └── styled
│ │ │ ├── bar.js
│ │ │ ├── step-number.js
│ │ │ ├── step-title.js
│ │ │ └── step.js
│ ├── index.jsx
│ └── styled
│ │ ├── button-next.js
│ │ ├── button-prev.js
│ │ └── button-wrapper.js
├── Wrapper
│ └── index.jsx
├── index.js
└── styles
│ └── theme.js
├── stories
├── RemoteForm.jsx
└── index.jsx
├── utils
├── postinstall.js
└── prepublish.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "targets": {
7 | "browsers": [
8 | "last 2 versions"
9 | ],
10 | "node": "current",
11 | "uglify": true
12 | }
13 | }
14 | ],
15 | "react",
16 | "stage-1"
17 | ],
18 | "plugins": [
19 | "transform-decorators-legacy",
20 | [
21 | "transform-runtime",
22 | {
23 | "polyfill": true
24 | }
25 | ],
26 | ["styled-components", {
27 | "ssr": true,
28 | "displayName": false
29 | }]
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | extends: airbnb
2 | rules:
3 | jsx-a11y/no-static-element-interactions: 0
4 | keyword-spacing:
5 | - 2
6 | -
7 | after: false
8 | overrides:
9 | from:
10 | after: true
11 | case:
12 | after: true
13 | return:
14 | after: true
15 | const:
16 | after: true
17 | let:
18 | after: true
19 | import:
20 | after: true
21 | export:
22 | after: true
23 | else:
24 | after: true
25 | try:
26 | after: true
27 | no-param-reassign:
28 | - 2
29 | -
30 | props: false
31 | max-len:
32 | - 2
33 | -
34 | code: 140
35 | ignoreComments: true
36 | ignoreUrls: true
37 | ignoreStrings: true
38 | ignoreTemplateLiterals: true
39 | jsx-quotes:
40 | - 2
41 | - prefer-single
42 | no-return-assign:
43 | - 0
44 | no-console:
45 | - 2
46 | -
47 | allow:
48 | - "warn"
49 | - "error"
50 | - "info"
51 | react/no-did-mount-set-state: 0
52 |
53 | parser: babel-eslint
54 | globals:
55 | window: true
56 | document: true
57 | requestAnimationFrame: true
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | lib
3 | storybook-static
4 | package-lock.json
5 | .vscode
6 |
7 | # Created by https://www.gitignore.io/api/node,phpstorm
8 |
9 | ### Node ###
10 | # Logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (http://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # Typescript v1 declaration files
49 | typings/
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional REPL history
58 | .node_repl_history
59 |
60 | # Output of 'npm pack'
61 | *.tgz
62 |
63 | # Yarn Integrity file
64 | .yarn-integrity
65 |
66 | # dotenv environment variables file
67 | .env
68 |
69 |
70 | ### PhpStorm ###
71 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
72 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
73 |
74 | # User-specific stuff:
75 | .idea/**/workspace.xml
76 | .idea/**/tasks.xml
77 |
78 | # Sensitive or high-churn files:
79 | .idea/**/dataSources/
80 | .idea/**/dataSources.ids
81 | .idea/**/dataSources.xml
82 | .idea/**/dataSources.local.xml
83 | .idea/**/sqlDataSources.xml
84 | .idea/**/dynamic.xml
85 | .idea/**/uiDesigner.xml
86 |
87 | # Gradle:
88 | .idea/**/gradle.xml
89 | .idea/**/libraries
90 |
91 | # Mongo Explorer plugin:
92 | .idea/**/mongoSettings.xml
93 |
94 | ## File-based project format:
95 | *.iws
96 |
97 | ## Plugin-specific files:
98 |
99 | # IntelliJ
100 | /out/
101 |
102 | # mpeltonen/sbt-idea plugin
103 | .idea_modules/
104 |
105 | # JIRA plugin
106 | atlassian-ide-plugin.xml
107 |
108 | # Crashlytics plugin (for Android Studio and IntelliJ)
109 | com_crashlytics_export_strings.xml
110 | crashlytics.properties
111 | crashlytics-build.properties
112 | fabric.properties
113 |
114 | ### PhpStorm Patch ###
115 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
116 |
117 | # *.iml
118 | # modules.xml
119 | # .idea/misc.xml
120 | # *.ipr
121 |
122 | # End of https://www.gitignore.io/api/node,phpstorm
123 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | # lib <-- we want to include this on npm!!
3 |
4 |
5 | # no storybook things on npm
6 | storybook-static
7 | .storybook
8 | stories
9 |
10 | # Created by https://www.gitignore.io/api/node,phpstorm
11 |
12 | ### Node ###
13 | # Logs
14 | logs
15 | *.log
16 | npm-debug.log*
17 | yarn-debug.log*
18 | yarn-error.log*
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (http://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 | jspm_packages/
50 |
51 | # Typescript v1 declaration files
52 | typings/
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Optional REPL history
61 | .node_repl_history
62 |
63 | # Output of 'npm pack'
64 | *.tgz
65 |
66 | # Yarn Integrity file
67 | .yarn-integrity
68 |
69 | # dotenv environment variables file
70 | .env
71 |
72 |
73 | ### PhpStorm ###
74 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
75 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
76 |
77 | # User-specific stuff:
78 | .idea/**/workspace.xml
79 | .idea/**/tasks.xml
80 |
81 | # Sensitive or high-churn files:
82 | .idea/**/dataSources/
83 | .idea/**/dataSources.ids
84 | .idea/**/dataSources.xml
85 | .idea/**/dataSources.local.xml
86 | .idea/**/sqlDataSources.xml
87 | .idea/**/dynamic.xml
88 | .idea/**/uiDesigner.xml
89 |
90 | # Gradle:
91 | .idea/**/gradle.xml
92 | .idea/**/libraries
93 |
94 | # Mongo Explorer plugin:
95 | .idea/**/mongoSettings.xml
96 |
97 | ## File-based project format:
98 | *.iws
99 |
100 | ## Plugin-specific files:
101 |
102 | # IntelliJ
103 | /out/
104 |
105 | # mpeltonen/sbt-idea plugin
106 | .idea_modules/
107 |
108 | # JIRA plugin
109 | atlassian-ide-plugin.xml
110 |
111 | # Crashlytics plugin (for Android Studio and IntelliJ)
112 | com_crashlytics_export_strings.xml
113 | crashlytics.properties
114 | crashlytics-build.properties
115 | fabric.properties
116 |
117 | ### PhpStorm Patch ###
118 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
119 |
120 | # *.iml
121 | # modules.xml
122 | # .idea/misc.xml
123 | # *.ipr
124 |
125 | # End of https://www.gitignore.io/api/node,phpstorm
126 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/headless-ninja/hn-react-webform/52137ff405287a1946e0d058b54f0d66579fd190/.storybook/addons.js
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@kadira/storybook';
2 |
3 | function loadStories() {
4 | require('../stories/index.jsx');
5 | }
6 |
7 | configure(loadStories, module);
8 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = {
4 | module: {
5 | loaders: [{
6 | test: /\.md$/,
7 | loader: "raw"
8 | }, {
9 | test: /\.json$/,
10 | loader: 'json'
11 | }, {
12 | test: /\.css$/,
13 | exclude: /highlight.*\.css$/,
14 | loader: 'style!css?importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'
15 | }, {
16 | test: /highlight.*\.css$/,
17 | loader: 'style!css'
18 | }]
19 | },
20 |
21 | plugins: [
22 | new webpack.LoaderOptionsPlugin({
23 | options: {
24 | context: '/',
25 | },
26 | })
27 | ]
28 | };
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 |
5 | script:
6 | - yarn test
7 | - yarn build
8 | - yarn build-storybook
9 |
10 | branches:
11 | except:
12 | - /^v\d+\.\d+\.\d+$/
13 |
14 | after_success:
15 | - yarn semantic-release
16 |
17 | deploy:
18 | skip_cleanup: true
19 | provider: pages
20 | local-dir: storybook-static
21 | github-token:
22 | secure: "UJwWzAq1MabmWKc4cTzQAf1AwRgcYllgnC2I9ex/iYUNAyf9jUmz81FvcLneGBjAO885Namu8O3RE57wHrj1uul2lhFFXM15ALp3I4rrMtEfW7cTMhmsqxpLKO6sqTjw6JI+uUUN/Tc8inHV7YuQxvCidnmzL5D2ENytrqrFpeIwCMta3UKZ2uMK0+KrMgd9JG9NmKvYGRGH1lpmK7Yq7gJtPJB/t/ACgsVcrAbhmMbh3ssaOidjPMc9aslqCepff8fIrMgacdficJ1b2NWOzjs1caQpHRpo4wassB+NJ8DNOkBikGNMUiD87TEdFZifj5MW8pU2j/sJv+ntLRAValDxw8DclYSE/Nt3PXD+AZwLFjkFM/VqMHLErGdEaS1YGVRoFIDm16J+AegoGyE6207hwh3ClyMI/bXy/HaPivppVqmkap2RMZlQoZ9MpOqbLtQtqz+nGRKqB+CM3Ngyj24/XxRZGBACq76wN4ncBnVxq2+RhclyZnd8HT5LGUsZ+q5M2msA7HawBOeYVshD98mDHf4DLnT1m7tarsM8aV2h6MEK4GiBq5y+/GQN84CER3XJMJLaczYDr0j5DJOIcM/MgPPJRbHeDxb4LB3iYmtTvyJo4wOIQAySPy30nMzady3BLkNga7zd5IpjxytyC1OLoXaZIgxaWFjgO6yPCls="
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :warning: This repository is still in development phase and is in active development. Don't expect anything to work yet!
2 |
3 | [](https://travis-ci.org/burst-digital/hn-react-webform)
4 | [](https://www.bithound.io/github/burst-digital/hn-react-webform)
5 |
6 | # HN-React-Webform
7 | With this awesome React component, you can render complete Drupal Webforms in React. With validation, easy custom styling and a modern, clean interface.
8 |
9 | ## Installation
10 |
11 | First, install *hn-react-webform* in your project:
12 | ```bash
13 | $ npm install hn-react-webform
14 | # or
15 | $ yarn add hn-react-webform
16 | ```
17 | Then, import the component:
18 | ```javascript
19 | // ES6
20 | import Webform from 'hn-react-webform';
21 | // ES5
22 | var Webform = require('hn-react-webform').default;
23 | ```
24 | This project uses *CSS Modules*, *CSS.next* and *ES7* to ease styling. Your web-bundler (like Webpack) needs to support *CSS Modules* to correctly parse all styling.
25 | # Contributing
26 |
27 | If you want to help contributing, follow these steps:
28 |
29 | 1. Clone this repo
30 | 2. `cd` into the folder
31 | 3. Run `npm install`
32 | 5. Run `npm run storybook`
33 | 6. Edit files in `/src` and view changes on http://localhost:6006/
34 |
35 |
36 | If you want to contribute and to use this version of the module in a different project on your local machine without storybook, run `npm link`
37 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | - Make sure the rules aren't set on construct() or in render functions, but everything in the static meta object, and make it a computed value on Field
2 | - Transfer the submit function to Form, with only the parameter (isDraft = false) which handles everything. Drafts should only send up to the current page.
3 | - Remove all obsolete parameters, and provide the formStore trough the mobx Provider.
4 | - Implement semantic-release, and run yarn build on the CI.
5 | - Implement mobx strict mode, and do everything with actions.
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hn-react-webform",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "repository": "https://github.com/burst-digital/hn-react-webform",
6 | "scripts": {
7 | "start": "npm run build -- --watch",
8 | "build": "rimraf lib && babel src --out-dir lib --copy-files",
9 | "lint": "npm run lint-js",
10 | "lint-js": "eslint --ext .jsx,.js src",
11 | "lint-js-fix": "eslint --ext .jsx,.js src --fix",
12 | "lint-js-watch": "node node_modules/eslint-watch/bin/esw -w --fix --ext .jsx,.js src",
13 | "lint-fix": "npm run lint-js-fix",
14 | "storybook": "start-storybook -p 6006",
15 | "build-storybook": "build-storybook",
16 | "test": "npm run lint && jest src",
17 | "precommit": "lint-staged && jest src",
18 | "semantic-release": "semantic-release pre && node utils/prepublish.js && npm publish && semantic-release post",
19 | "postinstall": "node utils/postinstall.js"
20 | },
21 | "dependencies": {
22 | "fetch-everywhere": "^1.0.5",
23 | "get-nested": "^4.0.0",
24 | "google_tag": "^1.1.1",
25 | "html-entities": "^1.2.1",
26 | "ibantools": "^1.3.0",
27 | "mobx": "^4.9.4",
28 | "mobx-react": "^5.4.3",
29 | "moment": "^2.18.1",
30 | "prop-types": "^15.5.10",
31 | "react-css-modules": "^4.3.0",
32 | "react-day-picker": "^5.5.1",
33 | "react-google-recaptcha": "^2.0.1",
34 | "react-html-parser": "^1.0.3",
35 | "react-input-mask": "^0.8.0",
36 | "react-select": "^1.3.0",
37 | "styled-components": "^3.3.3",
38 | "validator": "^7.0.0"
39 | },
40 | "lint-staged": {
41 | "*.{js,jsx}": [
42 | "eslint --fix",
43 | "git add"
44 | ]
45 | },
46 | "devDependencies": {
47 | "@kadira/storybook": "^2.35.3",
48 | "babel-cli": "^6.24.1",
49 | "babel-eslint": "^7.2.3",
50 | "babel-plugin-styled-components": "^1.5.1",
51 | "babel-plugin-syntax-object-rest-spread": "^6.13.0",
52 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
53 | "babel-plugin-transform-runtime": "^6.23.0",
54 | "babel-preset-env": "^1.4.0",
55 | "babel-preset-react": "^6.24.1",
56 | "babel-preset-stage-1": "^6.24.1",
57 | "css-loader": "^0.28.1",
58 | "eslint": "^3.19.0",
59 | "eslint-config-airbnb": "^15.0.1",
60 | "eslint-plugin-import": "^2.2.0",
61 | "eslint-plugin-jsx-a11y": "^5.0.3",
62 | "eslint-plugin-react": "^7.0.1",
63 | "eslint-watch": "^3.1.0",
64 | "highlight.js": "^9.11.0",
65 | "husky": "^0.13.3",
66 | "jest": "^20.0.4",
67 | "jest-cli": "^20.0.3",
68 | "json-loader": "^0.5.4",
69 | "lint-staged": "^4.0.3",
70 | "raw-loader": "^0.5.1",
71 | "react": "^16.0.0",
72 | "react-addons-test-utils": "^15.5.1",
73 | "react-dom": "^16.0.0",
74 | "react-remarkable": "^1.1.1",
75 | "rimraf": "^2.6.1",
76 | "semantic-release": "^7.0.2",
77 | "storybook-readme": "^2.0.2",
78 | "style-loader": "^0.17.0",
79 | "stylelint": "^7.10.1",
80 | "stylelint-config-standard": "^16.0.0",
81 | "webpack": "^2.5.1"
82 | },
83 | "peerDependencies": {
84 | "react": "^16.0.0",
85 | "react-dom": "^16.0.0"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Address/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import { get } from 'mobx';
5 | import composeLookUp from '../LookUp';
6 | import Fieldset from '../Fieldset';
7 | import rules from '../Webform/rules';
8 | import RuleHint from '../RuleHint';
9 | import WebformUtils from '../WebformUtils';
10 | import FormStore from '../Observables/Form';
11 |
12 | // styled
13 | import FieldsetFormRow from '../Fieldset/styled/wrapper';
14 | import ValidationMessage from '../LookUp/styled/validation-message';
15 |
16 | @observer
17 | class Address extends Component {
18 | static meta = {
19 | wrapper: FieldsetFormRow,
20 | label: Fieldset.meta.label,
21 | wrapperProps: Fieldset.meta.wrapperProps,
22 | labelVisibility: Fieldset.meta.labelVisibility,
23 | validations: [el => `address_${el.key}`],
24 | hasValue: false,
25 | };
26 |
27 | static propTypes = {
28 | field: PropTypes.shape({
29 | '#webform_key': PropTypes.string.isRequired,
30 | '#addressError': PropTypes.string,
31 | composite_elements: PropTypes.arrayOf(PropTypes.shape()),
32 | parent: PropTypes.shape({
33 | props: PropTypes.shape({
34 | field: PropTypes.shape(),
35 | }),
36 | }),
37 | }).isRequired,
38 | getField: PropTypes.func.isRequired,
39 | onBlur: PropTypes.func.isRequired,
40 | onChange: PropTypes.func.isRequired,
41 | formKeySuffix: PropTypes.string.isRequired,
42 | url: PropTypes.string.isRequired,
43 | formStore: PropTypes.instanceOf(FormStore).isRequired,
44 | fields: PropTypes.arrayOf(PropTypes.shape()).isRequired,
45 | settings: PropTypes.shape().isRequired,
46 | registerLookUp: PropTypes.func.isRequired,
47 | };
48 |
49 | constructor(props) {
50 | super(props);
51 |
52 | const field = props.formStore.getField(props.field['#webform_key']);
53 |
54 | const lookUpIsBlocking = field.element['#address_validation'] || (
55 | field.parent && field.parent.element['#address_validation']);
56 |
57 | this.lookUpFields = {
58 | street: {
59 | elementKey: 'street',
60 | formKey: `address_street${props.formKeySuffix}`,
61 | apiValue: address => address.street,
62 | hideField: true,
63 | disableField: lookUpIsBlocking,
64 | },
65 | postcode: {
66 | elementKey: 'postcode',
67 | formKey: `address_postcode${props.formKeySuffix}`,
68 | apiValue: address => (address.postcode || '').toUpperCase(),
69 | triggerLookUp: true,
70 | },
71 | number: {
72 | elementKey: 'number',
73 | formKey: `address_number${props.formKeySuffix}`,
74 | apiValue: () => false,
75 | triggerLookUp: true,
76 | },
77 | addition: {
78 | elementKey: 'addition',
79 | formKey: `address_number_add${props.formKeySuffix}`,
80 | apiValue: () => false,
81 | triggerLookUp: true,
82 | },
83 | city: {
84 | elementKey: 'city',
85 | formKey: `address_city${props.formKeySuffix}`,
86 | apiValue: address => address.city.label,
87 | hideField: true,
88 | disableField: lookUpIsBlocking,
89 | },
90 | locationLat: {
91 | elementKey: 'locationLat',
92 | formKey: `address_location_lat${props.formKeySuffix}`,
93 | apiValue: address => address.geo.center.wgs84.coordinates[1],
94 | },
95 | locationLng: {
96 | elementKey: 'locationLng',
97 | formKey: `address_location_lng${props.formKeySuffix}`,
98 | apiValue: address => address.geo.center.wgs84.coordinates[0],
99 | },
100 | manualOverride: {
101 | elementKey: 'manualOverride',
102 | formKey: `address_manual_override${props.formKeySuffix}`,
103 | apiValue: () => false,
104 | },
105 | };
106 |
107 |
108 | this.lookUpBase = `${props.url}/postcode-api/address?_format=json`;
109 |
110 | const lookUpKey = this.getLookUpKey(props);
111 |
112 | rules.set(`address_${props.field['#webform_key']}`, {
113 | rule: () => {
114 | const lookUp = get(field.lookUps, lookUpKey);
115 | return !lookUpIsBlocking || !lookUp || !lookUp.lookUpSent || (
116 | lookUp.lookUpSent && lookUp.lookUpSuccessful
117 | );
118 | },
119 | hint: () => null,
120 | shouldValidate: () => props.fields.reduce(
121 | (shouldValidate, item) =>
122 | shouldValidate && !item.isEmpty && item.isBlurred && item.valid,
123 | true,
124 | ),
125 | });
126 |
127 | props.registerLookUp(lookUpKey, this.lookUpFields, true);
128 | }
129 |
130 | getLookUpKey(props) {
131 | return `${(
132 | props || this.props
133 | ).field['#webform_key']}-address`;
134 | }
135 |
136 | prepareLookUp(fields) {
137 | const postCodeField = this.props.getField('postcode').field;
138 | const numberField = this.props.getField('number').field;
139 |
140 | if(!fields.postcode || !postCodeField || !numberField || !postCodeField.valid || !fields.number || !numberField.valid) {
141 | return false;
142 | }
143 |
144 | const query = `&postcode=${fields.postcode.toUpperCase()}${fields.number ? `&number=${fields.number}` : ''}`;
145 |
146 | return {
147 | query,
148 | // eslint-disable-next-line no-underscore-dangle
149 | checkResponse: json => json,
150 | isSuccessful: json => (!!json.id),
151 | };
152 | }
153 |
154 | render() {
155 | const field = this.props.formStore.getField(this.lookUpFields.postcode.formKey);
156 | const lookUpKey = this.getLookUpKey();
157 | const lookUp = get(field.lookUps, lookUpKey);
158 |
159 | return (
160 |
171 | );
172 | }
173 | }
174 |
175 | export default composeLookUp(Address);
176 |
--------------------------------------------------------------------------------
/src/BaseButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from './styled/button';
4 |
5 | const BaseButton = ({ disabled, label, formSubmitAttributes, onClick, primary, type }) => (
6 |
16 | );
17 |
18 | BaseButton.propTypes = {
19 | disabled: PropTypes.bool,
20 | formSubmitAttributes: PropTypes.arrayOf(PropTypes.string),
21 | label: PropTypes.string,
22 | primary: PropTypes.bool,
23 | onClick: PropTypes.func,
24 | type: PropTypes.string,
25 | };
26 |
27 | BaseButton.defaultProps = {
28 | disabled: null,
29 | formSubmitAttributes: [],
30 | label: '',
31 | primary: true,
32 | onClick: () => {},
33 | type: 'button',
34 | };
35 |
36 | export default BaseButton;
37 |
--------------------------------------------------------------------------------
/src/BaseButton/styled/button.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.button`
4 | cursor: pointer;
5 | -webkit-appearance: none;
6 | margin: calc(${p => p.theme.spacingUnit} * 1.5) 0 0;
7 | font-size: 1em;
8 | line-height: inherit;
9 | font-family: inherit;
10 | border: none;
11 | color: ${p => p.theme.buttonTextColor};
12 | background-color: ${p => p.theme.buttonColor};
13 | border-radius: ${p => p.theme.borderRadius};
14 | padding: ${p => p.theme.buttonSpacingV} ${p => p.theme.buttonSpacingH};
15 | transition: background-color 350ms ease;
16 |
17 | &:hover {
18 | background-color: ${p => p.theme.buttonColorHover};
19 | }
20 |
21 | ${p => p.disabled && `
22 | background-color: ${p.theme.buttonColorDisabled};
23 | cursor: default;
24 |
25 | &:hover {
26 | background-color: ${p.theme.buttonColorDisabled};
27 | }
28 | `}
29 |
30 | ${p => !p.primary && `
31 | background-color: ${p.theme.buttonSecondaryColor};
32 | color: ${p.theme.buttonSecondaryTextColor};
33 |
34 | &:hover {
35 | background-color: ${p.theme.buttonSecondaryColorHover};
36 | }
37 | `}
38 | `;
39 |
--------------------------------------------------------------------------------
/src/BaseInput/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import InputMask from 'react-input-mask';
4 | import { observer } from 'mobx-react';
5 | import WebformElement from '../WebformElement';
6 | // styled
7 | import Input from './styled/input';
8 |
9 | const StyledInputMask = Input.withComponent(({ error, ...p }) => );
10 |
11 | @observer
12 | class BaseInput extends Component {
13 | static propTypes = {
14 | field: PropTypes.shape({
15 | '#type': PropTypes.string.isRequired,
16 | '#placeholder': PropTypes.string,
17 | '#field_prefix': PropTypes.string,
18 | '#field_suffix': PropTypes.string,
19 | '#webform_key': PropTypes.string.isRequired,
20 | '#required': PropTypes.bool,
21 | '#mask': PropTypes.oneOfType([
22 | PropTypes.string,
23 | PropTypes.bool,
24 | ]),
25 | '#alwaysShowMask': PropTypes.oneOfType([
26 | PropTypes.string,
27 | PropTypes.bool,
28 | ]),
29 | '#min': PropTypes.string,
30 | '#max': PropTypes.string,
31 | '#step': PropTypes.string,
32 | '#attributes': PropTypes.oneOfType([
33 | PropTypes.shape({
34 | autoComplete: PropTypes.string,
35 | }),
36 | PropTypes.array,
37 | ]),
38 | }).isRequired,
39 | className: PropTypes.string,
40 | value: PropTypes.oneOfType([
41 | PropTypes.string,
42 | PropTypes.number,
43 | PropTypes.bool,
44 | ]).isRequired,
45 | type: PropTypes.string,
46 | id: PropTypes.number,
47 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
48 | onChange: PropTypes.func.isRequired,
49 | onBlur: PropTypes.func.isRequired,
50 | onFocus: PropTypes.func,
51 | onClick: PropTypes.func,
52 | onKeyDown: PropTypes.func,
53 | parentRef: PropTypes.func,
54 | state: PropTypes.shape({
55 | required: PropTypes.bool.isRequired,
56 | enabled: PropTypes.bool.isRequired,
57 | }).isRequired,
58 | };
59 |
60 | static defaultProps = {
61 | id: 0,
62 | className: undefined,
63 | type: 'text',
64 | autoComplete: '',
65 | onFocus: () => {},
66 | onClick: () => {},
67 | onKeyDown: () => {},
68 | parentRef: () => {},
69 | };
70 |
71 | render() {
72 | const attrs = {
73 | 'aria-invalid': this.props.webformElement.isValid() ? null : true,
74 | 'aria-required': this.props.state.required ? true : null,
75 | ...(this.props.field['#attributes'] || {}),
76 | };
77 |
78 | let InputComponent = Input; // Input HTML element is 'input' by default
79 |
80 | // When there is a mask from Drupal.
81 | if(this.props.field['#mask']) {
82 | InputComponent = StyledInputMask; // Use InputMask element instead.
83 | attrs.mask = this.props.field['#mask'];
84 | attrs.alwaysShowMask = this.props.field['#alwaysShowMask'] || true;
85 | }
86 |
87 | return (
88 | );
109 | }
110 | }
111 |
112 | export default BaseInput;
113 |
--------------------------------------------------------------------------------
/src/BaseInput/styled/input.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import ValidationIcon from './validation-icon';
3 |
4 | export default styled.input`
5 | box-sizing: border-box;
6 | margin: 0 0 calc(${p => p.theme.spacingUnit} / 2);
7 | font-size: 0.9em;
8 | line-height: ${p => p.theme.inputLineHeight};
9 | font-family: inherit;
10 | width: 100%;
11 | border: 1px solid ${p => p.theme.borderColor};
12 | background-color: ${p => p.theme.inputBgColor};
13 | border-radius: ${p => p.theme.borderRadius};
14 | padding: calc(${p => p.theme.spacingUnit} / 4) calc(${p => p.theme.spacingUnit} / 2);
15 |
16 | &:focus {
17 | outline: none;
18 | box-shadow: 0 0 2px 3px ${p => p.theme.focusColor};
19 | }
20 |
21 | &:disabled {
22 | background-color: ${p => p.theme.inputDisabledBgColor};
23 | }
24 |
25 | &::placeholder {
26 | color: ${p => p.theme.placeholderColor};
27 | }
28 |
29 | ${p => p.error && `
30 | border-color: ${p.theme.errorColor};
31 | background-color: ${p.theme.errorBgColor};
32 |
33 | &::placeholder {
34 | color: ${p.theme.errorColor};
35 | }
36 | `}
37 |
38 | &[type="range"] + ${ValidationIcon} {
39 | position: absolute;
40 | right: -22px;
41 | top: 5px;
42 | }
43 | `;
44 |
--------------------------------------------------------------------------------
/src/BaseInput/styled/validation-icon.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | display: inline-block;
5 | width: calc(${p => p.theme.spacingUnit} / 1.5);
6 | height: calc(${p => p.theme.spacingUnit} / 1.5);
7 | margin-left: calc(${p => p.theme.spacingUnit} / 1.5);
8 |
9 | &::after {
10 | content: '';
11 | display: block;
12 | width: 60%;
13 | height: 100%;
14 | border-bottom: 2px solid ${p => p.theme.successColor};
15 | border-right: 2px solid ${p => p.theme.successColor};
16 | transform: rotate(45deg) translateY(-4px);
17 | transform: rotate(45deg) translateY(-4px);
18 | opacity: 0;
19 | }
20 |
21 | ${p => p.success && `
22 | &::after {
23 | opacity: 1;
24 | }
25 | `}
26 | `;
27 |
--------------------------------------------------------------------------------
/src/CheckboxField/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import Parser from '../Parser';
5 | import Fieldset from '../Fieldset';
6 | // styled
7 | import Wrapper from './styled/wrapper';
8 | import Label from './styled/label';
9 | import Checkbox from './styled/checkbox';
10 | import Indicator from './styled/indicator';
11 | import InnerLabel from './styled/inner-label';
12 | import StyledFieldsetFormRow from './styled/fieldset';
13 |
14 | @observer
15 | class CheckboxField extends Component {
16 | static meta = {
17 | wrapper: StyledFieldsetFormRow,
18 | label: Fieldset.meta.label,
19 | wrapperProps: Fieldset.meta.wrapperProps,
20 | field_display: {
21 | '#description': 'NO_DESCRIPTION',
22 | },
23 | };
24 |
25 | static propTypes = {
26 | field: PropTypes.shape({
27 | '#webform_key': PropTypes.string.isRequired,
28 | '#title_display': PropTypes.string,
29 | '#description': PropTypes.string,
30 | '#required': PropTypes.bool,
31 | }).isRequired,
32 | value: PropTypes.oneOfType([
33 | PropTypes.string,
34 | PropTypes.number,
35 | PropTypes.bool,
36 | ]).isRequired,
37 | id: PropTypes.number,
38 | onChange: PropTypes.func.isRequired,
39 | onBlur: PropTypes.func.isRequired,
40 | state: PropTypes.shape({
41 | required: PropTypes.bool.isRequired,
42 | enabled: PropTypes.bool.isRequired,
43 | }).isRequired,
44 | };
45 |
46 | static defaultProps = {
47 | id: 0,
48 | };
49 |
50 | constructor(props) {
51 | super(props);
52 |
53 | this.onChange = this.onChange.bind(this);
54 | }
55 |
56 | onChange(e) {
57 | this.props.onChange(e.target.checked);
58 | this.props.onBlur(e);
59 | }
60 |
61 | getValue() {
62 | return this.props.value === '1' || this.props.value === true ? 'checked' : false;
63 | }
64 |
65 | render() {
66 | const value = this.getValue();
67 | return (
68 |
69 |
85 |
86 | );
87 | }
88 | }
89 |
90 | export default CheckboxField;
91 |
--------------------------------------------------------------------------------
/src/CheckboxField/styled/checkbox.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Indicator from './indicator';
3 |
4 | export default styled.input`
5 | /* Reset anything that could peek out or interfere with dimensions, but don't use display:none for WCAG */
6 | position: absolute;
7 | overflow: hidden;
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | outline: 0;
12 | opacity: 0;
13 |
14 | &:focus {
15 | & + ${Indicator} {
16 | box-shadow: 0 0 2px 3px ${p => p.theme.focusColor};
17 | }
18 | }
19 |
20 | &:checked {
21 | & + ${Indicator} {
22 | background: ${p => p.theme.iconCheckbox} no-repeat center center ${p => p.theme.inputBgColor};
23 | background-size: calc(${p => p.theme.spacingUnit} * 0.8) auto;
24 | }
25 | }
26 | `;
27 |
--------------------------------------------------------------------------------
/src/CheckboxField/styled/fieldset.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import FieldsetFormRow from '../../Fieldset/styled/wrapper';
3 |
4 | export default styled(FieldsetFormRow)`
5 | margin-bottom: 0;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/CheckboxField/styled/indicator.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | content: '';
5 | float: left;
6 | display: inline-block;
7 | width: ${p => p.theme.spacingUnitCheckbox};
8 | height: ${p => p.theme.spacingUnitCheckbox};
9 | margin-right: calc(${p => p.theme.spacingUnitCheckbox} * 1.5 * -1);
10 | background-color: ${p => p.theme.inputBgColor};
11 | border: 1px solid ${p => p.theme.borderColor};
12 | border-radius: ${p => p.theme.borderRadius};
13 | `;
14 |
--------------------------------------------------------------------------------
/src/CheckboxField/styled/inner-label.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | display: inline-block;
5 | padding-left: calc(${p => p.theme.spacingUnitCheckbox} * 1.5);
6 | line-height: 1.5;
7 | top: -0.2em;
8 | position: relative;
9 | `;
10 |
--------------------------------------------------------------------------------
/src/CheckboxField/styled/label.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.label`
4 | width: auto;
5 | margin-bottom: 0;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/CheckboxField/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | display: inline-block;
5 | padding-bottom: calc(${p => p.theme.spacingUnit} / 4);
6 |
7 | ${p => p.labelDisplay === 'inline' && `
8 | @media (min-width: 768px) {
9 | float: left;
10 | width: calc(50% - (${p.theme.spacingUnit} / 2) - 0.5em);
11 | }
12 | `}
13 | `;
14 |
--------------------------------------------------------------------------------
/src/Converse/formula.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Pass a formula and an object with values to evaluate the strings after replacing the variables.
3 | * Examples formulas are:
4 | * * 10 + 10 => 20
5 | * * amount * 10 => will check the 'values' parameter object for an 'amount' key and use the value to replace it and evaluate the string.
6 | * * (amount|0| * 12) + 1.95 => will check the 'values' parameter object for an 'amount' key and use the value to replace it and evaluate the string. If the 'amount' key is not present it will use '0' instead.
7 | * @param formula The formula needs to be a string and can contain the basic mathematical operators, i.e. + - * / () ^
8 | * @param values Object with the keys corresponding to the variables in the formula and with values to be used instead of the variables.
9 | * @returns number
10 | */
11 | // eslint-disable-next-line import/prefer-default-export
12 | export const parse = (formula, values) => {
13 | const formattedFormula = formula.replace(/\|([0-9]+)\|/g, ''); // Remove all fallback values from formula (|1|)
14 |
15 | const formulaParts = formula.split('|'); // Split formula into parts, an array of fields & fallback values ('field|1| + field2|0|' => ['field', 1, 'field2', 0])
16 |
17 | const replacedFormula = formattedFormula.match(/([a-zA-Z_])+/g).reduce((result, variable) => {
18 | let value = values[variable];
19 | if(typeof value === 'undefined') {
20 | const fallbackIndex = formulaParts.findIndex(item => item.endsWith(variable)); // Get index of field in formula parts
21 | const fallbackValue = parseFloat(formulaParts[fallbackIndex + 1]); // Next to field is the fallback value, hence the + 1
22 | if(fallbackIndex !== false && !isNaN(fallbackValue)) { // If the fallback value exists, and is a number
23 | value = fallbackValue;
24 | }
25 | }
26 |
27 | return result.replace(variable, value);
28 | }, formattedFormula);
29 |
30 | // Replace comma's with periods.
31 | const removeCommasFormula = replacedFormula.replace(/,/g, '.');
32 |
33 | if(removeCommasFormula.match(/[^-()\d\s/*+.]/g)) {
34 | console.error(`Invalid characters found in formula ${removeCommasFormula.toString()}`);
35 | return 0;
36 | }
37 |
38 | // eslint-disable-next-line no-eval
39 | return eval(removeCommasFormula);
40 | };
41 |
--------------------------------------------------------------------------------
/src/Converse/index.jsx:
--------------------------------------------------------------------------------
1 | import { observer, inject } from 'mobx-react';
2 | import PropTypes from 'prop-types';
3 | import React, { Component } from 'react';
4 | import Fieldset from '../Fieldset';
5 | import { parse } from './formula';
6 |
7 | // styled
8 | import FieldsetFormRow from '../Fieldset/styled/wrapper';
9 |
10 | @inject('formStore')
11 | @observer
12 | class Converse extends Component {
13 | static meta = {
14 | wrapper: FieldsetFormRow,
15 | label: Fieldset.meta.label,
16 | wrapperProps: Fieldset.meta.wrapperProps,
17 | labelVisibility: Fieldset.meta.labelVisibility,
18 | hasValue: false,
19 | };
20 |
21 | static propTypes = {
22 | onBlur: PropTypes.func.isRequired,
23 | onChange: PropTypes.func.isRequired,
24 | };
25 |
26 | static getTokens(formStore, field) {
27 | if(field.visible) {
28 | const amount = Converse.calculateFormula(formStore.values, field.element['#payment_amount']) || 0;
29 | const formattedAmount = parseFloat(amount).toFixed(2);
30 | return {
31 | payment_amount: formattedAmount,
32 | payment_amount_number: formattedAmount.slice(0, -3),
33 | payment_amount_decimals: formattedAmount.slice(-2),
34 | };
35 | }
36 | return {};
37 | }
38 |
39 | /**
40 | * financial_amount_monthly_suggestion|0| + financial_amount_monthly|0| + financial_amount_yearly_suggestion|0| + financial_amount_yearly|0|
41 | * @param values
42 | * @param formula
43 | * @returns {*}
44 | */
45 | static calculateFormula(values, formula) {
46 | const amount = parse(formula, values);
47 | if(isNaN(amount)) {
48 | return null;
49 | }
50 | return amount < 0 ? 0 : amount;
51 | }
52 |
53 | render() {
54 | return (
55 |
61 | );
62 | }
63 | }
64 |
65 | export default Converse;
66 |
--------------------------------------------------------------------------------
/src/Date/index.jsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import moment from 'moment';
3 | import PropTypes from 'prop-types';
4 | import React, { Component } from 'react';
5 | import MomentLocaleUtils from 'react-day-picker/moment';
6 | import BaseInput from '../BaseInput';
7 | import Fieldset from '../Fieldset';
8 | import Input from '../Input';
9 | import RuleHint from '../RuleHint';
10 | import rules from '../Webform/rules';
11 | import WebformElement from '../WebformElement';
12 | import WebformUtils from '../WebformUtils';
13 | import labelTranslations from './labelTranslations';
14 | // styled
15 | import Wrapper from './styled/wrapper';
16 | import OverlayWrapper from './styled/overlay-wrapper';
17 | import Overlay from './styled/overlay';
18 | import DayPicker from './styled/day-picker';
19 | import ValidationIcon from './styled/validation-icon';
20 |
21 | @observer
22 | class Date extends Component {
23 | static meta = {
24 | validations: [
25 | el => `date_${el.key}`,
26 | el => `date_range_${el.key}`,
27 | ],
28 | };
29 |
30 | static propTypes = {
31 | field: PropTypes.shape({
32 | '#webform_key': PropTypes.string,
33 | '#default_value': PropTypes.string,
34 | '#date_date_min': PropTypes.string,
35 | '#date_date_max': PropTypes.string,
36 | '#min': PropTypes.string,
37 | '#max': PropTypes.string,
38 | '#mask': PropTypes.oneOfType([
39 | PropTypes.string,
40 | PropTypes.bool,
41 | ]),
42 | '#dateError': PropTypes.string,
43 | '#dateRangeError': PropTypes.string,
44 | '#dateBeforeError': PropTypes.string,
45 | '#dateAfterError': PropTypes.string,
46 | '#title_display': PropTypes.string,
47 | '#datepicker': PropTypes.bool,
48 | composite_elements: PropTypes.oneOfType([
49 | PropTypes.array,
50 | PropTypes.object,
51 | ]),
52 | }).isRequired,
53 | value: PropTypes.oneOfType([
54 | PropTypes.string,
55 | PropTypes.bool,
56 | ]).isRequired,
57 | type: PropTypes.string,
58 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
59 | onChange: PropTypes.func.isRequired,
60 | onFocus: PropTypes.func,
61 | onBlur: PropTypes.func.isRequired,
62 | settings: PropTypes.shape().isRequired,
63 | locale: PropTypes.string,
64 | };
65 |
66 | static defaultProps = {
67 | locale: 'nl',
68 | type: 'text',
69 | onFocus: () => {},
70 | };
71 |
72 | static rewriteValue(value) {
73 | const date = moment(value, Date.dateFormat, true);
74 | return date.format('YYYY-MM-DD');
75 | }
76 |
77 | static dateFormat = 'DD/MM/YYYY';
78 |
79 | constructor(props) {
80 | super(props);
81 |
82 | this.min = props.field['#min'] || props.field['#date_date_min'];
83 | this.max = props.field['#max'] || props.field['#date_date_max'];
84 |
85 | rules.set(`date_${props.field['#webform_key']}`, {
86 | rule: (value) => {
87 | const timestamp = moment(value, Date.dateFormat, true);
88 | return WebformUtils.isEmpty(props.field, value) || timestamp.isValid();
89 | },
90 | hint: () =>
91 | (),
95 | shouldValidate: field => field.isBlurred && !WebformUtils.isEmpty(field, field.getValue()),
96 | });
97 |
98 | rules.set(`date_range_${props.field['#webform_key']}`, {
99 | rule: value => WebformUtils.isEmpty(props.field, value) || this.calculateDateRange(value).valid,
100 | hint: (value) => {
101 | const result = this.calculateDateRange(value);
102 | let hint;
103 |
104 | switch(result.type) {
105 | case 'before':
106 | hint = WebformUtils.getCustomValue(props.field, 'dateBeforeError', props.settings) || WebformUtils.getErrorMessage(props.field, '#required_error') || 'Please enter a date before {{max}}';
107 | break;
108 | case 'after':
109 | hint = WebformUtils.getCustomValue(props.field, 'dateAfterError', props.settings) || WebformUtils.getErrorMessage(props.field, '#required_error') || 'Please enter a date after {{min}}';
110 | break;
111 | default:
112 | case 'range':
113 | hint = WebformUtils.getCustomValue(props.field, 'dateRangeError', props.settings) || WebformUtils.getErrorMessage(props.field, '#required_error') || 'Please enter a date between {{min}} and {{max}}';
114 | break;
115 | }
116 |
117 | return ();
126 | },
127 | shouldValidate: field => field.isBlurred && !WebformUtils.isEmpty(field, field.getValue()),
128 | });
129 |
130 | this.clickedInside = false;
131 |
132 | this.state = {
133 | showOverlay: false,
134 | selectedDay: moment(props.value, Date.dateFormat, true).isValid() ? moment(props.value, Date.dateFormat).toDate() : null,
135 | };
136 |
137 | this.calculateDisabledDates = this.calculateDisabledDates.bind(this);
138 | this.handleInputFocus = this.handleInputFocus.bind(this);
139 | this.handleInputBlur = this.handleInputBlur.bind(this);
140 | this.handleInputChange = this.handleInputChange.bind(this);
141 | this.handleContainerMouseDown = this.handleContainerMouseDown.bind(this);
142 | this.handleDayClick = this.handleDayClick.bind(this);
143 | this.setRef = this.setRef.bind(this);
144 | }
145 |
146 | componentWillUnmount() {
147 | clearTimeout(this.clickTimeout);
148 | }
149 |
150 | setRef(ref, el) {
151 | this[ref] = el;
152 | }
153 |
154 | getDateRange(result = {}, dateFormat = Date.dateFormat) {
155 | const minMoment = moment(this.min, dateFormat, true);
156 | const maxMoment = moment(this.max, dateFormat, true);
157 |
158 | result.min = minMoment;
159 | if(!minMoment.isValid()) {
160 | const [amount, unit] = this.min.split(' ');
161 | result.min = moment().add(amount, unit);
162 | }
163 |
164 | result.max = maxMoment;
165 | if(!maxMoment.isValid()) {
166 | const [amount, unit] = this.max.split(' ');
167 | result.max = moment().add(amount, unit);
168 | }
169 |
170 | return result;
171 | }
172 |
173 | getLabelDisplay() {
174 | return this.props.webformElement.getLabelDisplay();
175 | }
176 |
177 | calculateDateRange(value) {
178 | const result = this.getDateRange({
179 | valid: true,
180 | });
181 |
182 | const timestamp = moment(value, Date.dateFormat, true);
183 |
184 | if(this.min && this.max) {
185 | result.type = 'range';
186 | result.valid = timestamp.isBetween(result.min, result.max);
187 | } else if(this.min) {
188 | result.type = 'after';
189 | result.valid = timestamp.isAfter(result.min);
190 | } else if(this.max) {
191 | result.type = 'before';
192 | result.valid = timestamp.isBefore(result.max);
193 | }
194 |
195 | return result;
196 | }
197 |
198 | calculateDisabledDates(day) {
199 | const result = this.calculateDateRange(moment(day).format(Date.dateFormat));
200 | return !result.valid;
201 | }
202 |
203 | handleInputBlur() {
204 | const showOverlay = this.clickedInside;
205 |
206 | this.setState({ showOverlay });
207 |
208 | // Force input's focus if blur event was caused by clicking on the calendar.
209 | if(showOverlay) {
210 | this.props.onFocus();
211 | }
212 | }
213 |
214 | handleInputFocus() {
215 | this.setState({ showOverlay: true });
216 | }
217 |
218 | handleInputChange(e) {
219 | const value = e.target.value;
220 | const momentDay = moment(value, Date.dateFormat, true);
221 | const newValue = momentDay.isValid() ? momentDay.format(Date.dateFormat) : value;
222 | const selectedDay = momentDay.isValid() ? momentDay.toDate() : null;
223 | this.props.onChange(newValue);
224 | this.setState({ selectedDay },
225 | () => selectedDay && this.daypicker.showMonth(selectedDay),
226 | );
227 | }
228 |
229 | handleContainerMouseDown() {
230 | this.clickedInside = true;
231 | // The input's onBlur method is called from a queue right after onMouseDown event.
232 | // setTimeout adds another callback in the queue, but is called later than onBlur event.
233 | this.clickTimeout = setTimeout(() => this.clickedInside = false, 0);
234 | }
235 |
236 | handleDayClick(selectedDay) {
237 | this.props.onChange(moment(selectedDay).format(Date.dateFormat));
238 | this.setState({
239 | selectedDay,
240 | showOverlay: false,
241 | });
242 | this.props.onBlur();
243 | }
244 |
245 | render() {
246 | const { value, field } = this.props;
247 |
248 | field['#mask'] = Fieldset.getValue(field, 'mask');
249 | if(field['#mask'] === false) {
250 | field['#mask'] = '99/99/9999';
251 | }
252 |
253 | if(!field['#datepicker']) {
254 | return (
255 |
260 | );
261 | }
262 |
263 | const DateInput = (
264 |
273 | );
274 |
275 | return (
276 |
282 | {DateInput}
283 | {this.state.showOverlay && (
284 |
285 |
286 | this.setRef('daypicker', el)}
288 | initialMonth={this.state.selectedDay || undefined}
289 | onDayClick={this.handleDayClick}
290 | selectedDays={this.state.selectedDay}
291 | locale={this.props.locale}
292 | localeUtils={MomentLocaleUtils}
293 | labels={labelTranslations[this.props.locale]}
294 | enableOutsideDays
295 | disabledDays={[this.calculateDisabledDates]}
296 | />
297 |
298 |
299 | )}
300 |
301 |
302 | );
303 | }
304 | }
305 |
306 | export default Date;
307 |
--------------------------------------------------------------------------------
/src/Date/labelTranslations.js:
--------------------------------------------------------------------------------
1 | export const nlLabels = {
2 | nextMonth: 'Volgende maand',
3 | previousMonth: 'Vorige maand',
4 | };
5 |
6 | export default {
7 | nl: nlLabels,
8 | };
9 |
--------------------------------------------------------------------------------
/src/Date/styled/day-picker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions */
2 | import styled from 'styled-components';
3 | import DayPicker from 'react-day-picker';
4 |
5 | export default styled(DayPicker)`
6 | /* DayPicker styles */
7 |
8 | .DayPicker {
9 | display: -webkit-box;
10 | display: -ms-flexbox;
11 | display: flex;
12 | -ms-flex-wrap: wrap;
13 | flex-wrap: wrap;
14 | -webkit-box-pack: center;
15 | -ms-flex-pack: center;
16 | justify-content: center;
17 | position: relative;
18 | padding: 1rem 0;
19 | -webkit-user-select: none;
20 | -moz-user-select: none;
21 | -ms-user-select: none;
22 | user-select: none;
23 | -webkit-box-orient: horizontal;
24 | -webkit-box-direction: normal;
25 | -ms-flex-direction: row;
26 | flex-direction: row;
27 | }
28 |
29 | .DayPicker-Month {
30 | display: table;
31 | border-collapse: collapse;
32 | border-spacing: 0;
33 | -webkit-user-select: none;
34 | -moz-user-select: none;
35 | -ms-user-select: none;
36 | user-select: none;
37 | margin: 0 1rem;
38 | }
39 |
40 | .DayPicker-NavBar {
41 | position: absolute;
42 | left: 0;
43 | right: 0;
44 | padding: 0 .5rem;
45 | top: 1rem;
46 | }
47 |
48 | .DayPicker-NavButton {
49 | position: absolute;
50 | width: 1.5rem;
51 | height: 1.5rem;
52 | background-repeat: no-repeat;
53 | background-position: center;
54 | background-size: contain;
55 | cursor: pointer;
56 | }
57 |
58 | .DayPicker-NavButton--prev {
59 | left: 1rem;
60 | background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2cHgiIGhlaWdodD0iNTBweCIgdmlld0JveD0iMCAwIDI2IDUwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5wcmV2PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9InByZXYiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEzLjM5MzE5MywgMjUuMDAwMDAwKSBzY2FsZSgtMSwgMSkgdHJhbnNsYXRlKC0xMy4zOTMxOTMsIC0yNS4wMDAwMDApIHRyYW5zbGF0ZSgwLjg5MzE5MywgMC4wMDAwMDApIiBmaWxsPSIjNTY1QTVDIj4KICAgICAgICAgICAgPHBhdGggZD0iTTAsNDkuMTIzNzMzMSBMMCw0NS4zNjc0MzQ1IEwyMC4xMzE4NDU5LDI0LjcyMzA2MTIgTDAsNC4yMzEzODMxNCBMMCwwLjQ3NTA4NDQ1OSBMMjUsMjQuNzIzMDYxMiBMMCw0OS4xMjM3MzMxIEwwLDQ5LjEyMzczMzEgWiIgaWQ9InJpZ2h0IiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K");
61 | }
62 |
63 | .DayPicker-NavButton--next {
64 | right: 1rem;
65 | background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2cHgiIGhlaWdodD0iNTBweCIgdmlld0JveD0iMCAwIDI2IDUwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5uZXh0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Im5leHQiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuOTUxNDUxLCAwLjAwMDAwMCkiIGZpbGw9IiM1NjVBNUMiPgogICAgICAgICAgICA8cGF0aCBkPSJNMCw0OS4xMjM3MzMxIEwwLDQ1LjM2NzQzNDUgTDIwLjEzMTg0NTksMjQuNzIzMDYxMiBMMCw0LjIzMTM4MzE0IEwwLDAuNDc1MDg0NDU5IEwyNSwyNC43MjMwNjEyIEwwLDQ5LjEyMzczMzEgTDAsNDkuMTIzNzMzMSBaIiBpZD0icmlnaHQiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=");
66 | }
67 |
68 |
69 | .DayPicker-Caption {
70 | display: table-caption;
71 | height: 1.5rem;
72 | text-align: center;
73 | }
74 |
75 | .DayPicker-Weekdays {
76 | display: table-header-group;
77 | }
78 |
79 | .DayPicker-WeekdaysRow {
80 | display: table-row;
81 | }
82 |
83 | .DayPicker-Weekday {
84 | display: table-cell;
85 | padding: .5rem;
86 | font-size: .875em;
87 | text-align: center;
88 | color: #8b9898;
89 | }
90 |
91 | .DayPicker-Body {
92 | display: table-row-group;
93 | }
94 |
95 | .DayPicker-Week {
96 | display: table-row;
97 | }
98 |
99 | .DayPicker-Day {
100 | display: table-cell;
101 | padding: .5rem;
102 | border: 1px solid #eaecec;
103 | text-align: center;
104 | cursor: pointer;
105 | vertical-align: middle;
106 | }
107 |
108 | .DayPicker-WeekNumber {
109 | display: table-cell;
110 | padding: .5rem;
111 | text-align: right;
112 | vertical-align: middle;
113 | min-width: 1rem;
114 | font-size: 0.75em;
115 | cursor: pointer;
116 | color: #8b9898;
117 | }
118 |
119 | .DayPicker--interactionDisabled .DayPicker-Day {
120 | cursor: default;
121 | }
122 |
123 | .DayPicker-Footer {
124 | display: table-caption;
125 | caption-side: bottom;
126 | padding-top: .5rem;
127 | }
128 |
129 | .DayPicker-TodayButton {
130 | border:none;
131 | background-image:none;
132 | background-color:transparent;
133 | -webkit-box-shadow: none;
134 | box-shadow: none;
135 | cursor: pointer;
136 | color: #4A90E2;
137 | font-size: 0.875em;
138 | }
139 |
140 | /* Default modifiers */
141 |
142 | .DayPicker-Day--today {
143 | color: #d0021b;
144 | font-weight: 500;
145 | }
146 |
147 | .DayPicker-Day--disabled {
148 | color: #dce0e0;
149 | cursor: default;
150 | background-color: #eff1f1;
151 | }
152 |
153 | .DayPicker-Day--outside {
154 | cursor: default;
155 | color: #dce0e0;
156 | }
157 |
158 | /* Example modifiers */
159 |
160 | .DayPicker-Day--sunday {
161 | background-color: #f7f8f8;
162 | }
163 |
164 | .DayPicker-Day--sunday:not(.DayPicker-Day--today) {
165 | color: #dce0e0;
166 | }
167 |
168 | .DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) {
169 | color: #FFF;
170 | background-color: #4A90E2;
171 | }
172 |
173 | /* DayPickerInput */
174 |
175 | .DayPickerInput {
176 | display: inline-block;
177 | }
178 |
179 | .DayPickerInput-OverlayWrapper {
180 | position: relative;
181 | }
182 |
183 | .DayPickerInput-Overlay {
184 | left: 0;
185 | position: absolute;
186 | background: white;
187 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .15);
188 | box-shadow: 0 2px 5px rgba(0, 0, 0, .15);
189 | }
190 | /* Navigation */
191 | .DayPicker-Month {
192 | font-weight: bold;
193 | }
194 |
195 | .DayPicker-Weekday {
196 | color: #757575;
197 | font-size: 0.8em;
198 | }
199 |
200 | /* Days */
201 | .DayPicker-Day {
202 | font-weight: normal;
203 | transition: background-color 350ms ease;
204 | }
205 |
206 | /* Enabled days */
207 | .DayPicker-Day:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) {
208 | color: ${p => p.theme.baseColor};
209 |
210 | &:hover {
211 | background-color: #e4e7e7;
212 | }
213 | }
214 |
215 | /* Disabled days */
216 |
217 | /* .DayPicker-Day.DayPicker-Day--outside {
218 | color: purple;
219 | } */
220 |
221 | .DayPicker-Day--disabled {
222 | &:hover {
223 | background-color: #fff;
224 | }
225 | }
226 |
227 | /* Today */
228 | .DayPicker-Day--today {
229 | font-weight: bold;
230 | color: ${p => p.theme.baseColor};
231 | }
232 |
233 | /* Selected day */
234 | .DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) {
235 | color: ${p => p.theme.buttonTextColor};
236 | background-color: ${p => p.theme.buttonColor};
237 | }
238 | `;
239 |
--------------------------------------------------------------------------------
/src/Date/styled/overlay-wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | position: relative;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/Date/styled/overlay.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | position: absolute;
5 | left: 0;
6 | top: 5px;
7 | background: #fff;
8 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
9 | z-index: 1;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/Date/styled/validation-icon.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import ValidationIcon from '../../BaseInput/styled/validation-icon';
3 |
4 | export default styled(ValidationIcon)`
5 | position: absolute;
6 | top: 10px;
7 | right: 0;
8 | `;
9 |
--------------------------------------------------------------------------------
/src/Date/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | position: relative;
5 |
6 | ${p => p.labelDisplay === 'inline' && `
7 | width: calc(${p.theme.inputWidth} + 20px);
8 | padding-right: 20px;
9 |
10 | @media (min-width: 768px) {
11 | display: inline-block;
12 | float: left;
13 | }
14 | `}
15 |
16 | ${p => p.labelDisplay === 'before' && `
17 | width: calc(${p.theme.inputWidth} + 20px);
18 | padding-right: 20px;
19 |
20 | @media (min-width: 768px) {
21 | width: calc(${p.theme.inlineLabelWidth} + ${p.theme.inputWidth} + 20px);
22 | }
23 | `}
24 | `;
25 |
--------------------------------------------------------------------------------
/src/EmailField/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import validator from 'validator';
4 | import { get } from 'mobx';
5 | import { observer } from 'mobx-react';
6 | import rules from '../Webform/rules';
7 | import RuleHint from '../RuleHint';
8 | import composeLookUp from '../LookUp';
9 | import WebformUtils from '../WebformUtils';
10 | import FormStore from '../Observables/Form';
11 | // styled
12 | import Email from './styled/email';
13 |
14 | @observer
15 | class EmailField extends Component {
16 | static meta = {
17 | validations: [
18 | el => `email_${el.key}`,
19 | el => `email_neverbounce_${el.key}`,
20 | ],
21 | };
22 |
23 | static propTypes = {
24 | field: PropTypes.shape({
25 | '#webform_key': PropTypes.string.isRequired,
26 | '#emailError': PropTypes.string,
27 | '#neverBounceEmail': PropTypes.string,
28 | }).isRequired,
29 | getField: PropTypes.func.isRequired,
30 | onChange: PropTypes.func.isRequired,
31 | onBlur: PropTypes.func.isRequired,
32 | settings: PropTypes.shape().isRequired,
33 | url: PropTypes.string.isRequired,
34 | formStore: PropTypes.instanceOf(FormStore).isRequired,
35 | registerLookUp: PropTypes.func.isRequired,
36 | };
37 |
38 | constructor(props) {
39 | super(props);
40 |
41 | this.lookUpFields = {
42 | email: {
43 | elementKey: 'email',
44 | formKey: props.field['#webform_key'],
45 | triggerLookUp: true,
46 | apiValue: () => false,
47 | required: true,
48 | },
49 | };
50 | this.lookUpBase = `${props.url}/neverbounce/validate-single?_format=json`;
51 |
52 | const lookUpKey = this.getLookUpKey(props);
53 | const field = props.formStore.getField(props.field['#webform_key']);
54 |
55 | rules.set(`email_${props.field['#webform_key']}`, {
56 | rule: () => field.isEmpty || validator.isEmail(field.value),
57 | hint: value =>
58 | ,
59 | shouldValidate: () => field.isBlurred && !field.isEmpty,
60 | });
61 | rules.set(`email_neverbounce_${props.field['#webform_key']}`, {
62 | rule: () => {
63 | const lookUp = get(field.lookUps, lookUpKey);
64 | return field.isEmpty || !lookUp || lookUp.lookUpSuccessful;
65 | },
66 | hint: () =>
67 | ,
68 | shouldValidate: () => field.isBlurred && !field.isEmpty && validator.isEmail(field.value),
69 | });
70 |
71 |
72 | props.registerLookUp(lookUpKey, this.lookUpFields);
73 | }
74 |
75 | getLookUpKey(props) {
76 | return `${(props || this.props).field['#webform_key']}-email`;
77 | }
78 |
79 | prepareLookUp(fields) {
80 | const emailField = this.props.getField('email');
81 |
82 | if(
83 | !fields.email ||
84 | !emailField.field ||
85 | WebformUtils.isEmpty(emailField.field.props, fields.email) ||
86 | !validator.isEmail(fields.email)
87 | ) {
88 | return false;
89 | }
90 |
91 | const query = `&email=${fields.email}`;
92 |
93 | return {
94 | query,
95 | checkResponse: json => (json.success ? json : false),
96 | isSuccessful: response => response.result !== 1,
97 | };
98 | }
99 |
100 | render() {
101 | return (
102 |
107 | );
108 | }
109 | }
110 |
111 | export default composeLookUp(EmailField);
112 |
--------------------------------------------------------------------------------
/src/EmailField/styled/email.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Input from '../../Input';
3 |
4 | export default styled(Input)`
5 | color: #000;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/FetchForm/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import fetch from 'fetch-everywhere';
4 | import { observer } from 'mobx-react';
5 | import Webform from '../Webform';
6 |
7 | const LOAD_STATES = {
8 | LOADING: 0,
9 | SUCCESS: 1,
10 | ERROR: 2,
11 | };
12 |
13 |
14 | @observer
15 | class FetchForm extends Component {
16 | static propTypes = {
17 | baseUrl: PropTypes.string.isRequired,
18 | path: PropTypes.string.isRequired,
19 | field: PropTypes.string,
20 | }
21 |
22 | static defaultProps = {
23 | field: 'field_form',
24 | }
25 |
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = {
30 | loadState: LOAD_STATES.LOADING,
31 | };
32 |
33 | this.fetchForm = this.fetchForm.bind(this);
34 | }
35 |
36 | componentDidMount() {
37 | this.fetchForm();
38 | }
39 |
40 | componentDidUpdate(prevProps) {
41 | if(prevProps.baseUrl !== this.props.baseUrl || prevProps.path !== this.props.path || prevProps.field !== this.props.field) {
42 | this.fetchForm();
43 | }
44 | }
45 |
46 | fetchForm() {
47 | if(!this.props.baseUrl) {
48 | this.setState({ loadState: LOAD_STATES.ERROR });
49 | return;
50 | }
51 |
52 | // console.info('Loading..');
53 | this.setState({ loadState: LOAD_STATES.LOADING });
54 |
55 | this.fetch = fetch(`${this.props.baseUrl}/url?url=/${this.props.path}&_format=json`)
56 | .then(data => data.json())
57 | .then((json) => {
58 | if(typeof json.content !== 'object' || (!this.props.field && !json.content.form_id) || (this.props.field && !json.content[this.props.field].form_id)) {
59 | throw Error('Combination of url && field didn\'t work');
60 | }
61 | // console.info('JSON!', json.content[this.props.field]);
62 | this.setState({
63 | form: this.props.field ? json.content[this.props.field] : json.content,
64 | loadState: LOAD_STATES.SUCCESS,
65 | });
66 | })
67 | .catch((error) => {
68 | console.error('Error loading form:', error);
69 | this.setState({ loadState: LOAD_STATES.ERROR });
70 | });
71 | }
72 |
73 | render() {
74 | switch(this.state.loadState) {
75 | case LOAD_STATES.LOADING:
76 | return LOADING..
;
77 | case LOAD_STATES.ERROR:
78 | return ERROR LOADING FORM.
;
79 | case LOAD_STATES.SUCCESS:
80 | return ( {
88 | const block = false;
89 | console.info('submithook, block:', block);
90 | return Promise.resolve({
91 | submit: !block,
92 | errors: ['Custom error, override submit'],
93 | });
94 | }}
95 | onAfterSubmit={() => console.info('afterhook')}
96 | />);
97 | default:
98 | return null;
99 | }
100 | }
101 | }
102 |
103 | export default FetchForm;
104 |
--------------------------------------------------------------------------------
/src/Fieldset/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import getNested from 'get-nested';
4 | import { observer } from 'mobx-react';
5 | import WebformElement from '../WebformElement';
6 | import FormStore from '../Observables/Form';
7 | // styled
8 | import StyledFieldset from './styled/fieldset';
9 | import Legend from './styled/legend';
10 | import FieldsetInner from './styled/fieldset-inner';
11 |
12 | @observer
13 | class Fieldset extends Component {
14 | static meta = {
15 | wrapper: StyledFieldset,
16 | wrapperProps: { className: 'hrw-fieldset hrw-form-row' },
17 | label: Legend,
18 | labelVisibility: 'invisible',
19 | hasValue: false,
20 | };
21 |
22 | static propTypes = {
23 | field: PropTypes.shape({
24 | composite_elements: PropTypes.array,
25 | '#webform_key': PropTypes.string.isRequired,
26 | }).isRequired,
27 | formStore: PropTypes.instanceOf(FormStore).isRequired,
28 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
29 | onChange: PropTypes.func.isRequired,
30 | onBlur: PropTypes.func.isRequired,
31 | settings: PropTypes.shape().isRequired,
32 | children: PropTypes.node,
33 | webformSettings: PropTypes.shape().isRequired,
34 | style: PropTypes.shape(),
35 | webformPage: PropTypes.string,
36 | form: PropTypes.shape({
37 | settings: PropTypes.object.isRequired,
38 | }).isRequired,
39 | status: PropTypes.string.isRequired,
40 | childrenAdjacent: PropTypes.bool,
41 | url: PropTypes.string.isRequired,
42 | };
43 |
44 | static defaultProps = {
45 | children: [],
46 | style: {},
47 | webformPage: null,
48 | childrenAdjacent: false,
49 | };
50 |
51 | /**
52 | * Get value from field based on key. Checks overridden values, and if so, provides master value
53 | *
54 | * @param field Full field object
55 | * @param key Key without leading #, e.g. pattern
56 | *
57 | * @return any Value of field or master, depending of overrides.
58 | */
59 | static getValue(field, key) {
60 | if(key.startsWith('#')) {
61 | throw new Error('Please use the field without leading hash.');
62 | }
63 |
64 | if(field[`#override_${key}`]) {
65 | return field[`#${key}`];
66 | }
67 |
68 | return getNested(() => field.composite_elements.find(element => element['#key'] === key)['#default_value'], null);
69 | }
70 |
71 | getFormElements() {
72 | const formElements = this.props.field.composite_elements || [];
73 | return formElements.map(field => (
74 |
88 | ));
89 | }
90 |
91 | render() {
92 | const formElements = this.getFormElements();
93 | return (
94 |
95 | {!this.props.childrenAdjacent && this.props.children}
96 | {formElements}
97 | {this.props.childrenAdjacent && this.props.children}
98 |
99 | );
100 | }
101 | }
102 |
103 | export default Fieldset;
104 |
--------------------------------------------------------------------------------
/src/Fieldset/styled/fieldset-inner.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | & > :last-child {
5 | margin-bottom: 0;
6 | }
7 | `;
8 |
--------------------------------------------------------------------------------
/src/Fieldset/styled/fieldset.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import FormRow from '../../WebformElement/styled/form-row';
3 |
4 | export default styled.fieldset`
5 | box-sizing: border-box;
6 | margin-bottom: calc(${p => p.theme.spacingUnitFieldset} * 1.5);
7 | border: ${p => p.theme.fieldsetBorder};
8 | border-radius: ${p => p.theme.borderRadius};
9 | background-color: ${p => p.theme.fieldsetBgColor};
10 | padding: calc(${p => p.theme.spacingUnitFieldset} * 1) calc(${p => p.theme.spacingUnitFieldset} * 1);
11 |
12 | @media screen and (min-width: 768px) {
13 | padding: calc(${p => p.theme.spacingUnitFieldset} * 2.5) calc(${p => p.theme.spacingUnitFieldset} * 2.5);
14 | }
15 |
16 | &${FormRow} {
17 | border: none;
18 | }
19 | `;
20 |
--------------------------------------------------------------------------------
/src/Fieldset/styled/legend.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Label from '../../WebformElement/styled/label';
3 |
4 | const Legend = Label.withComponent('legend');
5 |
6 | export default styled(Legend)`
7 | width: 100%;
8 | float: left;
9 | padding: 0;
10 | margin-bottom: calc(${p => p.theme.spacingUnitFieldset} / 2);
11 |
12 | ${p => p.labelDisplay === 'inline' && `
13 | @media (min-width: 768px) {
14 | width: ${p.theme.inlineLabelWidth};
15 | padding-right: calc(${p.theme.spacingUnit} / 2);
16 | }
17 | `}
18 |
19 | ${p => p.labelDisplay === 'invisible' && `
20 | display: none;
21 | `}
22 |
23 | ${p => p.labelDisplay === 'before' && `
24 | display: block;
25 | float: none;
26 | width: 100%;
27 | `}
28 | `;
29 |
--------------------------------------------------------------------------------
/src/Fieldset/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import FormRow from '../../WebformElement/styled/form-row';
2 |
3 | const Fieldset = FormRow.withComponent('fieldset');
4 |
5 | export default Fieldset;
6 |
--------------------------------------------------------------------------------
/src/FormRow/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Fieldset from '../Fieldset';
3 | // styled
4 | import FieldsetFormRow from '../Fieldset/styled/wrapper';
5 |
6 | const FormRow = props => (
7 |
10 | );
11 | FormRow.meta = {
12 | wrapper: FieldsetFormRow,
13 | label: Fieldset.meta.label,
14 | wrapperProps: Fieldset.meta.wrapperProps,
15 | labelVisibility: Fieldset.meta.labelVisibility,
16 | hasValue: false,
17 | };
18 |
19 | export default FormRow;
20 |
--------------------------------------------------------------------------------
/src/Hidden/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import Converse from '../Converse';
5 | import Form from '../Observables/Form';
6 |
7 | // styled
8 | import StyledHidden from './styled/hidden';
9 |
10 | const Hidden = props => (
11 |
16 | );
17 |
18 | /**
19 | * ~financial_amount_monthly_suggestion|0| + financial_amount_monthly|0| + financial_amount_yearly_suggestion|0| + financial_amount_yearly|0|~
20 | * @param value
21 | * @param values
22 | * @returns {*}
23 | */
24 | Hidden.rewriteValue = (value, values) => {
25 | if(!value || !value.toString().startsWith('~') || !value.toString().endsWith('~')) return value;
26 | return Converse.calculateFormula(values, value.slice(1, -1));
27 | };
28 |
29 | Hidden.propTypes = {
30 | value: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]).isRequired,
31 | formStore: PropTypes.instanceOf(Form).isRequired,
32 | };
33 |
34 | const DecoratedHidden = observer(Hidden);
35 |
36 | DecoratedHidden.meta = {
37 | wrapper: 'div',
38 | wrapperProps: { style: { display: 'none' } },
39 | };
40 |
41 | DecoratedHidden.rewriteValue = Hidden.rewriteValue;
42 |
43 | export default DecoratedHidden;
44 |
--------------------------------------------------------------------------------
/src/Hidden/styled/hidden.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Input from '../../Input/styled/input';
3 |
4 | export default styled(Input)`
5 | display: none;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/IBAN/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { isValidIBAN } from 'ibantools';
4 | import { observer } from 'mobx-react';
5 | import Input from '../Input';
6 | import rules from '../Webform/rules';
7 | import RuleHint from '../RuleHint';
8 | import WebformUtils from '../WebformUtils';
9 |
10 | @observer
11 | class IBAN extends Component {
12 | static meta = {
13 | validations: [el => `iban_${el.key}`],
14 | };
15 |
16 | static propTypes = {
17 | field: PropTypes.shape({
18 | '#webform_key': PropTypes.string.isRequired,
19 | '#ibanError': PropTypes.string,
20 | }).isRequired,
21 | settings: PropTypes.shape().isRequired,
22 | };
23 |
24 | static rewriteValue(value) {
25 | return (value || '').replace(/\s/g, '').toUpperCase();
26 | }
27 |
28 | constructor(props) {
29 | super(props);
30 |
31 | rules.delete(`pattern_${props.field['#webform_key']}`); // IBAN has own validation, pattern is only used in back end.
32 |
33 | rules.set(`iban_${props.field['#webform_key']}`, {
34 | rule: value => isValidIBAN(value),
35 | hint: () =>
36 | ,
37 | shouldValidate: field => field.isBlurred,
38 | });
39 | }
40 |
41 | render() {
42 | return (
43 |
47 | );
48 | }
49 | }
50 |
51 | export default IBAN;
52 |
--------------------------------------------------------------------------------
/src/Input/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import Parser from '../Parser';
5 | import BaseInput from '../BaseInput';
6 | import WebformElement from '../WebformElement';
7 | // styled
8 | import Wrapper from './styled/wrapper';
9 | import InnerWrapper from './styled/inner-wrapper';
10 | import Prefix from './styled/prefix';
11 | import Suffix from './styled/suffix';
12 | import ValidationIcon from './styled/validation-icon';
13 |
14 | @observer
15 | class Input extends Component {
16 | static propTypes = {
17 | field: PropTypes.shape({
18 | '#type': PropTypes.string.isRequired,
19 | '#placeholder': PropTypes.string,
20 | '#field_prefix': PropTypes.string,
21 | '#field_suffix': PropTypes.string,
22 | '#webform_key': PropTypes.string.isRequired,
23 | '#required': PropTypes.bool,
24 | '#mask': PropTypes.oneOfType([
25 | PropTypes.string,
26 | PropTypes.bool,
27 | ]),
28 | '#alwaysShowMask': PropTypes.oneOfType([
29 | PropTypes.string,
30 | PropTypes.bool,
31 | ]),
32 | '#min': PropTypes.string,
33 | '#max': PropTypes.string,
34 | '#step': PropTypes.string,
35 | '#attributes': PropTypes.oneOfType([
36 | PropTypes.shape({
37 | autoComplete: PropTypes.string,
38 | }),
39 | PropTypes.array,
40 | ]),
41 | }).isRequired,
42 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
43 | };
44 |
45 | getLabelDisplay() {
46 | return this.props.webformElement.getLabelDisplay();
47 | }
48 |
49 | renderTextContent(selector) {
50 | const value = this.props.field[`#field_${selector}`];
51 | if(value) {
52 | const TextContent = selector === 'prefix' ? Prefix : Suffix;
53 |
54 | return {Parser(value)};
55 | }
56 | return null;
57 | }
58 |
59 | render() {
60 | return (
61 |
62 |
63 |
64 | {this.renderTextContent('prefix')}
65 |
69 | {this.renderTextContent('suffix')}
70 |
71 |
72 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default Input;
79 |
--------------------------------------------------------------------------------
/src/Input/styled/inner-wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | display: flex;
5 | justify-content: space-between;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Input/styled/input.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import BaseInput from '../../BaseInput/styled/input';
3 |
4 | export default styled(BaseInput)`
5 |
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Input/styled/prefix.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import TextContent from './text-content';
3 |
4 | export default styled(TextContent)`
5 | margin-right: 0.5em;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Input/styled/suffix.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import TextContent from './text-content';
3 |
4 | export default styled(TextContent)`
5 | margin-left: 0.5em;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Input/styled/text-content.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | display: inline-block;
5 | align-self: center;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Input/styled/validation-icon.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import ValidationIcon from '../../BaseInput/styled/validation-icon';
3 |
4 | export default styled(ValidationIcon)`
5 |
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Input/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | display: inline-block;
5 |
6 | ${p => p.labelDisplay === 'inline' && `
7 | width: ${p.theme.inputWidth};
8 | `}
9 |
10 | ${p => p.labelDisplay === 'before' && `
11 | width: ${p.theme.inputWidth};
12 | max-width: 100%;
13 |
14 | @media (min-width: 768px) {
15 | width: calc(${p.theme.inlineLabelWidth} + ${p.theme.inputWidth});
16 | }
17 | `}
18 | `;
19 |
--------------------------------------------------------------------------------
/src/LookUp/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import fetch from 'fetch-everywhere';
4 | import getNested from 'get-nested';
5 | import { get, set } from 'mobx';
6 | import FormStore from '../Observables/Form';
7 |
8 | function composeLookUp(LookUpComponent) {
9 | return class extends Component {
10 | static meta = LookUpComponent.meta || {};
11 |
12 | static propTypes = {
13 | formStore: PropTypes.instanceOf(FormStore).isRequired,
14 | field: PropTypes.shape({
15 | '#webform_key': PropTypes.string.isRequired,
16 | composite_elements: PropTypes.arrayOf(PropTypes.shape({
17 | '#webform_key': PropTypes.string.isRequired,
18 | '#default_value': PropTypes.string,
19 | })),
20 | }).isRequired,
21 | onBlur: PropTypes.func.isRequired,
22 | onChange: PropTypes.func.isRequired,
23 | };
24 |
25 | static rewriteValue = LookUpComponent.rewriteValue;
26 |
27 | constructor(props) {
28 | super(props);
29 |
30 | this.formKeySuffix = `-${props.field['#webform_key']}`;
31 |
32 | /**
33 | * Example: {
34 | * street: {
35 | * formKey: 'street', // #webform_key in Drupal
36 | * apiValue: response => response.street, // Method that receives the response object, and returns the value to pre-fill or false if not to pre-fill.
37 | * hideField: true, // Boolean to determine default state of field.
38 | * triggerLookUp: false, // Boolean to determine whether blurring this field should trigger the look-up.
39 | * },
40 | * }
41 | */
42 | this.lookUpFields = {};
43 |
44 | this.lookUpOnBlurOnly = false;
45 |
46 | this.lookUpBase = null;
47 | this.el = null;
48 |
49 | this.state = {
50 | query: '',
51 | };
52 |
53 | this.onChange = this.onChange.bind(this);
54 | this.onBlur = this.onBlur.bind(this);
55 | this.onMount = this.onMount.bind(this);
56 | this.getField = this.getField.bind(this);
57 | this.getState = this.getState.bind(this);
58 | this.fieldIterator = this.fieldIterator.bind(this);
59 | this.triggerLookUp = this.triggerLookUp.bind(this);
60 | this.registerLookUp = this.registerLookUp.bind(this);
61 | }
62 |
63 | componentDidMount() {
64 | this.setFieldVisibility(true);
65 | this.setManualOverride(false);
66 | }
67 |
68 | onMount(el) {
69 | if(el) {
70 | this.el = el;
71 | this.lookUpBase = el.lookUpBase;
72 | this.lookUpFields = el.lookUpFields || {};
73 | }
74 | }
75 |
76 | onBlur(e) {
77 | const triggerElement = Object.values(this.lookUpFields).find(element => element.formKey === e.target.name);
78 | if(!triggerElement || !triggerElement.triggerLookUp) {
79 | return;
80 | }
81 |
82 | this.triggerLookUp();
83 |
84 | this.props.onBlur(e);
85 | }
86 |
87 | onChange(e) {
88 | const triggerElement = Object.values(this.lookUpFields).find(element => element.formKey === e.target.name);
89 | if(triggerElement && !triggerElement.triggerLookUp) {
90 | this.setManualOverride(true);
91 | }
92 | this.props.onChange(e);
93 |
94 | if(!triggerElement || this.lookUpOnBlurOnly) return;
95 |
96 | const field = this.props.formStore.getField(triggerElement.formKey);
97 |
98 | if(!field) return;
99 |
100 | const lookUpKey = this.el.getLookUpKey();
101 | const lookUp = get(field.lookUps, lookUpKey);
102 | if(lookUp && lookUp.lookUpSent) this.triggerLookUp();
103 | }
104 |
105 | setFieldVisibility() {
106 | const lookUpKey = this.el.getLookUpKey();
107 | this.fieldIterator((field, element) => {
108 | const lookUp = get(field.lookUps, lookUpKey);
109 | if(lookUp) {
110 | set(field.lookUps, lookUpKey, {
111 | ...lookUp,
112 | lookUpHide: !!element.hideField,
113 | lookUpDisabled: !!element.disableField,
114 | });
115 | }
116 | });
117 | }
118 |
119 | setManualOverride(override) {
120 | const { field } = this.getField('manualOverride');
121 | if(field) {
122 | field.value = override.toString();
123 | }
124 | }
125 |
126 | /**
127 | * @param elementKey
128 | * @returns {Boolean | {field: Field, element}}
129 | */
130 | getField(elementKey) {
131 | if(!this.lookUpFields) {
132 | return false;
133 | }
134 | const element = this.lookUpFields[elementKey];
135 | if(!element) {
136 | return false;
137 | }
138 | const field = this.props.formStore.getField(element.formKey);
139 | return {
140 | element,
141 | field,
142 | };
143 | }
144 |
145 | getFields() {
146 | if(!this.lookUpFields) {
147 | return [];
148 | }
149 | return Object.values(this.lookUpFields).map(element => this.props.formStore.getField(element.formKey));
150 | }
151 |
152 | getState() {
153 | return this.state;
154 | }
155 |
156 | registerLookUp(lookUpKey, lookUpFields, lookUpOnBlurOnly = false) {
157 | this.lookUpFields = lookUpFields;
158 | this.lookUpOnBlurOnly = lookUpOnBlurOnly;
159 | this.fieldIterator((field, element) => {
160 | set(field.lookUps, lookUpKey, {
161 | lookUpSent: false,
162 | lookUpSuccessful: true,
163 | lookUpHide: element.lookUps,
164 | lookUpDisabled: false,
165 | });
166 | });
167 | }
168 |
169 | triggerLookUp() {
170 | const fields = {};
171 | this.fieldIterator((field, element) => {
172 | const value = field.value;
173 | if(!field.empty) {
174 | fields[element.elementKey] = value;
175 | }
176 | });
177 |
178 | const lookUpObject = this.el.prepareLookUp ? this.el.prepareLookUp(fields) : false;
179 | if(lookUpObject && lookUpObject.query !== this.state.query) {
180 | this.lookUp(lookUpObject);
181 | }
182 | }
183 |
184 | fieldIterator(cb) {
185 | let stop = false;
186 | if(!this.lookUpFields) {
187 | return false;
188 | }
189 | Object.keys(this.lookUpFields).forEach((elementKey) => {
190 | if(stop === true) {
191 | return;
192 | }
193 | const { element, field } = this.getField(elementKey);
194 | if(field) {
195 | stop = cb(field, element) === false;
196 | }
197 | });
198 | return true;
199 | }
200 |
201 | lookUp(request) {
202 | const { query, headers = {} } = request;
203 | this.setState({ query });
204 |
205 | // eslint-disable-next-line no-undef
206 | const headersObject = new Headers(headers);
207 |
208 | fetch(`${this.lookUpBase}${query}`, {
209 | headers: headersObject,
210 | })
211 | .then(res => res.json())
212 | .then(response => this.processResponse(response, request))
213 | .catch(response => this.processResponse(response, request));
214 | }
215 |
216 | processResponse(jsonResponse, {
217 | query,
218 | checkResponse = () => false,
219 | isSuccessful = () => true,
220 | }) {
221 | if(this.state.query !== query) {
222 | console.warn('A lookUp query was returned, but we already fired another one. Ignoring this result.', query, jsonResponse);
223 | return;
224 | }
225 |
226 | const response = checkResponse(jsonResponse);
227 | const successful = isSuccessful(response);
228 |
229 | const lookUpKey = this.el.getLookUpKey();
230 | const lookUpField = this.props.formStore.getField(this.props.field['#webform_key']);
231 |
232 | set(lookUpField.lookUps, lookUpKey, {
233 | ...get(lookUpField.lookUps, lookUpKey),
234 | lookUpSent: true,
235 | lookUpSuccessful: successful,
236 | });
237 |
238 | // Let every field know the lookUp was sent, and if it was successful
239 | Object.keys(this.lookUpFields).forEach((elementKey) => {
240 | const { field } = this.getField(elementKey);
241 | if(field) {
242 | set(field.lookUps, lookUpKey, {
243 | ...get(field.lookUps, lookUpKey),
244 | lookUpSent: true,
245 | lookUpSuccessful: successful,
246 | });
247 | }
248 | });
249 |
250 | this.fieldIterator((field, element) => {
251 | const value = getNested(() => element.apiValue(response));
252 | if(value) {
253 | field.value = value;
254 | field.isBlurred = true;
255 | }
256 | });
257 |
258 | this.setManualOverride(!successful); // If successful, set to false since values are overridden
259 |
260 | if(this.el.lookUpCallback) {
261 | this.el.lookUpCallback(response);
262 | }
263 | }
264 |
265 | render() {
266 | return (
267 |
280 | );
281 | }
282 | };
283 | }
284 |
285 | export default composeLookUp;
286 |
--------------------------------------------------------------------------------
/src/LookUp/styled/validation-message.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.p`
4 | margin: 0;
5 | font-size: 0.8em;
6 | padding-bottom: calc(${p => p.theme.spacingUnit});
7 | line-height: 1.2em;
8 | `;
9 |
--------------------------------------------------------------------------------
/src/Number/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer } from 'mobx-react';
4 | import Input from '../Input';
5 | import WebformUtils from '../WebformUtils';
6 |
7 | @observer
8 | class Number extends Component {
9 |
10 | /**
11 | * @param {Field} field Field
12 | */
13 | static validate(field) {
14 | const numberError = WebformUtils.getCustomValue(field.element, 'numberError', field.formStore.form.settings);
15 |
16 | if(field.element['#step']) {
17 | const dec = Math.max(...[field.element['#step'], field.value].map(n => `${n}.`.split('.')[1].length));
18 | if(Math.round(field.value * (10 ** dec)) % Math.round(field.element['#step'] * (10 ** dec)) !== 0) {
19 | return [numberError];
20 | }
21 | }
22 |
23 | if(field.element['#min'] && parseFloat(field.value) < parseFloat(field.element['#min'])) {
24 | return [numberError];
25 | }
26 |
27 | if(field.element['#max'] && parseFloat(field.value) > parseFloat(field.element['#max'])) {
28 | return [numberError];
29 | }
30 |
31 | return [];
32 | }
33 |
34 | static propTypes = {
35 | onChange: PropTypes.func.isRequired,
36 | onBlur: PropTypes.func.isRequired,
37 | };
38 |
39 | constructor(props) {
40 | super(props);
41 |
42 | this.onChange = this.onChange.bind(this);
43 | }
44 |
45 | onChange(e) {
46 | this.props.onChange(e);
47 | }
48 |
49 | render() {
50 | return (
51 |
57 | );
58 | }
59 | }
60 |
61 | export default Number;
62 |
--------------------------------------------------------------------------------
/src/Observables/Field.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { observable, computed, values } from 'mobx';
3 | import getNested from 'get-nested';
4 | import RuleHint from '../RuleHint';
5 | import rules from '../Webform/rules';
6 | import { components } from '../index';
7 | import { formatConditionals, checkConditionals, supportedActions } from '../Webform/conditionals';
8 | import WebformUtils from '../WebformUtils';
9 |
10 | class Field {
11 | key;
12 | element;
13 |
14 | /**
15 | * @var Form
16 | */
17 | formStore;
18 | componentClass;
19 | conditionals;
20 |
21 | /**
22 | * @var {Field}
23 | */
24 | parent;
25 | /**
26 | *
27 | * @type {Array.}
28 | */
29 | parents = [];
30 |
31 | page = 'no-page';
32 |
33 | /**
34 | * @deprecated
35 | * Better not use the component at all. All logic should be in this file
36 | */
37 | @observable component = null;
38 |
39 | /**
40 | * By default, the value is ''.
41 | * This value gets updated in the WebformElement component.
42 | * @type {string}
43 | */
44 | @observable value = '';
45 |
46 | /**
47 | * As soon as the field was ever blurred or an user tries to submit an page, this value becomes true.
48 | * It can be used to determine if the field should display a checkmark or errors etc.
49 | * @type {boolean}
50 | */
51 | @observable isBlurred = false;
52 |
53 | @observable lookUps = {};
54 |
55 | constructor(formStore, element, parent) {
56 | if(!element['#webform_key']) {
57 | throw new Error('Element key is required');
58 | }
59 |
60 | this.value = element['#value'] || element['#default_value'] || '';
61 |
62 | this.formStore = formStore;
63 | this.key = element['#webform_key'];
64 | this.element = element;
65 | this.componentClass = components[element['#type']] || components.default;
66 | this.conditionals = formatConditionals(element['#states']);
67 | this.parent = parent;
68 | if(parent) {
69 | this.parents = [parent, ...this.parent.parents];
70 | }
71 | if(element['#type'] === 'webform_wizard_page') {
72 | this.page = this.key;
73 | } else if(parent) {
74 | this.page = this.parent.page;
75 | }
76 |
77 | rules.set(`${supportedActions.required}_${this.key}`, {
78 | rule: () => !this.isEmpty &&
79 | (!this.componentClass.isEmpty || !this.componentClass.isEmpty(this)),
80 | hint: value =>
81 | (),
90 | shouldValidate: field => field.isBlurred,
91 | });
92 |
93 | const pattern = this.element['#pattern'];
94 | if(pattern) {
95 | rules.set(`pattern_${this.key}`, {
96 | rule: (value = '') => new RegExp(pattern).test(value) || this.isEmpty,
97 | hint: (value) => {
98 | const patternError = WebformUtils.getCustomValue(this.element, 'patternError', this.formStore.form.settings);
99 | const populatedPatternError = getNested(() => this.formStore.form.settings.custom_elements.patternError['#options'][patternError], this.element['#patternError'] || WebformUtils.getErrorMessage(element, '#required_error') || 'Please enter a valid value.');
100 | return ;
101 | },
102 | shouldValidate: field => field.isBlurred && WebformUtils.validateRule(rules.get(`${supportedActions.required}_${this.key}`), field),
103 | });
104 | }
105 | }
106 |
107 | /**
108 | * Checks is the field is currently valid.
109 | * @returns {boolean}
110 | */
111 | @computed get valid() {
112 | return this.errors.length === 0;
113 | }
114 |
115 | @computed get validations() {
116 | const validations = [
117 | this.required ? `${supportedActions.required}_${this.key}` : null,
118 | this.element['#pattern'] ? `pattern_${this.key}` : null,
119 | ];
120 |
121 | const populatedValidations = validations.map(validation => rules.get(validation) || null);
122 |
123 | const customValidationKeys = getNested(() => this.componentClass.meta.validations, [])
124 | .map(validation => validation(this) || null);
125 |
126 | populatedValidations.push(...customValidationKeys.map(k => rules.get(k)).filter(k => k));
127 |
128 | return populatedValidations;
129 | }
130 |
131 | /**
132 | * This is used to check if the 'required' validation is met.
133 | * @returns {boolean}
134 | */
135 | @computed get isEmpty() {
136 | if(this.value === '' || this.value === false) {
137 | return true;
138 | }
139 |
140 | if(this.element['#mask']) {
141 | const mask = this.element['#mask'].replace(/9|a|A/g, '_');
142 | return this.value === mask;
143 | }
144 |
145 | return false;
146 | }
147 |
148 | @computed get errors() {
149 | const validations = this.validations;
150 | const field = this;
151 |
152 | // Field is always valid, if there is none, OR the field is invisible, OR a parent is invisible.
153 | if(!field || !this.visible) {
154 | return [];
155 | }
156 |
157 | const errors = [];
158 |
159 | // Check if there is a static 'validate' function on the component class.
160 | if(typeof field.componentClass.validate === 'function') {
161 | errors.push(...field.componentClass.validate(this));
162 | }
163 |
164 | const fails = validations ? validations.filter(validation => !WebformUtils.validateRule(validation, field)) : [];
165 |
166 | errors.push(...fails.map(rule => rule.hint(this.value)));
167 |
168 | return errors;
169 | }
170 |
171 | @computed get visible() {
172 | const lookUps = values(this.lookUps);
173 | if(lookUps.length > 0 && lookUps.every(l => l.lookUpHide && !l.lookUpSent)) return false;
174 | return (this.parent ? this.parent.visible : true) && (typeof this.conditionalLogicResults.visible === 'undefined') ? true : this.conditionalLogicResults.visible;
175 | }
176 |
177 | @computed get required() {
178 | return (typeof this.conditionalLogicResults.required === 'undefined') ? !!this.element['#required'] : this.conditionalLogicResults.required;
179 | }
180 |
181 | @computed get enabled() {
182 | const lookUps = values(this.lookUps);
183 | if(lookUps.length > 0 && lookUps.every(l => l.lookUpDisabled)) return false;
184 | return (typeof this.conditionalLogicResults.enabled === 'undefined') ? true : this.conditionalLogicResults.enabled;
185 | }
186 |
187 | @computed get conditionalLogicResults() {
188 | return checkConditionals(this.formStore, this.key);
189 | }
190 |
191 | /**
192 | * @deprecated
193 | * Use field.value.
194 | */
195 | getValue() {
196 | return this.value;
197 | }
198 | }
199 |
200 | export default Field;
201 |
--------------------------------------------------------------------------------
/src/Observables/Form.js:
--------------------------------------------------------------------------------
1 | import { observable, computed, action } from 'mobx';
2 | import getNested from 'get-nested';
3 | import Field from './Field';
4 |
5 | class Form {
6 |
7 | /**
8 | * The unique key for this form.
9 | */
10 | key = null;
11 |
12 | /**
13 | * The raw data provided by Drupal.
14 | */
15 | form;
16 |
17 | @observable settings = {};
18 |
19 | /**
20 | * All fields in this form.
21 | * @type {Array.}
22 | */
23 | @observable fields = [];
24 |
25 | @observable page = 'no-page';
26 |
27 | /**
28 | * All visible fields in this form.
29 | * @type {Array.}
30 | */
31 | @computed get visibleFields() {
32 | return this.fields.filter(field => field.visible);
33 | }
34 |
35 | /**
36 | * All visible fields in this form of the current page.
37 | * @type {Array.}
38 | */
39 | @computed get visibleFieldsOfCurrentPage() {
40 | return this.visibleFields.filter(field => field.page === this.page);
41 | }
42 |
43 |
44 | constructor(formId, { settings, form, defaultValues = {} }) {
45 | this.key = formId;
46 | this.form = form;
47 | this.settings = settings;
48 |
49 | // Create all fields.
50 | this.form.elements.forEach(element => this.createField(element));
51 |
52 | // Set all default values.
53 | Object.keys(defaultValues).forEach((key) => {
54 | const field = this.getField(key);
55 | if(field) field.value = defaultValues[key];
56 | });
57 |
58 | // Start at the first page
59 | const page = this.fields.find(f => f.page !== 'no-page');
60 | if(page) this.page = page.page;
61 | }
62 |
63 | @action.bound
64 | createField(element, parent) {
65 | const field = new Field(this, element, parent);
66 | if(field.componentClass) this.fields.push(field);
67 |
68 | // If it has children, create them too.
69 | if(element.composite_elements) {
70 | element.composite_elements.forEach(e => this.createField(e, field));
71 | }
72 | }
73 |
74 | /**
75 | * @param key
76 | * @returns {Field}
77 | */
78 | getField(key) {
79 | return this.fields.find(field => field.key === key);
80 | }
81 |
82 | @computed get values() {
83 | const values = {};
84 |
85 | this.visibleFields.filter(field => !field.isEmpty).forEach((field) => {
86 | values[field.key] = typeof field.componentClass.rewriteValue === 'function' ? field.componentClass.rewriteValue(field.value, values) : field.value;
87 | });
88 |
89 | return values;
90 | }
91 |
92 | @computed get valid() {
93 | return !this.fields.find(field => field.visible && !field.valid);
94 | }
95 |
96 | isValid(page) {
97 | const invalid = this.fields.find((field) => {
98 | // Only check the current page
99 | if(!field.component || field.component.props.webformPage !== page) return false;
100 |
101 | // If an error was found, return true
102 | return !field.valid;
103 | });
104 |
105 | // If an error was found, return false
106 | return !invalid;
107 | }
108 |
109 | @computed get tokens() {
110 | const tokens = {};
111 | this.fields.forEach((field) => {
112 | tokens[field.key] = field.value;
113 | if(typeof field.componentClass.getTokens === 'function') {
114 | Object.assign(tokens, field.componentClass.getTokens(this, field));
115 | }
116 | });
117 | return tokens;
118 | }
119 |
120 | @observable isSubmitting = false;
121 |
122 | @action.bound
123 | isMultipage() {
124 | return (getNested(() => this.form.elements) || []).find(element => element['#webform_key'] === 'wizard_pages') !== undefined;
125 | }
126 |
127 | }
128 |
129 | export default Form;
130 |
--------------------------------------------------------------------------------
/src/Observables/Forms.js:
--------------------------------------------------------------------------------
1 | import { observable, action } from 'mobx';
2 | import Form from './Form';
3 |
4 | class Forms {
5 |
6 | @observable forms = {};
7 |
8 | getForm(formID, options) {
9 | if(this.forms[formID]) return this.forms[formID];
10 | else if(!options) throw Error('Tried to get a form ', formID, 'but the form was not initiated.');
11 |
12 | this.forms[formID] = Forms.createForm(formID, options);
13 |
14 | return this.forms[formID];
15 | }
16 |
17 | @action
18 | deleteForm(formID) {
19 | delete this.forms[formID];
20 | }
21 |
22 | @action
23 | static createForm(formID, options) {
24 | return new Form(formID, options);
25 | }
26 | }
27 |
28 | const form = new Forms();
29 |
30 | export default form;
31 |
--------------------------------------------------------------------------------
/src/ParagraphField/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject, observer } from 'mobx-react';
4 | import Parser, { template } from '../Parser';
5 | import FormStore from '../Observables/Form';
6 |
7 | @inject('formStore')
8 | @observer
9 | class ParagraphField extends Component {
10 | static propTypes = {
11 | field: PropTypes.shape({
12 | '#message_message': PropTypes.string,
13 | }).isRequired,
14 | formStore: PropTypes.instanceOf(FormStore).isRequired,
15 | };
16 |
17 | getFormattedMarkup() {
18 | return Parser(template(this.props.formStore, this.props.field['#message_message']));
19 | }
20 |
21 | render() {
22 | return (
23 | {this.getFormattedMarkup()}
24 | );
25 | }
26 | }
27 |
28 | export default ParagraphField;
29 |
--------------------------------------------------------------------------------
/src/Parser/index.js:
--------------------------------------------------------------------------------
1 | import Parser from 'react-html-parser';
2 | import { Html5Entities } from 'html-entities';
3 | import template from './template';
4 |
5 | const entities = new Html5Entities();
6 |
7 | export default function (string) {
8 | const decoded = entities.decode(string);
9 | return Parser(decoded);
10 | }
11 |
12 | export { template };
13 |
--------------------------------------------------------------------------------
/src/Parser/template.js:
--------------------------------------------------------------------------------
1 | const regex = /{{(\w+)}}/g;
2 |
3 | function parseTemplate(formStore, text) {
4 | // First check if it is really a string
5 | if(typeof text !== 'string' || typeof text.replace !== 'function') {
6 | console.info('Tried to template', text, 'but it doesn\'t have a replace function');
7 | return text;
8 | }
9 |
10 | return text.replace(regex, (found, variable) => formStore.tokens[variable]);
11 | }
12 |
13 | export default parseTemplate;
14 |
--------------------------------------------------------------------------------
/src/PhoneField/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // styled
3 | import Phone from './styled/phone';
4 |
5 | const PhoneField = props => (
6 |
11 | );
12 |
13 | export default PhoneField;
14 |
--------------------------------------------------------------------------------
/src/PhoneField/styled/phone.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Input from '../../Input';
3 |
4 | export default styled(Input)`
5 | -webkit-appearance: none;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/RadioField/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import getNested from 'get-nested';
4 | import Parser from '../Parser';
5 | import WebformElement from '../WebformElement';
6 | import Field from '../Observables/Field';
7 | import Fieldset from '../Fieldset';
8 | // styled
9 | import Wrapper from './styled/wrapper';
10 | import RadioLabel from './styled/radio-label';
11 | import Radio from './styled/radio';
12 | import Indicator from './styled/indicator';
13 | import FieldsetFormRow from '../Fieldset/styled/wrapper';
14 |
15 | class RadioField extends Component {
16 | static meta = {
17 | wrapper: FieldsetFormRow,
18 | label: Fieldset.meta.label,
19 | wrapperProps: Fieldset.meta.wrapperProps,
20 | };
21 |
22 | static propTypes = {
23 | field: PropTypes.shape({
24 | '#required': PropTypes.bool,
25 | '#options': PropTypes.arrayOf(PropTypes.shape({
26 | value: PropTypes.node,
27 | text: PropTypes.node,
28 | })),
29 | '#webform_key': PropTypes.string.isRequired,
30 | '#title_display': PropTypes.string,
31 | '#options_display': PropTypes.string,
32 | }).isRequired,
33 | value: PropTypes.oneOfType([
34 | PropTypes.string,
35 | PropTypes.number,
36 | PropTypes.bool,
37 | ]).isRequired,
38 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
39 | onChange: PropTypes.func.isRequired,
40 | onBlur: PropTypes.func.isRequired,
41 | state: PropTypes.instanceOf(Field).isRequired,
42 | };
43 |
44 | getOptionPositionDisplay() {
45 | return this.props.field['#options_display'];
46 | }
47 |
48 | getLabelPositionDisplay() {
49 | return this.props.field['#title_display'];
50 | }
51 |
52 | render() {
53 | const wrapperAttrs = {
54 | 'aria-invalid': this.props.webformElement.isValid() ? null : true,
55 | 'aria-required': this.props.field['#required'] ? true : null,
56 | };
57 |
58 | return (
59 |
64 | {
65 | getNested(() => this.props.field['#options'], []).map((option, index) => {
66 | const labelKey = `${this.props.field['#webform_key']}_${index}`;
67 | return (
68 |
73 |
83 |
84 | {Parser(option.text)}
85 |
86 | );
87 | })
88 | }
89 |
90 | );
91 | }
92 | }
93 |
94 | export default RadioField;
95 |
--------------------------------------------------------------------------------
/src/RadioField/styled/indicator.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | content: '';
5 | display: inline-block;
6 | width: ${p => p.theme.spacingUnitRadio};
7 | height: ${p => p.theme.spacingUnitRadio};
8 | margin-right: calc(${p => p.theme.spacingUnitRadio} / 2);
9 | background-color: ${p => p.theme.inputBgColor};
10 | border: 1px solid ${p => p.theme.borderColor};
11 | border-radius: 50%;
12 | `;
13 |
--------------------------------------------------------------------------------
/src/RadioField/styled/radio-label.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.label`
4 | display: block;
5 | margin-bottom: calc(${p => p.theme.spacingUnit} / 3);
6 | line-height: ${p => p.theme.inputLineHeight};
7 |
8 | ${p => [
9 | 'side_by_side',
10 | 'two_columns',
11 | 'three_columns',
12 | ].includes(p.optionDisplay) && `
13 | margin-right: calc(${p.theme.spacingUnit} / 1);
14 | display: inline-block;
15 | width: auto;
16 | `}
17 | `;
18 |
--------------------------------------------------------------------------------
/src/RadioField/styled/radio.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Indicator from './indicator';
3 |
4 | export default styled.input`
5 | /* Reset anything that could peek out or interfere with dimensions, but don't use display:none for WCAG */
6 | position: absolute;
7 | overflow: hidden;
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | outline: 0;
12 | opacity: 0;
13 |
14 | &:focus {
15 | & + ${Indicator} {
16 | box-shadow: 0 0 2px 3px ${p => p.theme.focusColor};
17 | }
18 | }
19 |
20 | &:checked {
21 | & + ${Indicator} {
22 | position: relative;
23 |
24 | &::before {
25 | content: '';
26 | position: absolute;
27 | top: 25%;
28 | left: 25%;
29 | width: 50%;
30 | height: 50%;
31 | background-color: ${p => p.theme.checkedColor};
32 | border-radius: 50%;
33 | }
34 | }
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/src/RadioField/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | @media (min-width: 768px) {
5 | display: inline-block;
6 | float: left;
7 | }
8 |
9 | ${p => p.labelDisplay === 'inline' && `
10 | width: calc(${p.theme.inlineLabelWidth} - (${p.theme.spacingUnit} / 2) - 0.5em);
11 | `}
12 |
13 | ${p => p.labelDisplay === 'before' && `
14 | float: none;
15 | width: 100%;
16 | `}
17 | `;
18 |
--------------------------------------------------------------------------------
/src/RangeField/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import WebformElement from '../WebformElement';
4 | // styled
5 | import Wrapper from './styled/wrapper';
6 | import Range from './styled/range';
7 | import RangeValueWrapper from './styled/range-value-wrapper';
8 | import RangeValue from './styled/range-value';
9 | import ValidationIcon from './styled/validation-icon';
10 |
11 | class RangeField extends Component {
12 | static propTypes = {
13 | field: PropTypes.shape({
14 | '#title_display': PropTypes.string,
15 | '#min': PropTypes.string,
16 | '#max': PropTypes.string,
17 | }).isRequired,
18 | value: PropTypes.oneOfType([
19 | PropTypes.string,
20 | PropTypes.number,
21 | PropTypes.bool,
22 | ]).isRequired,
23 | onChange: PropTypes.func.isRequired,
24 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
25 | };
26 |
27 | componentDidMount() {
28 | // rewrite to 0 if not numeric
29 | const value = !isNaN(parseFloat(this.props.value)) && isFinite(this.props.value) ? this.props.value : 0;
30 | this.props.onChange(value);
31 | }
32 |
33 | getLabelPosition() {
34 | return this.props.field['#title_display'];
35 | }
36 |
37 | getPercentageValue() {
38 | const minValue = parseFloat(this.props.field['#min']);
39 | const range = parseFloat(this.props.field['#max']) - minValue;
40 | let rangeValue = (this.props.value - minValue) / range;
41 | rangeValue = isFinite(rangeValue) ? rangeValue : 0;
42 | rangeValue = rangeValue > 0 ? rangeValue : 0;
43 |
44 | return rangeValue * 100;
45 | }
46 |
47 | render() {
48 | return (
49 |
50 |
51 |
55 |
56 | {this.props.value}
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | export default RangeField;
66 |
--------------------------------------------------------------------------------
/src/RangeField/styled/range-value-wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | box-sizing: border-box;
5 | position: relative;
6 | display: inline-block;
7 | padding-left: 7px;
8 | padding-right: 7px;
9 | width: 100%;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/RangeField/styled/range-value.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | display: inline-block;
5 | position: relative;
6 | line-height: 20px;
7 | text-align: center;
8 | border-radius: ${p => p.theme.borderRadius};
9 | background: ${p => p.theme.borderColor};
10 | padding: 5px 10px;
11 | margin-top: 7px;
12 | transform: translateX(-50%);
13 |
14 | &::after {
15 | content: '';
16 | width: 0;
17 | height: 0;
18 | position: absolute;
19 | left: 50%;
20 | top: -7px;
21 | transform: translateX(-50%);
22 | border-left: 7px solid transparent;
23 | border-bottom: 7px solid ${p => p.theme.borderColor};
24 | border-right: 7px solid transparent;
25 | }
26 | `;
27 |
--------------------------------------------------------------------------------
/src/RangeField/styled/range.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import BaseInput from '../../BaseInput/styled/input';
3 |
4 | export default styled(BaseInput)`
5 | color: #000;
6 | padding-left: 0;
7 | padding-right: 0;
8 | margin-left: 0;
9 | margin-right: 0;
10 |
11 | &:focus {
12 | box-shadow: none;
13 | }
14 |
15 | &::-webkit-slider-thumb,
16 | &::-moz-range-thumb,
17 | &::-ms-thumb {
18 | &:focus {
19 | box-shadow: 0 0 2px 3px ${p => p.theme.focusColor};
20 | }
21 | }
22 | `;
23 |
--------------------------------------------------------------------------------
/src/RangeField/styled/validation-icon.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import ValidationIcon from '../../BaseInput/styled/validation-icon';
3 |
4 | export default styled(ValidationIcon)`
5 | position: absolute;
6 | right: 0;
7 | top: 10px;
8 | `;
9 |
--------------------------------------------------------------------------------
/src/RangeField/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | position: relative;
5 | margin: 0 0 calc(${p => p.theme.spacingUnit} / 2);
6 |
7 | @media (min-width: 768px) {
8 | display: inline-block;
9 | float: left;
10 | }
11 |
12 | & input {
13 | width: 100%;
14 | }
15 |
16 | ${p => p.labelDisplay === 'inline' && `
17 | width: calc(${p.theme.inputWidth} + 20px);
18 | padding-right: 20px;
19 | `}
20 |
21 | ${p => p.labelDisplay === 'before' && `
22 | width: calc(${p.theme.inputWidth} + 20px);
23 | padding-right: 20px;
24 |
25 | @media (min-width: 768px) {
26 | width: calc(${p.theme.inlineLabelWidth} + ${p.theme.inputWidth} + 20px);
27 | }
28 | `}
29 | `;
30 |
--------------------------------------------------------------------------------
/src/Relation/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import getNested from 'get-nested';
3 | import PropTypes from 'prop-types';
4 | import { observer } from 'mobx-react';
5 | import { get } from 'mobx';
6 | import composeLookUp from '../LookUp';
7 | import Fieldset from '../Fieldset';
8 | import RuleHint from '../RuleHint';
9 | import rules from '../Webform/rules';
10 | import FormStore from '../Observables/Form';
11 | import WebformUtils from '../WebformUtils';
12 | // styled
13 | import ValidationMessage from '../LookUp/styled/validation-message';
14 |
15 | @observer
16 | class Relation extends Component {
17 | static meta = {
18 | labelVisibility: Fieldset.meta.labelVisibility,
19 | validations: [el => `relation_membership_${el.key}`],
20 | hasValue: false,
21 | };
22 |
23 | static propTypes = {
24 | field: PropTypes.shape({
25 | '#webform_key': PropTypes.string.isRequired,
26 | '#relationError': PropTypes.string,
27 | '#membership_validation': PropTypes.oneOfType([
28 | PropTypes.number,
29 | PropTypes.bool,
30 | ]),
31 | composite_elements: PropTypes.arrayOf(PropTypes.shape()),
32 | }).isRequired,
33 | fields: PropTypes.arrayOf(PropTypes.shape()).isRequired,
34 | onBlur: PropTypes.func.isRequired,
35 | formKeySuffix: PropTypes.string.isRequired,
36 | formStore: PropTypes.instanceOf(FormStore).isRequired,
37 | url: PropTypes.string.isRequired,
38 | settings: PropTypes.shape().isRequired,
39 | registerLookUp: PropTypes.func.isRequired,
40 | };
41 |
42 | constructor(props) {
43 | super(props);
44 |
45 | this.lookUpFields = {
46 | relation_number: {
47 | elementKey: 'relation_number',
48 | formKey: `relation_number${props.formKeySuffix}`,
49 | triggerLookUp: true,
50 | apiValue: () => false,
51 | required: true,
52 | },
53 | postcode: {
54 | elementKey: 'postcode',
55 | formKey: `address_postcode${this.fullAddressLookUp() ? `-${this.fullAddressLookUp()}` : props.formKeySuffix}`,
56 | triggerLookUp: true,
57 | apiValue: () => false,
58 | required: true,
59 | },
60 | };
61 |
62 | this.lookUpBase = `${props.url}/salesforce-lookup/contact?_format=json`;
63 |
64 | const lookUpKey = this.getLookUpKey(props);
65 | const field = props.formStore.getField(props.field['#webform_key']);
66 |
67 | rules.set(`relation_membership_${props.field['#webform_key']}`, {
68 | rule: () => {
69 | const lookUp = get(field.lookUps, lookUpKey);
70 | return !field.element['#membership_validation'] || !lookUp || !lookUp.lookUpSent || (
71 | lookUp.lookUpSent && lookUp.lookUpSuccessful
72 | );
73 | },
74 | hint: () => null,
75 | shouldValidate: () => props.fields.reduce((shouldValidate, item) =>
76 | shouldValidate && !item.isEmpty && item.isBlurred && item.valid,
77 | true,
78 | ),
79 | });
80 |
81 | props.registerLookUp(lookUpKey, this.lookUpFields);
82 | }
83 |
84 | getLookUpKey(props) {
85 | return `${(
86 | props || this.props
87 | ).field['#webform_key']}-relation`;
88 | }
89 |
90 | /**
91 | * If Relation field has full address element, get its webform key to add as suffix to fields.
92 | * @returns {bool|string} Returns false if not full address field (i.e. postcode only) or string of address webform key.
93 | */
94 | fullAddressLookUp() {
95 | return getNested(() => this.props.field.composite_elements.find(element => element['#type'] === 'webform_address_custom' || element['#type'] === 'dutch_address')['#webform_key']);
96 | }
97 |
98 | prepareLookUp(fields) {
99 | const performLookUp = this.props.fields.reduce((shouldValidate, item) =>
100 | shouldValidate && !item.isEmpty && item.isBlurred && item.valid,
101 | true,
102 | );
103 |
104 | if(!performLookUp) {
105 | return false;
106 | }
107 |
108 | const query = `&relation=${fields.relation_number}&postal_code=${fields.postcode}`;
109 |
110 | return {
111 | query,
112 | checkResponse: json => (json.Id && (!this.props.field['#membership_validation'] || parseInt(json.Aantal_lidmaatschappen__c, 10) > 0)) || false,
113 | isSuccessful: response => (!!response),
114 | };
115 | }
116 |
117 | render() {
118 | const field = this.props.formStore.getField(this.lookUpFields.relation_number.formKey);
119 | const lookUpKey = this.getLookUpKey();
120 | const lookUp = get(field.lookUps, lookUpKey);
121 |
122 | return (
123 |
128 | );
129 | }
130 | }
131 |
132 | export default composeLookUp(Relation);
133 |
--------------------------------------------------------------------------------
/src/RuleHint/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import Parser, { template } from '../Parser';
5 | import FormStore from '../Observables/Form';
6 | // styled
7 | import ValidationMessage from './styled/validation-message';
8 |
9 | @inject('formStore')
10 | class RuleHint extends Component {
11 | static propTypes = {
12 | hint: PropTypes.oneOfType([
13 | PropTypes.string,
14 | PropTypes.element,
15 | ]).isRequired,
16 | tokens: PropTypes.objectOf(PropTypes.node),
17 | component: PropTypes.oneOfType([
18 | PropTypes.string,
19 | PropTypes.shape({
20 | type: PropTypes.string.isRequired,
21 | props: PropTypes.shape({
22 | className: PropTypes.string,
23 | }),
24 | }),
25 | PropTypes.node,
26 | ]),
27 | formStore: PropTypes.instanceOf(FormStore).isRequired,
28 | };
29 |
30 | static defaultProps = {
31 | tokens: {},
32 | tokenCharacter: ':',
33 | component: 'li',
34 | }
35 |
36 | getHint() {
37 | const { tokens } = this.props;
38 | let hint = this.props.hint;
39 | Object.keys(tokens).forEach((token) => {
40 | hint = hint.replace(new RegExp(`{{${token}}}`, 'g'), tokens[token]);
41 | });
42 |
43 | return hint;
44 | }
45 |
46 | render() {
47 | const hint = this.getHint();
48 | const RuleComponent = ValidationMessage.withComponent(this.props.component.type || this.props.component);
49 | return (
50 |
51 |
52 | {Parser(template(this.props.formStore, hint))}
53 |
54 |
55 | );
56 | }
57 | }
58 |
59 | export default RuleHint;
60 |
--------------------------------------------------------------------------------
/src/RuleHint/styled/validation-message.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.li`
4 | padding: 2px 0 0;
5 | list-style-type: none;
6 | color: ${p => p.theme.errorColor};
7 | `;
8 |
--------------------------------------------------------------------------------
/src/SelectField/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Select from 'react-select';
4 | import WebformElement from '../WebformElement';
5 | // styled
6 | import Wrapper from './styled/wrapper';
7 | import ValidationIcon from './styled/validation-icon';
8 |
9 | /**
10 | * Select2
11 | * @source https://github.com/JedWatson/react-select
12 | */
13 |
14 | class SelectField extends Component {
15 | static propTypes = {
16 | field: PropTypes.shape({
17 | '#title_display': PropTypes.string.string,
18 | '#options': PropTypes.arrayOf(PropTypes.shape({
19 | value: PropTypes.node,
20 | text: PropTypes.node,
21 | })),
22 | '#webform_key': PropTypes.string.isRequired,
23 | '#multiple': PropTypes.bool,
24 | '#empty_option': PropTypes.string,
25 | }).isRequired,
26 | value: PropTypes.oneOfType([
27 | PropTypes.string,
28 | PropTypes.number,
29 | PropTypes.bool,
30 | ]).isRequired,
31 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
32 | onChange: PropTypes.func.isRequired,
33 | onBlur: PropTypes.func.isRequired,
34 | state: PropTypes.shape({
35 | required: PropTypes.bool.isRequired,
36 | enabled: PropTypes.bool.isRequired,
37 | }).isRequired,
38 | };
39 |
40 | constructor(props) {
41 | super(props);
42 |
43 | this.handleChange = this.handleChange.bind(this);
44 | }
45 |
46 | getLabelPosition() {
47 | return this.props.webformElement.getLabelDisplay();
48 | }
49 |
50 | handleChange(value) {
51 | const newValue = value || '';
52 | if(newValue && newValue.value) {
53 | this.props.onChange(newValue.value);
54 | } else {
55 | this.props.onChange(newValue);
56 | }
57 | }
58 |
59 | render() {
60 | const options = this.props.field['#options'] || {};
61 | const mappedOptions = options.map(option => ({
62 | label: option.text,
63 | value: option.value,
64 | }));
65 | return (
66 |
71 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | export default SelectField;
93 |
--------------------------------------------------------------------------------
/src/SelectField/styled/validation-icon.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import ValidationIcon from '../../BaseInput/styled/validation-icon';
3 |
4 | export default styled(ValidationIcon)`
5 | position: absolute;
6 | right: 0;
7 | top: 10px;
8 |
9 | ${p => p.success && `
10 | &::after {
11 | position: relative;
12 | top: -5px;
13 | }
14 | `}
15 | `;
16 |
--------------------------------------------------------------------------------
/src/SelectField/styled/wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | /**
5 | * React Select
6 | * ============
7 | * Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
8 | * https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
9 | * MIT License: https://github.com/JedWatson/react-select
10 | */
11 | .Select {
12 | position: relative;
13 | }
14 | .Select,
15 | .Select div,
16 | .Select input,
17 | .Select span {
18 | -webkit-box-sizing: border-box;
19 | -moz-box-sizing: border-box;
20 | box-sizing: border-box;
21 | }
22 | .Select.is-disabled > .Select-control {
23 | background-color: #f9f9f9;
24 | }
25 | .Select.is-disabled > .Select-control:hover {
26 | box-shadow: none;
27 | }
28 | .Select.is-disabled .Select-arrow-zone {
29 | cursor: default;
30 | pointer-events: none;
31 | opacity: 0.35;
32 | }
33 | .Select-control {
34 | background-color: #fff;
35 | border-color: #d9d9d9 #ccc #b3b3b3;
36 | border-radius: 4px;
37 | border: 1px solid #ccc;
38 | color: #333;
39 | cursor: default;
40 | display: table;
41 | border-spacing: 0;
42 | border-collapse: separate;
43 | height: 36px;
44 | outline: none;
45 | overflow: hidden;
46 | position: relative;
47 | width: 100%;
48 | }
49 | .Select-control:hover {
50 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
51 | }
52 | .Select-control .Select-input:focus {
53 | outline: none;
54 | }
55 | .is-searchable.is-open > .Select-control {
56 | cursor: text;
57 | }
58 | .is-open > .Select-control {
59 | border-bottom-right-radius: 0;
60 | border-bottom-left-radius: 0;
61 | background: #fff;
62 | border-color: #b3b3b3 #ccc #d9d9d9;
63 | }
64 | .is-open > .Select-control .Select-arrow {
65 | top: -2px;
66 | border-color: transparent transparent #999;
67 | border-width: 0 5px 5px;
68 | }
69 | .is-searchable.is-focused:not(.is-open) > .Select-control {
70 | cursor: text;
71 | }
72 | .is-focused:not(.is-open) > .Select-control {
73 | border-color: #007eff;
74 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1);
75 | }
76 | .Select-placeholder,
77 | .Select--single > .Select-control .Select-value {
78 | bottom: 0;
79 | color: #aaa;
80 | left: 0;
81 | line-height: 34px;
82 | padding-left: 10px;
83 | padding-right: 10px;
84 | position: absolute;
85 | right: 0;
86 | top: 0;
87 | max-width: 100%;
88 | overflow: hidden;
89 | text-overflow: ellipsis;
90 | white-space: nowrap;
91 | }
92 | .has-value.is-clearable.Select--single > .Select-control .Select-value {
93 | padding-right: 42px;
94 | }
95 | .has-value.Select--single > .Select-control .Select-value .Select-value-label,
96 | .has-value.is-pseudo-focused.Select--single > .Select-control .Select-value .Select-value-label {
97 | color: #333;
98 | }
99 | .has-value.Select--single > .Select-control .Select-value a.Select-value-label,
100 | .has-value.is-pseudo-focused.Select--single > .Select-control .Select-value a.Select-value-label {
101 | cursor: pointer;
102 | text-decoration: none;
103 | }
104 | .has-value.Select--single > .Select-control .Select-value a.Select-value-label:hover,
105 | .has-value.is-pseudo-focused.Select--single > .Select-control .Select-value a.Select-value-label:hover,
106 | .has-value.Select--single > .Select-control .Select-value a.Select-value-label:focus,
107 | .has-value.is-pseudo-focused.Select--single > .Select-control .Select-value a.Select-value-label:focus {
108 | color: #007eff;
109 | outline: none;
110 | text-decoration: underline;
111 | }
112 | .Select-input {
113 | height: 34px;
114 | padding-left: 10px;
115 | padding-right: 10px;
116 | vertical-align: middle;
117 | }
118 | .Select-input > input {
119 | width: 100%;
120 | background: none transparent;
121 | border: 0 none;
122 | box-shadow: none;
123 | cursor: default;
124 | display: inline-block;
125 | font-family: inherit;
126 | font-size: inherit;
127 | margin: 0;
128 | outline: none;
129 | line-height: 14px;
130 | /* For IE 8 compatibility */
131 | padding: 8px 0 12px;
132 | /* For IE 8 compatibility */
133 | -webkit-appearance: none;
134 | }
135 | .is-focused .Select-input > input {
136 | cursor: text;
137 | }
138 | .has-value.is-pseudo-focused .Select-input {
139 | opacity: 0;
140 | }
141 | .Select-control:not(.is-searchable) > .Select-input {
142 | outline: none;
143 | }
144 | .Select-loading-zone {
145 | cursor: pointer;
146 | display: table-cell;
147 | position: relative;
148 | text-align: center;
149 | vertical-align: middle;
150 | width: 16px;
151 | }
152 | .Select-loading {
153 | -webkit-animation: Select-animation-spin 400ms infinite linear;
154 | -o-animation: Select-animation-spin 400ms infinite linear;
155 | animation: Select-animation-spin 400ms infinite linear;
156 | width: 16px;
157 | height: 16px;
158 | box-sizing: border-box;
159 | border-radius: 50%;
160 | border: 2px solid #ccc;
161 | border-right-color: #333;
162 | display: inline-block;
163 | position: relative;
164 | vertical-align: middle;
165 | }
166 | .Select-clear-zone {
167 | -webkit-animation: Select-animation-fadeIn 200ms;
168 | -o-animation: Select-animation-fadeIn 200ms;
169 | animation: Select-animation-fadeIn 200ms;
170 | color: #999;
171 | cursor: pointer;
172 | display: table-cell;
173 | position: relative;
174 | text-align: center;
175 | vertical-align: middle;
176 | width: 17px;
177 | }
178 | .Select-clear-zone:hover {
179 | color: #D0021B;
180 | }
181 | .Select-clear {
182 | display: inline-block;
183 | font-size: 18px;
184 | line-height: 1;
185 | }
186 | .Select--multi .Select-clear-zone {
187 | width: 17px;
188 | }
189 | .Select-arrow-zone {
190 | cursor: pointer;
191 | display: table-cell;
192 | position: relative;
193 | text-align: center;
194 | vertical-align: middle;
195 | width: 25px;
196 | padding-right: 5px;
197 | }
198 | .Select-arrow {
199 | border-color: #999 transparent transparent;
200 | border-style: solid;
201 | border-width: 5px 5px 2.5px;
202 | display: inline-block;
203 | height: 0;
204 | width: 0;
205 | position: relative;
206 | }
207 | .is-open .Select-arrow,
208 | .Select-arrow-zone:hover > .Select-arrow {
209 | border-top-color: #666;
210 | }
211 | .Select--multi .Select-multi-value-wrapper {
212 | display: inline-block;
213 | }
214 | .Select .Select-aria-only {
215 | display: inline-block;
216 | height: 1px;
217 | width: 1px;
218 | margin: -1px;
219 | clip: rect(0, 0, 0, 0);
220 | overflow: hidden;
221 | float: left;
222 | }
223 | @-webkit-keyframes Select-animation-fadeIn {
224 | from {
225 | opacity: 0;
226 | }
227 | to {
228 | opacity: 1;
229 | }
230 | }
231 | @keyframes Select-animation-fadeIn {
232 | from {
233 | opacity: 0;
234 | }
235 | to {
236 | opacity: 1;
237 | }
238 | }
239 | .Select-menu-outer {
240 | border-bottom-right-radius: 4px;
241 | border-bottom-left-radius: 4px;
242 | background-color: #fff;
243 | border: 1px solid #ccc;
244 | border-top-color: #e6e6e6;
245 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
246 | box-sizing: border-box;
247 | margin-top: -1px;
248 | max-height: 200px;
249 | position: absolute;
250 | top: 100%;
251 | width: 100%;
252 | z-index: 1;
253 | -webkit-overflow-scrolling: touch;
254 | }
255 | .Select-menu {
256 | max-height: 198px;
257 | overflow-y: auto;
258 | }
259 | .Select-option {
260 | box-sizing: border-box;
261 | background-color: #fff;
262 | color: #666666;
263 | cursor: pointer;
264 | display: block;
265 | padding: 8px 10px;
266 | }
267 | .Select-option:last-child {
268 | border-bottom-right-radius: 4px;
269 | border-bottom-left-radius: 4px;
270 | }
271 | .Select-option.is-selected {
272 | background-color: #f5faff;
273 | /* Fallback color for IE 8 */
274 | background-color: rgba(0, 126, 255, 0.04);
275 | color: #333;
276 | }
277 | .Select-option.is-focused {
278 | background-color: #ebf5ff;
279 | /* Fallback color for IE 8 */
280 | background-color: rgba(0, 126, 255, 0.08);
281 | color: #333;
282 | }
283 | .Select-option.is-disabled {
284 | color: #cccccc;
285 | cursor: default;
286 | }
287 | .Select-noresults {
288 | box-sizing: border-box;
289 | color: #999999;
290 | cursor: default;
291 | display: block;
292 | padding: 8px 10px;
293 | }
294 | .Select--multi .Select-input {
295 | vertical-align: middle;
296 | margin-left: 10px;
297 | padding: 0;
298 | }
299 | .Select--multi.has-value .Select-input {
300 | margin-left: 5px;
301 | }
302 | .Select--multi .Select-value {
303 | background-color: #ebf5ff;
304 | /* Fallback color for IE 8 */
305 | background-color: rgba(0, 126, 255, 0.08);
306 | border-radius: 2px;
307 | border: 1px solid #c2e0ff;
308 | /* Fallback color for IE 8 */
309 | border: 1px solid rgba(0, 126, 255, 0.24);
310 | color: #007eff;
311 | display: inline-block;
312 | font-size: 0.9em;
313 | line-height: 1.4;
314 | margin-left: 5px;
315 | margin-top: 5px;
316 | vertical-align: top;
317 | }
318 | .Select--multi .Select-value-icon,
319 | .Select--multi .Select-value-label {
320 | display: inline-block;
321 | vertical-align: middle;
322 | }
323 | .Select--multi .Select-value-label {
324 | border-bottom-right-radius: 2px;
325 | border-top-right-radius: 2px;
326 | cursor: default;
327 | padding: 2px 5px;
328 | }
329 | .Select--multi a.Select-value-label {
330 | color: #007eff;
331 | cursor: pointer;
332 | text-decoration: none;
333 | }
334 | .Select--multi a.Select-value-label:hover {
335 | text-decoration: underline;
336 | }
337 | .Select--multi .Select-value-icon {
338 | cursor: pointer;
339 | border-bottom-left-radius: 2px;
340 | border-top-left-radius: 2px;
341 | border-right: 1px solid #c2e0ff;
342 | /* Fallback color for IE 8 */
343 | border-right: 1px solid rgba(0, 126, 255, 0.24);
344 | padding: 1px 5px 3px;
345 | }
346 | .Select--multi .Select-value-icon:hover,
347 | .Select--multi .Select-value-icon:focus {
348 | background-color: #d8eafd;
349 | /* Fallback color for IE 8 */
350 | background-color: rgba(0, 113, 230, 0.08);
351 | color: #0071e6;
352 | }
353 | .Select--multi .Select-value-icon:active {
354 | background-color: #c2e0ff;
355 | /* Fallback color for IE 8 */
356 | background-color: rgba(0, 126, 255, 0.24);
357 | }
358 | .Select--multi.is-disabled .Select-value {
359 | background-color: #fcfcfc;
360 | border: 1px solid #e3e3e3;
361 | color: #333;
362 | }
363 | .Select--multi.is-disabled .Select-value-icon {
364 | cursor: not-allowed;
365 | border-right: 1px solid #e3e3e3;
366 | }
367 | .Select--multi.is-disabled .Select-value-icon:hover,
368 | .Select--multi.is-disabled .Select-value-icon:focus,
369 | .Select--multi.is-disabled .Select-value-icon:active {
370 | background-color: #fcfcfc;
371 | }
372 | @keyframes Select-animation-spin {
373 | to {
374 | transform: rotate(1turn);
375 | }
376 | }
377 | @-webkit-keyframes Select-animation-spin {
378 | to {
379 | -webkit-transform: rotate(1turn);
380 | }
381 | }
382 |
383 | .Select {
384 | display: inline-block;
385 | width: 100%;
386 | }
387 |
388 | .Select-control {
389 | color: inherit;
390 | border: 1px solid ${p => p.theme.borderColor};
391 | background-color: ${p => p.theme.inputBgColor};
392 | border-radius: ${p => p.theme.borderRadius};
393 | height: ${p => p.theme.inputLineHeight};
394 | line-height: ${p => p.theme.inputLineHeight};
395 | width: 100%;
396 | max-width: 100%;
397 | overflow: visible;
398 | margin: 0 0 calc(${p => p.theme.spacingUnit} / 2);
399 | }
400 |
401 | .is-focused:not(.is-open) > .Select-control {
402 | outline: none;
403 | box-shadow: 0 0 2px 3px ${p => p.theme.focusColor};
404 | }
405 |
406 | .Select-placeholder,
407 | .Select--single > .Select-control .Select-value {
408 | padding: 0 calc(${p => p.theme.spacingUnit} / 2);
409 | font-size: 0.9em;
410 | position: static;
411 | }
412 |
413 | .Select-input {
414 | height: inherit;
415 | position: absolute;
416 | }
417 |
418 | .Select-input > input {
419 | padding: 0;
420 | }
421 |
422 | .Select-placeholder {
423 | color: ${p => p.theme.placeholderColor};
424 | }
425 |
426 | .Select-menu-outer {
427 | border-bottom-right-radius: ${p => p.theme.borderRadius};
428 | border-bottom-left-radius: ${p => p.theme.borderRadius};
429 | border-bottom-color: ${p => p.theme.borderColor};
430 | border-left-color: ${p => p.theme.borderColor};
431 | border-right-color: ${p => p.theme.borderColor};
432 | }
433 |
434 | .Select-option {
435 | padding: calc(${p => p.theme.spacingUnit} / 4) calc(${p => p.theme.spacingUnit} / 2);
436 | background-color: ${p => p.theme.inputBgColor};
437 | color: inherit;
438 | font-size: 0.9em;
439 | }
440 |
441 | .Select-option.is-focused {
442 | color: inherit;
443 | background-color: rgba(0, 0, 0, 0.2);
444 | }
445 |
446 | .Select-option:last-child {
447 | border-bottom-right-radius: ${p => p.theme.borderRadius};
448 | border-bottom-left-radius: ${p => p.theme.borderRadius};
449 | }
450 |
451 | .Select-clear-zone {
452 | display: none;
453 | padding-right: 3px;
454 | padding-bottom: 3px;
455 | }
456 |
457 | .Select-arrow-zone {
458 | padding-top: 5px;
459 | }
460 |
461 | position: relative;
462 | width: calc(${p => p.theme.inputWidth} + 20px);
463 | padding-right: 20px;
464 | max-width: calc(100% + 20px);
465 |
466 | @media (min-width: 768px) {
467 | width: calc(${p => p.theme.inlineLabelWidth} + ${p => p.theme.inputWidth} + 20px);
468 | }
469 |
470 | ${p => p.labelDisplay === 'inline' && `
471 | width: calc(${p.theme.inputWidth} + 20px);
472 | padding-right: 20px;
473 |
474 | @media (min-width: 768px) {
475 | display: inline-block;
476 | width: calc(${p.theme.inputWidth} * 1.3);
477 | }
478 |
479 | & .Select-placeholder,
480 | & .Select--single > .Select-control .Select-value {
481 | max-width: calc(${p.theme.inputWidth} - 25px);
482 | overflow: hidden;
483 | }
484 | `}
485 |
486 | ${p => p.labelDisplay === 'before' && `
487 | & .Select-placeholder,
488 | & .Select--single > .Select-control .Select-value {
489 | max-width: calc(100% - 25px);
490 | white-space: normal; /* since we're working with percentual width, we need the line to wrap */
491 | }
492 |
493 | & .Select--single > .Select-control .Select-value {
494 | height: calc((${p.theme.spacingUnit} / 2) + ${p.theme.inputLineHeight});
495 | overflow: hidden;
496 | }
497 | `}
498 |
499 | ${p => !p.success && `
500 | & .Select-control {
501 | border-color: ${p.theme.errorColor};
502 | background-color: ${p.theme.errorBgColor};
503 | }
504 |
505 | & .Select-menu-outer {
506 | border-bottom-color: ${p.theme.errorColor};
507 | border-left-color: ${p.theme.errorColor};
508 | border-right-color: ${p.theme.errorColor};
509 | }
510 |
511 | & .Select-placeholder {
512 | color: ${p.theme.errorColor};
513 | }
514 | `}
515 | `;
516 |
--------------------------------------------------------------------------------
/src/SubmitButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer, inject } from 'mobx-react';
4 | import Webform from '../Webform';
5 | import { template } from '../Parser';
6 | import BaseButton from '../BaseButton';
7 | import FormStore from '../Observables/Form';
8 |
9 | const SubmitButton = ({ field, formStore, status, show, loadingTimeout, loadingComponent }) => {
10 | if(field['#submit__hide'] === true || (show === false && formStore.isMultipage())) {
11 | return null;
12 | }
13 |
14 | const disabled = status === Webform.formStates.PENDING;
15 |
16 | const label = template(formStore, field['#submit__label']);
17 |
18 | if(!label || label === '') {
19 | return null;
20 | }
21 |
22 | return (
23 |
24 |
30 | {loadingTimeout && (
31 | loadingComponent
32 | )}
33 |
34 | );
35 | };
36 |
37 | SubmitButton.meta = {
38 | labelVisibility: 'invisible',
39 | };
40 |
41 | SubmitButton.propTypes = {
42 | field: PropTypes.shape().isRequired,
43 | status: PropTypes.string.isRequired,
44 | formStore: PropTypes.instanceOf(FormStore).isRequired,
45 | show: PropTypes.bool,
46 | loadingTimeout: PropTypes.bool.isRequired,
47 | loadingComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
48 | };
49 |
50 | SubmitButton.defaultProps = {
51 | type: 'button',
52 | show: false,
53 | loadingComponent: undefined,
54 | };
55 |
56 | const DecoratedSubmitButton = inject('formStore')(observer(SubmitButton));
57 |
58 | DecoratedSubmitButton.meta = SubmitButton.meta;
59 |
60 | export default DecoratedSubmitButton;
61 |
--------------------------------------------------------------------------------
/src/TextArea/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import WebformElement from '../WebformElement';
4 | // styled
5 | import StyledTextArea from './styled/text-area';
6 | import ValidationIcon from './styled/validation-icon';
7 |
8 | class TextArea extends Component {
9 | static propTypes = {
10 | field: PropTypes.shape({
11 | '#webform_key': PropTypes.string.isRequired,
12 | '#required': PropTypes.bool,
13 | }).isRequired,
14 | value: PropTypes.oneOfType([
15 | PropTypes.string,
16 | PropTypes.number,
17 | PropTypes.bool,
18 | ]),
19 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
20 | onChange: PropTypes.func.isRequired,
21 | onBlur: PropTypes.func.isRequired,
22 | state: PropTypes.shape({
23 | required: PropTypes.bool.isRequired,
24 | enabled: PropTypes.bool.isRequired,
25 | }).isRequired,
26 | };
27 |
28 | static defaultProps = {
29 | value: null,
30 | };
31 |
32 | getLabelPosition() {
33 | return this.props.webformElement.getLabelDisplay();
34 | }
35 |
36 | render() {
37 | const attrs = {
38 | 'aria-invalid': this.props.webformElement.isValid() ? null : true,
39 | 'aria-required': this.props.field['#required'] ? true : null,
40 | };
41 |
42 | return (
43 |
44 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | export default TextArea;
63 |
--------------------------------------------------------------------------------
/src/TextArea/styled/text-area.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.textarea`
4 | box-sizing: border-box;
5 | margin: 0 0 calc(${p => p.theme.spacingUnit} / 2);
6 | font-size: 0.9em;
7 | line-height: inherit;
8 | font-family: inherit;
9 | overflow: auto;
10 | border: 1px solid ${p => p.theme.borderColor};
11 | background-color: ${p => p.theme.inputBgColor};
12 | border-radius: ${p => p.theme.borderRadius};
13 | padding: calc(${p => p.theme.spacingUnit} / 4) calc(${p => p.theme.spacingUnit} / 2);
14 |
15 | ${p => p.labelDisplay === 'inline' && `
16 | width: ${p.theme.inputWidth};
17 | `}
18 |
19 | ${p => p.labelDisplay === 'before' && `
20 | width: ${p.theme.inputWidth};
21 | max-width: calc(100% - 10px);
22 | min-width: ${p.theme.inputWidth};
23 | min-height: ${p.theme.textAreaHeight};
24 | resize:vertical;
25 |
26 | @media (min-width: 768px) {
27 | width: calc(${p.theme.inlineLabelWidth} + ${p.theme.inputWidth});
28 | }
29 | `}
30 |
31 | ${p => !p.success && `
32 | border-color: ${p.theme.errorColor};
33 | background-color: ${p.theme.errorBgColor};
34 |
35 | &::placeholder {
36 | color: ${p.theme.errorColor};
37 | }
38 | `}
39 | `;
40 |
--------------------------------------------------------------------------------
/src/TextArea/styled/validation-icon.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import ValidationIcon from '../../BaseInput/styled/validation-icon';
3 |
4 | export default styled(ValidationIcon)`
5 | top: -10px;
6 | position: relative;
7 | `;
8 |
--------------------------------------------------------------------------------
/src/Webform/ThankYouMessage/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { inject } from 'mobx-react';
4 | import FormStore from '../../Observables/Form';
5 | import Parser, { template } from '../../Parser/index';
6 | // styled
7 | import Message from './styled/message';
8 |
9 | const ThankYouMessage = ({ message, formStore }) => (
10 | {Parser(template(formStore, message))}
11 | );
12 |
13 | ThankYouMessage.propTypes = {
14 | message: PropTypes.string.isRequired,
15 | formStore: PropTypes.instanceOf(FormStore).isRequired,
16 | };
17 |
18 | export default inject('formStore')(ThankYouMessage);
19 |
--------------------------------------------------------------------------------
/src/Webform/ThankYouMessage/styled/message.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.h2`
4 | border-radius: ${p => p.theme.borderRadius};
5 | padding: calc(${p => p.theme.spacingUnit} / 2) calc(${p => p.theme.spacingUnit} / 1);
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Webform/conditionals/__tests__/formatCondtionals.test.js:
--------------------------------------------------------------------------------
1 | // import { formatConditionals, checkConditionals, defaultStates, supportedActions } from './index';
2 | import { formatConditionals } from '../index';
3 |
4 | const { test, expect } = global;
5 |
6 | const oldStatesObjects = [
7 | {
8 | visible: {
9 | ':input[name="name"]': {
10 | value: true,
11 | },
12 | },
13 | },
14 | {
15 | visible: {
16 | ':input[name="name"]': {
17 | value: true,
18 | },
19 | ':input[name="last_name"]': {
20 | filled: true,
21 | },
22 | },
23 | },
24 | {
25 | visible: [
26 | {
27 | ':input[name="name"]': {
28 | value: true,
29 | },
30 | },
31 | 'or',
32 | {
33 | ':input[name="last_name"]': {
34 | filled: true,
35 | },
36 | },
37 | ],
38 | },
39 | {
40 | visible: [
41 | {
42 | ':input[name="name"]': {
43 | value: true,
44 | },
45 | },
46 | 'or',
47 | {
48 | ':input[name="last_name"]': {
49 | filled: true,
50 | },
51 | },
52 | ],
53 | required: {
54 | ':input[name="last_name"]': {
55 | value: true,
56 | },
57 | },
58 | },
59 | ];
60 |
61 | const newStatesObjects = [
62 | [
63 | {
64 | action: 'visible',
65 | logic: 'and',
66 | conditions: [
67 | {
68 | key: 'name',
69 | condition: 'value',
70 | value: true,
71 | },
72 | ],
73 | },
74 | ],
75 | [
76 | {
77 | action: 'visible',
78 | logic: 'and',
79 | conditions: [
80 | {
81 | key: 'name',
82 | condition: 'value',
83 | value: true,
84 | },
85 | {
86 | key: 'last_name',
87 | condition: 'filled',
88 | value: true,
89 | },
90 | ],
91 | },
92 | ],
93 | [
94 | {
95 | action: 'visible',
96 | logic: 'or',
97 | conditions: [
98 | {
99 | key: 'name',
100 | condition: 'value',
101 | value: true,
102 | },
103 | {
104 | key: 'last_name',
105 | condition: 'filled',
106 | value: true,
107 | },
108 | ],
109 | },
110 | ],
111 | [
112 | {
113 | action: 'visible',
114 | logic: 'or',
115 | conditions: [
116 | {
117 | key: 'name',
118 | condition: 'value',
119 | value: true,
120 | },
121 | {
122 | key: 'last_name',
123 | condition: 'filled',
124 | value: true,
125 | },
126 | ],
127 | },
128 | {
129 | action: 'required',
130 | logic: 'and',
131 | conditions: [
132 | {
133 | key: 'last_name',
134 | condition: 'value',
135 | value: true,
136 | },
137 | ],
138 | },
139 | ],
140 | ];
141 |
142 | for(let i = 0, l = oldStatesObjects.length; i < l; i += 1) {
143 | test(`Test formatting of conditionals: #${i + 1}`, () => {
144 | expect(formatConditionals(oldStatesObjects[i])).toEqual(newStatesObjects[i]);
145 | });
146 | }
147 |
--------------------------------------------------------------------------------
/src/Webform/conditionals/index.js:
--------------------------------------------------------------------------------
1 | export const supportedActions = {
2 | visible: 'visible',
3 | enabled: 'enabled',
4 | required: 'required',
5 | };
6 |
7 | export const supportedStates = {
8 | [supportedActions.visible]: supportedActions.visible,
9 | invisible: 'invisible',
10 | [supportedActions.enabled]: supportedActions.enabled,
11 | disabled: 'disabled',
12 | [supportedActions.required]: supportedActions.required,
13 | optional: 'optional',
14 | };
15 |
16 | export const supportedConditions = {
17 | empty: 'empty',
18 | filled: 'filled',
19 | checked: 'checked',
20 | unchecked: 'unchecked',
21 | value: 'value',
22 | };
23 |
24 | export const supportedLogic = {
25 | and: 'and',
26 | or: 'or',
27 | };
28 |
29 | export const support = {
30 | actions: supportedActions,
31 | states: supportedStates,
32 | conditions: supportedConditions,
33 | logic: supportedLogic,
34 | };
35 |
36 | /**
37 | * @deprecated We don't use state anymore! Use store.
38 | */
39 | export function defaultStates(field) {
40 | return {
41 | [supportedActions.visible]: true,
42 | [supportedActions.required]: field['#required'] || false,
43 | [supportedActions.enabled]: true,
44 | };
45 | }
46 |
47 | /**
48 | * Method to format a states object from Drupal, into a more JavaScript friendly format.
49 | * @param {object} states States object from Drupal field.
50 | * @returns {boolean|Array} Returns array with formatted conditionals or false if no conditionals found.
51 | *
52 | * Example
53 | * Old:
54 | * {
55 | * visible: [
56 | * {
57 | * ':input[name="name"]': {
58 | * empty: true
59 | * }
60 | * },
61 | * {
62 | * ':input[name="name"]': {
63 | * filled: true
64 | * }
65 | * }
66 | * ]
67 | * }
68 | * New:
69 | * [
70 | * {
71 | * action: 'visible',
72 | * logic: 'and',
73 | * conditions: [
74 | * {
75 | * key: 'name',
76 | * condition: 'empty',
77 | * value: true
78 | * },
79 | * {
80 | * key: 'name',
81 | * condition: 'visible',
82 | * value: true
83 | * }
84 | * ]
85 | * }
86 | * ]
87 | */
88 | export function formatConditionals(states = false) {
89 | if(!states) {
90 | return false;
91 | }
92 |
93 | const mappedStates = Object.keys(states).map((stateKey) => { // stateKey, e.g. visible.
94 | if(!supportedStates[stateKey]) {
95 | return false; // Don't format conditional if state isn't supported.
96 | }
97 |
98 | const conditionalKeys = Object.keys(states[stateKey]);
99 | const conditionLogic = states[stateKey][conditionalKeys[1]] === supportedLogic.or ? supportedLogic.or : supportedLogic.and; // Default conditional logic is and.
100 |
101 | const mappedConditions = conditionalKeys.map((conditionalKey2) => { // e.g. ':input[name="name"]'.
102 | let conditionalKey = conditionalKey2;
103 | let conditionObject = states[stateKey][conditionalKey];
104 |
105 | if(conditionLogic === supportedLogic.or) { // If conditional logic is set to 'or'.
106 | if(conditionObject === supportedLogic.or) {
107 | return false; // Don't format the 'or' item.
108 | }
109 |
110 | conditionalKey = Object.keys(conditionObject)[0];
111 | conditionObject = conditionObject[Object.keys(conditionObject)[0]]; // Remove one level of nesting when logic is set to 'or'.
112 | }
113 |
114 | const dependencyKeyRegex = conditionalKey.match(/name="(\S+)"/);
115 | if(!dependencyKeyRegex) {
116 | return false;
117 | }
118 | const formattedDependencyKey = dependencyKeyRegex[1]; // Field key of dependency, e.g. 'name' in above example.
119 | const condition = Object.keys(conditionObject)[0]; // e.g. empty.
120 | const conditionValue = conditionObject[condition]; // e.g. true.
121 |
122 | if(!supportedConditions[condition]) {
123 | return false; // Don't format conditional if condition isn't supported.
124 | }
125 |
126 | return {
127 | key: formattedDependencyKey,
128 | condition,
129 | value: conditionValue,
130 | };
131 | });
132 |
133 | const filteredConditions = mappedConditions.filter(c => c); // Filter out all 'false' values.
134 |
135 | if(!filteredConditions.length) {
136 | return false; // Return false when there are no valid conditions found.
137 | }
138 |
139 | return {
140 | action: stateKey,
141 | logic: conditionLogic,
142 | conditions: filteredConditions,
143 | };
144 | });
145 |
146 | const filteredStates = mappedStates.filter(s => s); // Filter out all 'false' values.
147 |
148 | return filteredStates.length ? filteredStates : false; // Return false when there are no valid states found.
149 | }
150 |
151 | function formatNewStates(newStates) {
152 | const formattedStates = {};
153 |
154 | Object.keys(newStates).forEach((stateKey) => {
155 | const condition = newStates[stateKey];
156 | switch(stateKey) {
157 | case supportedStates.visible:
158 | case supportedStates.enabled:
159 | case supportedStates.required:
160 | formattedStates[stateKey] = condition;
161 | break;
162 | case supportedStates.invisible:
163 | formattedStates[supportedActions.visible] = !condition;
164 | break;
165 | case supportedStates.disabled:
166 | formattedStates[supportedActions.enabled] = !condition;
167 | break;
168 | case supportedStates.optional:
169 | formattedStates[supportedActions.required] = !condition;
170 | break;
171 | default:
172 | break;
173 | }
174 | });
175 |
176 | return formattedStates;
177 | }
178 |
179 | export function checkConditionals(formStore, fieldKey) {
180 | if(!fieldKey) {
181 | console.error('Conditionals were checked without a fieldKey!');
182 | return {};
183 | }
184 |
185 | const field = formStore.getField(fieldKey);
186 |
187 | if(!field || !field.conditionals) {
188 | // When the form is still being built, not all fields are available.
189 | return {};
190 | }
191 |
192 | const newStates = {};
193 |
194 | /**
195 | * conditional example:
196 | * {
197 | * action: 'visible',
198 | * logic: 'and',
199 | * conditions: [condition, condition]
200 | * }
201 | */
202 | // Reduce conditionals to true or false value, based on conditionals and logic.
203 | field.conditionals.forEach((conditional) => {
204 | /**
205 | * condition example:
206 | * {
207 | * key: 'name',
208 | * condition: 'empty',
209 | * value: true
210 | * }
211 | */
212 | // Go through conditions per conditional.
213 | newStates[conditional.action] = conditional.conditions.reduce((prevOutcome, condition) => {
214 | let conditionalOutcome = false;
215 |
216 | const dependency = formStore.getField(condition.key);
217 | if(!dependency) {
218 | console.error(`Cant find dependency ${condition.key} in the conditionals of ${fieldKey}`);
219 | return false;
220 | }
221 | const dependencyValue = dependency.value;
222 |
223 | // See what the action of the condition should be.
224 | switch(condition.condition) {
225 | case supportedConditions.empty:
226 | case supportedConditions.filled: {
227 | const isEmpty = dependency.isEmpty;
228 | const check = condition.condition === supportedConditions.empty ? isEmpty : !isEmpty;
229 | conditionalOutcome = check;
230 | break;
231 | }
232 | case supportedConditions.checked:
233 | case supportedConditions.unchecked: {
234 | // When dependencyValue is true, then it is checked.
235 | const check = condition.condition === supportedConditions.checked;
236 | conditionalOutcome = check === (dependencyValue === true || dependencyValue === '1' || dependencyValue === 'true');
237 | break;
238 | }
239 | case supportedConditions.value: {
240 | // Check if value matches condition.
241 | conditionalOutcome = dependencyValue === condition.value;
242 | break;
243 | }
244 | default: {
245 | break;
246 | }
247 | }
248 |
249 | if(conditional.logic === supportedLogic.and) {
250 | return prevOutcome && conditionalOutcome;
251 | } else if(conditional.logic === supportedLogic.or) {
252 | return prevOutcome || conditionalOutcome;
253 | }
254 | return true;
255 | }, conditional.logic === supportedLogic.and);
256 | });
257 |
258 | return formatNewStates(newStates);
259 | }
260 |
--------------------------------------------------------------------------------
/src/Webform/index.jsx:
--------------------------------------------------------------------------------
1 | import fetch from 'fetch-everywhere';
2 | import getNested from 'get-nested';
3 | import GoogleTag from 'google_tag';
4 | import { observer, Provider } from 'mobx-react';
5 | import PropTypes from 'prop-types';
6 | import React, { Component } from 'react';
7 | import { ThemeProvider } from 'styled-components';
8 | import ReCAPTCHA from 'react-google-recaptcha';
9 | import Forms from '../Observables/Forms';
10 | import Parser from '../Parser';
11 | import ThankYouMessage from './ThankYouMessage';
12 | import WebformElement from '../WebformElement';
13 | import theme from '../styles/theme';
14 | // styled
15 | import StyledWebform from './styled/webform';
16 | import FormTitle from './styled/form-title';
17 | import List from './styled/list';
18 | import ListItem from './styled/list-item';
19 | import Element from './styled/element';
20 |
21 | @observer
22 | class Webform extends Component {
23 | static formStates = {
24 | DEFAULT: 'DEFAULT',
25 | SENT: 'SENT',
26 | ERROR: 'ERROR',
27 | PENDING: 'PENDING',
28 | CONVERTED: 'CONVERTED',
29 | };
30 |
31 | static analyticsEventsCategories = {
32 | SUCCESSFUL: 'Successful submission',
33 | ERROR: 'Error during submission',
34 | };
35 |
36 | static analyticsEventsActions = {
37 | FORM_SUBMISSION: 'Form Submission',
38 | };
39 |
40 | static propTypes = {
41 | url: PropTypes.string.isRequired,
42 | settings: PropTypes.shape({
43 | title: PropTypes.string,
44 | tracking: PropTypes.oneOfType([
45 | PropTypes.shape({
46 | gtm_id: PropTypes.oneOfType([
47 | PropTypes.string,
48 | PropTypes.bool,
49 | ]),
50 | }),
51 | PropTypes.bool,
52 | ]),
53 | langcode: PropTypes.string,
54 | }).isRequired,
55 | form: PropTypes.shape({
56 | form_id: PropTypes.string.isRequired,
57 | settings: PropTypes.shape({
58 | nm_gtm_id: PropTypes.string,
59 | nm_required_hint: PropTypes.string,
60 | confirmation_message: PropTypes.string,
61 | }),
62 | elements: PropTypes.arrayOf(PropTypes.shape({
63 | '#type': PropTypes.string.isRequired,
64 | })).isRequired,
65 | token: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
66 | langcode: PropTypes.string,
67 | }).isRequired,
68 | onSubmit: PropTypes.func,
69 | onSubmitSuccess: PropTypes.func,
70 | onSubmitFail: PropTypes.func,
71 | defaultValues: PropTypes.objectOf(PropTypes.string),
72 | hiddenData: PropTypes.objectOf(PropTypes.string),
73 | noValidation: PropTypes.bool,
74 | showThankYouMessage: PropTypes.bool,
75 | theme: PropTypes.shape(),
76 | loadingTimeout: PropTypes.number,
77 | loadingComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
78 | };
79 |
80 | static defaultProps = {
81 | onSubmit: () => {},
82 | onSubmitSuccess: () => {},
83 | onSubmitFail: () => {},
84 | nm_gtm_id: false,
85 | settings: {
86 | tracking: false,
87 | },
88 | defaultValues: {},
89 | hiddenData: {},
90 | noValidation: true,
91 | showThankYouMessage: true,
92 | theme: {},
93 | loadingTimeout: 500,
94 | loadingComponent: undefined,
95 | };
96 |
97 | constructor(props) {
98 | super(props);
99 |
100 | this.state = {
101 | status: Webform.formStates.DEFAULT,
102 | errors: {},
103 | loadingTimeout: false,
104 | };
105 |
106 | this.key = props.form.form_id;
107 |
108 | this.onSubmit = this.onSubmit.bind(this);
109 | this.submit = this.submit.bind(this);
110 |
111 | this.formStore = this.getFormstore(props);
112 | }
113 |
114 | componentDidMount() {
115 | const GTM = getNested(() => this.props.settings.tracking.gtm_id) || this.props.form.settings.nm_gtm_id;
116 |
117 | if(GTM) {
118 | GoogleTag.addTag(GTM);
119 | }
120 | }
121 |
122 | componentWillReceiveProps(nextProps) {
123 | this.formStore = this.getFormstore(nextProps);
124 | }
125 |
126 | componentWillUnmount() {
127 | clearTimeout(this.loadingTimeout);
128 | }
129 |
130 | onSubmit(e) {
131 | e.preventDefault();
132 |
133 | // If the 'onSubmit' is being overwritten, use that function.
134 | // If it returns false, don't submit, otherwise continue.
135 | if(typeof this.onSubmitOverwrite === 'function') {
136 | const result = this.onSubmitOverwrite(e);
137 | if(!result) {
138 | return result;
139 | }
140 | }
141 |
142 | // Make sure that all errors are visible by marking all visible fields as blurred.
143 | this.formStore.visibleFields.forEach((field) => {
144 | field.isBlurred = true;
145 | });
146 |
147 | const isValid = this.isValid();
148 | if(isValid) {
149 | if(this.getCaptchaField() && this.recaptchaRef) {
150 | return this.recaptchaRef.execute();
151 | }
152 | return this.updateSubmission();
153 | }
154 | console.warn('The user tried to submit a form, but not all fields are valid.');
155 |
156 | return true;
157 | }
158 |
159 | getCaptchaField() {
160 | return this.formStore.form.elements.find(field => field['#type'] === 'captcha');
161 | }
162 |
163 | getFormstore(props) {
164 | return Forms.getForm(props.form.form_id, {
165 | form: props.form,
166 | settings: props.settings,
167 | defaultValues: this.props.defaultValues,
168 | });
169 | }
170 |
171 | getFormElements() {
172 | const formElements = getNested(() => this.props.form.elements, []);
173 | return formElements.map(field => (
174 |
186 | ));
187 | }
188 |
189 | isValid() {
190 | return this.formStore.valid;
191 | }
192 |
193 | resetForm() {
194 | Forms.deleteForm(this.props.form.form_id);
195 | this.formStore = this.getFormstore(this.props);
196 | }
197 |
198 | async updateSubmission() {
199 | if(this.props.loadingComponent) {
200 | this.loadingTimeout = setTimeout(() => {
201 | this.setState({ loadingTimeout: true });
202 | }, this.props.loadingTimeout);
203 | }
204 |
205 | let response = await this.props.onSubmit(this); // Trigger onSubmit hook and store response.
206 | if(!response || response.submit !== false) { // If onSubmit hook response is false, don't trigger default submit.
207 | response = await this.submit();
208 | }
209 |
210 | clearTimeout(this.loadingTimeout);
211 | this.setState({ loadingTimeout: false });
212 |
213 | if(response.status === 200 || response.status === 201) {
214 | this.response = response;
215 | this.setState({ status: Webform.formStates.SENT });
216 | this.props.onSubmitSuccess({
217 | webform: this,
218 | response: this.response,
219 | values: this.formStore.values,
220 | }); // Trigger onSubmitSuccess hook.
221 | this.resetForm();
222 | } else {
223 | this.setState({
224 | status: Webform.formStates.ERROR,
225 | errors: response.message || [],
226 | });
227 | this.props.onSubmitFail(this); // Trigger onSubmitFail hook.
228 | }
229 | }
230 |
231 | async submit(extraFields = {}) {
232 | // eslint-disable-next-line no-undef
233 | const headers = new Headers({
234 | 'Content-Type': 'application/json',
235 | 'X-CSRF-Token': this.props.form.token,
236 | });
237 |
238 | const values = Object.assign({}, this.props.hiddenData, this.formStore.values);
239 | if(!extraFields.in_draft) {
240 | this.setState({ status: Webform.formStates.PENDING });
241 | }
242 |
243 | try {
244 | const response = await fetch(`${this.props.url}/hn-webform-submission?_format=json`, {
245 | headers,
246 | method: 'POST',
247 | body: JSON.stringify(Object.assign({
248 | form_id: this.props.form.form_id,
249 | }, extraFields, values)),
250 | });
251 | return response.json();
252 | } catch(err) {
253 | console.error(err);
254 | return null;
255 | }
256 | }
257 |
258 | recaptchaRef = null;
259 |
260 | render() {
261 | if(!this.formStore) return null;
262 | const formElements = this.getFormElements();
263 |
264 | let requiredHint = null;
265 | if(
266 | this.formStore.visibleFields.find(field => field.required) &&
267 | this.props.form.settings.nm_required_hint
268 | ) {
269 | requiredHint = {Parser(this.props.form.settings.nm_required_hint)};
270 | }
271 | const errors = Object.entries(this.state.errors);
272 | const errorsEl = errors.length > 0 && (
273 |
274 | {errors.map(([key, error]) => (
275 |
276 | {key.replace(/]\[/g, '.')}: {Parser(error)}
277 |
278 | ))}
279 |
280 | );
281 |
282 | const renderCaptcha = () => {
283 | const field = this.getCaptchaField();
284 |
285 | if(!field) {
286 | return null;
287 | }
288 |
289 | if(!field['#captcha_sitekey']) {
290 | console.warn('No google reCaptcha sitekey found.');
291 | return null;
292 | }
293 |
294 | return (
295 |
296 | { this.recaptchaRef = ref; }}
300 | onChange={() => this.updateSubmission()}
301 | hl={this.props.settings.langcode || this.props.form.langcode || 'en'}
302 | />
303 |
304 | );
305 | };
306 |
307 | return (
308 |
309 |
310 |
311 | {this.props.settings.title && (
312 | {this.props.settings.title}
313 | )}
314 | {this.state.status !== Webform.formStates.SENT && errorsEl}
315 | {this.state.status !== Webform.formStates.SENT && (
316 |
327 | )}
328 | {this.props.showThankYouMessage && this.state.status === Webform.formStates.SENT && (
329 |
330 | )}
331 |
332 |
333 |
334 | );
335 | }
336 | }
337 |
338 | export default Webform;
339 |
--------------------------------------------------------------------------------
/src/Webform/rules.js:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 |
3 | // TODO: Remove the general 'rules' and make everything locally accessable in the store.
4 | export default observable.map({});
5 |
--------------------------------------------------------------------------------
/src/Webform/styled/element.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | font-size: 10px;
5 |
6 | ${p => p.error && `
7 | font-size: 16px;
8 | color: #fff;
9 | `}
10 | `;
11 |
--------------------------------------------------------------------------------
/src/Webform/styled/form-title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.h2`
4 | margin: 0 0 calc(${p => p.theme.spacingUnit} / 2) 0;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/Webform/styled/list-item.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.li`
4 | list-style: none outside none;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/Webform/styled/list.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.ul`
4 | padding-left: 0;
5 | margin-bottom: 2em;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Webform/styled/stdcss.0.0.7.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions */
2 | import { injectGlobal } from 'styled-components';
3 |
4 | injectGlobal`
5 | ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,q,fieldset,dl,dt,dd,iframe,table,tbody,thead,td,th,address,legend{margin:0;padding:0;font-size:1em;font-style:inherit;font-family:inherit;vertical-align:baseline}article,aside,canvas,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}*{-webkit-tap-highlight-color:rgba(0,0,0,0)}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{font-size:12px;line-height:1.5em;font-family:Arial,Helvetica,sans-serif}h1{font-size:1.5em;line-height:1em;margin:2em 0 1em}h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child{margin-top:0}* html h1,* html h2{margin-top:0}h2{font-size:1.3333em;line-height:1.125em;margin-top:1.6875em;margin-bottom:.5625em}h3{font-size:1.1667em;line-height:1.286em;margin-top:1.929em;margin-bottom:.643em}h2+h3{margin-top:1.2857em}h4{margin-top:1.5em}h3+h4{margin-top:.5em}h5{margin-top:1.5em;font-weight:400}h4+h5{margin-top:0}p{font-size:1em;line-height:1.5em;margin:0 0 1.5em}p.intro{font-size:1.08333em;line-height:1.3846em;font-weight:700}blockquote{margin:0 2.5em 1.5em;font-style:oblique}q{font-style:oblique}hr{margin:0 0 1.5em;height:1px;background:#333;border:0}small{font-size:.8333em;line-height:1.8em}a img,:link img,:visited img{border:0}a{background:transparent;outline:0}address{font-style:normal}figure{margin:0}ul,ol{margin:0 0 1.5em 2.5em}li ul,li ol{margin:0 0 0 2.5em}dl{margin:0 0 1.5em}dt{font-weight:700;margin:1.5em 0 0}dd{margin:0 0 0 2.5em}.break{clear:both}.right{float:right}.left{float:left}.hide,.skip{display:none}.center{text-align:center}fieldset{border:0}form br{clear:left}label{float:left;width:150px;margin:0 0 .9em}label.inline{width:auto;display:inline;margin-right:15px}input,input.text,textarea,select{font-family:inherit;font-size:100%;width:300px;margin:0 0 .9em}textarea{overflow:auto}label input{width:auto;height:auto;margin:0 5px 0 0;padding:0;vertical-align:middle;border:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input.inline,select.inline{width:150px}input.small{width:30px}input.medium{width:60px}label,button{cursor:pointer}button,input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button,input{line-height:normal}button[disabled],input[disabled]{cursor:default}input::-moz-focus-inner,button::-moz-focus-inner{border:0;padding:0}.feedback-error,.feedback-warning,.feedback-confirm,.feedback-notice{margin:0 0 1.5em;padding:1.4255em 10px 1.4255em 50px;list-style:none;color:#000;background-repeat:no-repeat;background-position:3% 50%;border:1px solid #d8000c;background-color:#ffbaba}.feedback-warning{border:1px solid #FFD700;background-color:#feffca}.feedback-confirm{border:1px solid #3e983b;background-color:#e7f9e8}.feedback-notice{border:1px solid #00529b;background-color:#bde5f8}
6 | `;
7 |
--------------------------------------------------------------------------------
/src/Webform/styled/webform.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | font-family: ${p => p.theme.primaryFont};
5 | max-width: ${p => p.theme.formMaxWidth};
6 | color: ${p => p.theme.baseColor};
7 | margin: ${p => p.theme.formMargin};
8 | `;
9 |
--------------------------------------------------------------------------------
/src/WebformElement/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import getNested from 'get-nested';
4 | import { observer } from 'mobx-react';
5 | import Parser, { template } from '../Parser';
6 | import FormStore from '../Observables/Form';
7 | import Hidden from '../Hidden';
8 | // styled
9 | import RequiredMarker from './styled/required-marker';
10 | import ValidationMessage from './styled/validation-message';
11 | import TextContent from './styled/text-content';
12 | import Label from './styled/label';
13 | import FormRow from './styled/form-row';
14 |
15 | @observer
16 | class WebformElement extends Component {
17 | static propTypes = {
18 | field: PropTypes.shape({
19 | '#type': PropTypes.string.isRequired,
20 | '#default_value': PropTypes.oneOfType([
21 | PropTypes.string,
22 | PropTypes.objectOf(PropTypes.string),
23 | ]),
24 | '#webform_key': PropTypes.string.isRequired,
25 | '#required': PropTypes.bool,
26 | '#pattern': PropTypes.oneOfType([
27 | PropTypes.string,
28 | PropTypes.instanceOf(RegExp),
29 | ]),
30 | '#requiredError': PropTypes.string,
31 | '#patternError': PropTypes.string,
32 | '#emailError': PropTypes.string,
33 | '#title': PropTypes.string,
34 | '#title_display': PropTypes.string,
35 | '#options_display': PropTypes.string,
36 | '#admin': PropTypes.bool,
37 | composite_elements: PropTypes.array,
38 | }).isRequired,
39 | formStore: PropTypes.instanceOf(FormStore).isRequired,
40 | onChange: PropTypes.func,
41 | onBlur: PropTypes.func,
42 | settings: PropTypes.shape({
43 | custom_elements: PropTypes.shape({
44 | patternError: PropTypes.shape({
45 | '#default_value': PropTypes.string,
46 | '#options': PropTypes.objectOf(PropTypes.string),
47 | }),
48 | }),
49 | }).isRequired,
50 | webformSettings: PropTypes.shape().isRequired,
51 | webformPage: PropTypes.string,
52 | form: PropTypes.shape({
53 | settings: PropTypes.object.isRequired,
54 | }).isRequired,
55 | status: PropTypes.string.isRequired,
56 | url: PropTypes.string.isRequired,
57 | };
58 |
59 | static defaultProps = {
60 | label: false,
61 | parent: false,
62 | onChange: () => {
63 | },
64 | onBlur: () => {
65 | },
66 | webformPage: 'none',
67 | };
68 |
69 | constructor(props) {
70 | super(props);
71 |
72 | this.key = props.field['#webform_key'];
73 |
74 | this.onChange = this.onChange.bind(this);
75 | this.onBlur = this.onBlur.bind(this);
76 |
77 | /**
78 | * @deprecated Don't use state! Use the store.
79 | */
80 | this.state = {
81 | // errors: [],
82 | };
83 | /**
84 | * @deprecated
85 | */
86 | this.setState = this.setState;
87 |
88 | // Object.assign(this.state, defaultStates(props.field));
89 | }
90 |
91 | onChange(e) {
92 | const field = this.getField();
93 |
94 | // First, check if this field has a value (e.g. fieldsets don't)
95 | const meta = field.componentClass.meta || {};
96 | const hasValue = meta.hasValue;
97 | if(typeof hasValue === 'undefined' || hasValue) {
98 | // Get the value
99 | const value = (e && e.target) ? e.target.value : e; // Check if 'e' is event, or direct value
100 |
101 | field.value = value;
102 | }
103 |
104 | this.props.onChange(e);
105 |
106 | return true;
107 | }
108 |
109 | onBlur(e) {
110 | const field = this.getField();
111 |
112 | // First, check if this field has a value (e.g. fieldsets don't)
113 | const meta = field.componentClass.meta || {};
114 | const hasValue = meta.hasValue;
115 | if(typeof hasValue === 'undefined' || hasValue) {
116 | // Update the field storage to set it to blurred.
117 | field.isBlurred = true;
118 | }
119 |
120 | this.props.onBlur(e);
121 | }
122 |
123 | /**
124 | * @returns {Field}
125 | */
126 | getField(key = this.key) {
127 | return this.props.formStore.getField(key);
128 | }
129 |
130 | getFormElement() {
131 | if(!this.shouldRender()) {
132 | return false;
133 | }
134 |
135 | const ElementComponent = getNested(() => this.getField().componentClass);
136 | if(ElementComponent) {
137 | return {
138 | class: ElementComponent,
139 | element: (
140 |
150 | ),
151 | };
152 | }
153 | return false;
154 | }
155 |
156 | getValue(key = this.key) {
157 | const field = this.getField(key);
158 | if(!field) {
159 | return false;
160 | }
161 | return field.getValue();
162 | }
163 |
164 | getLabelDisplay() {
165 | const elementClass = this.getField().componentClass;
166 | return getNested(() => elementClass.meta.labelVisibility) || this.props.field['#title_display'] || 'inline';
167 | }
168 |
169 | /**
170 | * @deprecated
171 | * Just use this.getField().valid
172 | *
173 | * @param key
174 | * @returns {boolean}
175 | */
176 | isValid(key = this.key) {
177 | const field = this.getField(key);
178 | if(!field) {
179 | return true;
180 | }
181 | return field.valid;
182 | }
183 |
184 | /**
185 | * @deprecated
186 | * Just use this.getField().isBlurred
187 | */
188 | shouldValidate(force = false) {
189 | if(force) {
190 | return true;
191 | }
192 | const field = this.getField();
193 | if(!field) {
194 | return false;
195 | }
196 | return field.isBlurred;
197 | }
198 |
199 | shouldRender() {
200 | // If it is an admin only field, don't render.
201 | if(this.props.field['#admin']) {
202 | return false;
203 | }
204 |
205 | // Check if it is an composite element
206 | const children = (this.props.field.composite_elements || []).filter(e => !e['#admin']);
207 |
208 | // If it is, check if there is any child that's visible or of type Hidden.
209 | if(children.length && !children
210 | .map(c => this.getField(c['#webform_key']))
211 | .find(f => f && f.visible && f.componentClass !== Hidden)
212 | ) {
213 | // If there isn't any, don't render.
214 | return false;
215 | }
216 |
217 | // If all tests are passed, render.
218 | return true;
219 | }
220 |
221 | isSuccess() {
222 | return this.getField().isBlurred && this.isValid();
223 | }
224 |
225 | renderTextContent(selector, checkValue = false, addClass = '', show = true) {
226 | if(show) {
227 | const field = this.getField().componentClass;
228 | const value = this.props.field[getNested(() => field.meta.field_display[selector], selector)]; // Value in #description field
229 | const displayValue = this.props.field[`${selector}_display`];
230 | const cssClass = `${selector.replace(/#/g, '').replace(/_/g, '-')}${checkValue ? `-${checkValue}` : ''}`; // '#field_suffix' and 'suffix' become .field--suffix-suffix
231 |
232 | if(!value || (!!checkValue && checkValue !== displayValue)) {
233 | if(!(!displayValue && checkValue === 'isUndefined')) {
234 | return false;
235 | }
236 | }
237 |
238 | if(!value) {
239 | // don't output if there's no value
240 | return false;
241 | }
242 |
243 | return (
244 |
250 | {Parser(template(this.props.formStore, value))}
251 |
252 | );
253 | }
254 |
255 | return false;
256 | }
257 |
258 | renderFieldLabel(element, show = true) {
259 | /* If the label is a legend, it is supposed to be the first child of the fieldset wrapper. */
260 |
261 | if(this.props.field['#title'] && show) {
262 | const Wrapper = getNested(() => element.class.meta.label) || Label;
263 | return (
264 |
265 | {Parser(template(this.props.formStore, this.props.field['#title']))}
266 | {this.getField().required ? ( *) : null}
267 |
268 | );
269 | }
270 |
271 | return null;
272 | }
273 |
274 | render() {
275 | if(!this.shouldRender()) {
276 | return null;
277 | }
278 |
279 | const element = this.getFormElement();
280 | if(!element) return null;
281 |
282 | const errors = this.getField().errors.filter(error => error);
283 | const errorList = errors.length > 0 ? (
284 | {errors}
285 | ) : null;
286 |
287 | const Wrapper = getNested(() => element.class.meta.wrapper) || FormRow;
288 |
289 | return (
290 | element.class.meta.wrapperProps)}>
291 | {this.renderFieldLabel(element, getNested(() => element.class.meta.label.type) === 'legend')}
292 |
293 | {this.renderTextContent('#description', 'before') }
294 | {this.renderTextContent('#description', 'isUndefined', 'description-before', getNested(() => element.class.meta.label.type) === 'legend')}
295 |
296 | {this.renderFieldLabel(element, getNested(() => element.class.meta.label.type) !== 'legend')}
297 |
298 | {element.element}
299 |
300 | {this.renderTextContent('#description', 'after', this.getLabelDisplay())}
301 | {this.renderTextContent('#description', 'isUndefined', 'description-after', getNested(() => element.class.meta.label.type) !== 'legend')}
302 |
303 | {errorList}
304 |
305 | );
306 | }
307 | }
308 |
309 | export default WebformElement;
310 |
--------------------------------------------------------------------------------
/src/WebformElement/styled/form-row.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | margin-bottom: ${p => p.theme.spacingUnit};
5 |
6 | &::after {
7 | content: '';
8 | display: block;
9 | clear: both;
10 | }
11 |
12 | ${p => p.hidden && `
13 | display: none;
14 | `}
15 | `;
16 |
--------------------------------------------------------------------------------
/src/WebformElement/styled/label.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.label`
4 | display: inline-block;
5 | width: 100%;
6 | padding-left: 0; /* legend */
7 | margin-bottom: calc(${p => p.theme.spacingUnit} / 2);
8 | vertical-align: top;
9 | line-height: ${p => p.theme.inputHeight};
10 |
11 | ${p => p.labelDisplay === 'inline' && `
12 | @media (min-width: 768px) {
13 | float: left;
14 | width: ${p.theme.inlineLabelWidth};
15 | padding-right: calc(${p.theme.spacingUnit} / 2);
16 | }
17 | `}
18 |
19 | ${p => p.labelDisplay === 'invisible' && `
20 | display: none;
21 | `}
22 |
23 | ${p => p.labelDisplay === 'before' && `
24 | display: block;
25 | float: none;
26 | width: 100%;
27 | `}
28 | `;
29 |
--------------------------------------------------------------------------------
/src/WebformElement/styled/required-marker.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.small`
4 | color: ${p => p.theme.requiredColor};
5 | font-size: 0.8em;
6 | vertical-align: top;
7 | `;
8 |
--------------------------------------------------------------------------------
/src/WebformElement/styled/text-content.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | ${p => (p.class === 'description-before' || p.class === 'description-after') && `
5 | display: block;
6 | `}
7 |
8 | ${p => p.class === 'description-before' && `
9 | margin-bottom: calc(${p.theme.spacingUnit} / 2);
10 | line-height: 1.5;
11 | `}
12 |
13 | ${p => p.class === 'description-after' && `
14 | font-size: 0.8em;
15 | margin-top: 0;
16 | padding-bottom: calc(${p.theme.spacingUnit} / 2);
17 | line-height: 1.2em;
18 | clear: both;
19 |
20 | ${p.labelDisplay === 'inline' && `
21 | @media (min-width: 768px) {
22 | padding-left: calc(${p.theme.spacingUnit} / 2);
23 | margin-left: ${p.theme.inlineLabelWidth};
24 | float: left;
25 | }
26 | `}
27 | `}
28 |
29 | /* Prefix & Suffix */
30 | ${p => (p.value === 'field-prefix' || p.value === 'field-suffix') && `
31 | display: inline-block;
32 | `}
33 |
34 | ${p => p.value === 'field-prefix' && `
35 | margin-right: 5px;
36 | `}
37 |
38 | ${p => p.value === 'field-suffix' && `
39 | margin-left: 5px;
40 | `}
41 | `;
42 |
--------------------------------------------------------------------------------
/src/WebformElement/styled/validation-message.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.ul`
4 | margin: 0;
5 | padding: 0;
6 |
7 | font-size: 0.8em;
8 | margin-top: 0;
9 | padding-bottom: calc(${p => p.theme.spacingUnit} / 2);
10 | line-height: 1.2em;
11 | clear: both;
12 |
13 | ${p => p.labelDisplay === 'inline' && `
14 | @media (min-width: 768px) {
15 | padding-left: calc(${p.theme.spacingUnit} / 2);
16 | margin-left: ${p.theme.inlineLabelWidth};
17 | float: left;
18 | }
19 | `}
20 | `;
21 |
--------------------------------------------------------------------------------
/src/WebformUtils.js:
--------------------------------------------------------------------------------
1 | import getNested from 'get-nested';
2 |
3 | class WebformUtils {
4 |
5 | static getCustomValue(field, key, settings) {
6 | if(key.startsWith('#')) {
7 | throw new Error('Please use the field without leading hash.');
8 | }
9 |
10 | if(field[`#override_${key}`]) {
11 | return field[`#${key}`];
12 | }
13 |
14 | return getNested(() => settings.custom_elements[key]['#default_value'], null);
15 | }
16 |
17 | static getErrorMessage(field, key) {
18 | const errorMessage = getNested(() => field[key], null);
19 | return errorMessage && errorMessage !== '' ? errorMessage : null;
20 | }
21 |
22 | static validateRule(rule, field, force = false) {
23 | if(!rule) return true;
24 | else if(force || !rule.shouldValidate || rule.shouldValidate(field)) {
25 | return rule.rule(field.getValue());
26 | }
27 | return true;
28 | }
29 |
30 | /**
31 | * @deprecated
32 | * @param field
33 | * @param value
34 | * @returns {boolean}
35 | */
36 | static isEmpty(field, value) {
37 | if(value === '' || value === false) {
38 | return true;
39 | }
40 |
41 | if(field && field['#mask']) {
42 | const mask = field['#mask'].replace(/9|a|A/g, '_');
43 | return value === mask;
44 | }
45 |
46 | return false;
47 | }
48 |
49 | }
50 |
51 | export default WebformUtils;
52 |
--------------------------------------------------------------------------------
/src/Wizard/WizardProgress/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | // styled
4 | import Bar from './styled/bar';
5 | import Step from './styled/step';
6 | import StepNumber from './styled/step-number';
7 | import StepTitle from './styled/step-title';
8 |
9 | function WizardProgress({ pages, currentPage }) {
10 | return (
11 |
12 | {pages.map((page, i) => (
13 |
20 | {i + 1}
21 | {page['#title']}
22 |
23 | ))}
24 |
25 | );
26 | }
27 |
28 | WizardProgress.propTypes = {
29 | pages: PropTypes.arrayOf(
30 | PropTypes.shape({
31 | '#title': PropTypes.string.isRequired,
32 | '#webform_key': PropTypes.string.isRequired,
33 | }),
34 | ).isRequired,
35 | currentPage: PropTypes.number.isRequired,
36 | };
37 |
38 | export default WizardProgress;
39 |
--------------------------------------------------------------------------------
/src/Wizard/WizardProgress/styled/bar.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.ul`
4 | display: flex;
5 | margin: 0 0 calc(2 * ${p => p.theme.spacingUnit}) 0;
6 | padding: 0;
7 | width: calc(100% - 15px);
8 | line-height: 1em;
9 | `;
10 |
--------------------------------------------------------------------------------
/src/Wizard/WizardProgress/styled/step-number.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | margin-right: 5px;
5 | line-height: 1em;
6 |
7 | @media (min-width: 768px) {
8 | display: none;
9 | }
10 | `;
11 |
--------------------------------------------------------------------------------
/src/Wizard/WizardProgress/styled/step-title.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.span`
4 | display: none;
5 |
6 | @media (min-width: 768px) {
7 | line-height: 1em;
8 | display: block;
9 | }
10 | `;
11 |
--------------------------------------------------------------------------------
/src/Wizard/WizardProgress/styled/step.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.li`
4 | display: inline-block;
5 | padding: 15px 30px;
6 | position: relative;
7 | margin-right: 5px;
8 | background: #f4f4f4;
9 | z-index: ${p => 11 - p.step};
10 | margin-left: -5px;
11 |
12 | &:first-child {
13 | margin-left: 0;
14 | }
15 |
16 | &::before {
17 | position: absolute;
18 | content: '';
19 | top: -1px;
20 | left: 0;
21 | border-left: 20px solid #fff;
22 | border-bottom: 23.5px solid transparent;
23 | border-top: 23.5px solid transparent;
24 | clear: both;
25 | }
26 |
27 | &::after {
28 | position: absolute;
29 | content: '';
30 | top: -1px;
31 | right: -19px;
32 | border-left: 20px solid #f4f4f4;
33 | border-bottom: 23.5px solid transparent;
34 | border-top: 23.5px solid transparent;
35 | clear: both;
36 | }
37 |
38 | ${p => p.step === 0 && `
39 | &::before {
40 | display: none;
41 | }
42 | `}
43 |
44 | ${p => (p.done || p.active) && `
45 | background: ${p.theme.primaryColor};
46 | color: #fff;
47 |
48 | &::after {
49 | border-left: 20px solid ${p.theme.primaryColor};
50 | }
51 | `}
52 | `;
53 |
--------------------------------------------------------------------------------
/src/Wizard/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { observer, inject } from 'mobx-react';
4 | import getNested from 'get-nested';
5 | import WizardProgress from './WizardProgress';
6 | import Fieldset from '../Fieldset';
7 | import BaseButton from '../BaseButton';
8 | import SubmitButton from '../SubmitButton';
9 | import WebformElement from '../WebformElement';
10 | import Webform from '../Webform';
11 | import FormStore from '../Observables/Form';
12 | // styled
13 | import ButtonWrapper from './styled/button-wrapper';
14 | import ButtonNext from './styled/button-next';
15 | import ButtonPrev from './styled/button-prev';
16 |
17 | @inject('submit', 'webform')
18 | @observer
19 | class WizardPages extends Component {
20 |
21 | static propTypes = {
22 | field: PropTypes.shape({
23 | composite_elements: PropTypes.arrayOf(PropTypes.shape()).isRequired,
24 | }).isRequired,
25 | form: PropTypes.shape().isRequired,
26 | status: PropTypes.string.isRequired,
27 | formStore: PropTypes.instanceOf(FormStore).isRequired,
28 | submit: PropTypes.func.isRequired,
29 | onChange: PropTypes.func,
30 | onBlur: PropTypes.func,
31 | settings: PropTypes.shape({
32 | custom_elements: PropTypes.shape({
33 | patternError: PropTypes.shape({
34 | '#default_value': PropTypes.string,
35 | '#options': PropTypes.objectOf(PropTypes.string),
36 | }),
37 | }),
38 | }).isRequired,
39 | webform: PropTypes.instanceOf(Webform).isRequired,
40 | webformSettings: PropTypes.shape().isRequired,
41 | webformElement: PropTypes.instanceOf(WebformElement).isRequired,
42 | };
43 |
44 | static defaultProps = {
45 | onChange: () => {
46 | },
47 | onBlur: () => {
48 | },
49 | };
50 |
51 | componentDidMount() {
52 | this.props.webform.onSubmitOverwrite = () => {
53 | if(this.props.formStore.page === this.props.field.composite_elements[this.props.field.composite_elements.length - 1]['#webform_key']) {
54 | return true; // Execute normal onSubmit
55 | }
56 |
57 | this.navigateToNextPage();
58 | return false;
59 | };
60 | }
61 |
62 | componentWillUnmount() {
63 | delete this.props.webform.onSubmitOverwrite;
64 | }
65 |
66 | changePage(shift) {
67 | const pages = this.props.field.composite_elements;
68 | const pageI = pages.indexOf(pages.find(p => this.props.formStore.page === p['#webform_key']));
69 | const newPageI = pageI + shift;
70 | this.props.formStore.page = this.props.field.composite_elements[newPageI]['#webform_key'];
71 | }
72 |
73 | navigateToNextPage() {
74 | // Make sure all errors are shown of visible fields.
75 | this.props.formStore.visibleFieldsOfCurrentPage.forEach(field => field.isBlurred = true);
76 |
77 | // Check if all fields are valid.
78 | if(this.props.formStore.visibleFieldsOfCurrentPage.every(field => field.valid)) {
79 | // If all valid, change the page.
80 | this.changePage(+1);
81 |
82 | // If draft_auto_save is on, submit the page.
83 | if(getNested(() => this.props.form.settings.draft_auto_save)) {
84 | this.props.submit({
85 | in_draft: true,
86 | });
87 | }
88 | }
89 | }
90 |
91 | render() {
92 | const pages = this.props.field.composite_elements;
93 | const pageI = pages.indexOf(pages.find(p => this.props.formStore.page === p['#webform_key']));
94 |
95 | return (
96 |
97 |
98 | {pages.map(page => (
99 |
113 | ))}
114 |
115 |
116 | { e.preventDefault(); this.changePage(-1); }}
118 | disabled={this.props.formStore.page === pages[0]['#webform_key']}
119 | label={getNested(() => pages[pageI]['#prev_button_label']) || getNested(() => this.props.form.settings.wizard_prev_button_label) || 'Previous page'}
120 | primary={false}
121 | />
122 |
123 |
124 | {(this.props.formStore.page === pages[pages.length - 1]['#webform_key'])
125 | ? (
126 | element['#type'] === 'webform_actions')}
131 | show
132 | />
133 | )
134 | : (
135 | pages[pageI]['#next_button_label']) || getNested(() => this.props.form.settings.wizard_next_button_label) || 'Next page'}
137 | type='submit'
138 | />
139 | )
140 | }
141 |
142 |
143 |
144 | );
145 | }
146 | }
147 |
148 | export default WizardPages;
149 |
--------------------------------------------------------------------------------
/src/Wizard/styled/button-next.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | float: right;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/Wizard/styled/button-prev.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | float: left;
5 | `;
6 |
--------------------------------------------------------------------------------
/src/Wizard/styled/button-wrapper.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export default styled.div`
4 | &::after {
5 | content: '';
6 | display: block;
7 | clear: both;
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/src/Wrapper/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Wrapper = (p) => {
5 | const props = Object.assign({}, p, p.component.props); // Merge Wrapper props with Wrapper override props.
6 |
7 | const { component } = props; // Copy component property to delete it later.
8 |
9 | delete props.component; // Delete since component isn't a native React prop.
10 |
11 | props.className = `${props.className || ''} ${props['data-extendClassName'] || ''}`; // Concatenate className and data-extendClassName into the new className.
12 | const Component = React.cloneElement(component, props, props.children); // Clone passed component with merged props and pass children.
13 |
14 | return Component;
15 | };
16 |
17 | Wrapper.propTypes = {
18 | children: PropTypes.node,
19 | component: PropTypes.oneOfType([
20 | PropTypes.string,
21 | PropTypes.element,
22 | ]).isRequired,
23 | };
24 |
25 | Wrapper.defaultProps = {
26 | children: {},
27 | };
28 |
29 | export default Wrapper;
30 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Webform from './Webform';
2 | import TextArea from './TextArea';
3 | import CheckboxField from './CheckboxField';
4 | import EmailField from './EmailField';
5 | import PhoneField from './PhoneField';
6 | import Date from './Date';
7 | import ParagraphField from './ParagraphField';
8 | import SelectField from './SelectField';
9 | import Input from './Input';
10 | import RadioField from './RadioField';
11 | import FetchForm from './FetchForm';
12 | import Fieldset from './Fieldset';
13 | import IBAN from './IBAN';
14 | import Address from './Address';
15 | import Hidden from './Hidden';
16 | import RangeField from './RangeField';
17 | import WizardPages from './Wizard';
18 | import Relation from './Relation';
19 | import FormRow from './FormRow';
20 | import Parser from './Parser';
21 | import Number from './Number';
22 | import Converse from './Converse';
23 | import Label from './WebformElement/styled/label';
24 | import SubmitButton from './SubmitButton';
25 |
26 | const components = {
27 |
28 | // Webform core elements
29 | checkbox: CheckboxField,
30 | date: Date,
31 | email: EmailField,
32 | fieldset: Fieldset,
33 | hidden: Hidden,
34 | radios: RadioField,
35 | range: RangeField,
36 | select: SelectField,
37 | tel: PhoneField,
38 | textfield: Input,
39 | textarea: TextArea,
40 | number: Number,
41 | webform_wizard_pages: WizardPages,
42 | webform_wizard_page: Fieldset,
43 | webform_message: ParagraphField,
44 | webform_actions: SubmitButton,
45 |
46 | // Custom elements
47 | dutch_address: Address,
48 | date_of_birth: Date,
49 | iban_wrapper: FormRow,
50 | iban: IBAN,
51 | natuurmonumenten_relation: Relation,
52 | natuurmonumenten_relation_address: Relation,
53 | webform_converse_payment: Converse,
54 | webform_element_converse_payment: Converse,
55 |
56 | // Legacy elements
57 | webform_email_custom: EmailField,
58 | webform_address_custom: Address,
59 | webform_checkbox_custom: CheckboxField,
60 | webform_select_custom: SelectField,
61 | webform_textarea_custom: TextArea,
62 | webform_radios_custom: RadioField,
63 | webform_telephone_custom: PhoneField,
64 | webform_textfield_custom: Input,
65 | webform_date_custom: Date,
66 | webform_dateofbirth: Date,
67 | webform_iban: FormRow,
68 | webform_relation_custom: Relation,
69 | webform_relation_postcode_custom: Relation,
70 | webform_number_custom: Number,
71 | };
72 |
73 | export default Webform;
74 | export { components, FetchForm, Parser, Label };
75 |
--------------------------------------------------------------------------------
/src/styles/theme.js:
--------------------------------------------------------------------------------
1 | const theme = {
2 | baseColor: '#000',
3 | primaryColor: 'green',
4 | errorColor: '#b72525',
5 | errorBgColor: '#f9e6e6',
6 | borderRadius: '2px',
7 | secondaryColor: 'blue',
8 | inputWidth: '250px',
9 | textAreaHeight: '40px',
10 | inputLineHeight: '1.6em',
11 | inputBgColor: '#fdfdfd',
12 | inputDisabledBgColor: '#dcd9d9',
13 | inlineLabelWidth: '50%',
14 | fieldsetBgColor: '#f5f5f5',
15 | fieldsetBorder: 'none',
16 | placeholderColor: '#6a6a6a',
17 | focusColor: 'Highlight',
18 | successColor: 'lightgreen',
19 | borderColor: '#ccc',
20 | checkedColor: '#000',
21 | primaryFont: 'Helvetica',
22 | spacingUnit: '1em',
23 | iconCheckbox: 'url("data:image/svg+xmlbase64,PHN2ZyBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj48cG9seWdvbiBwb2ludHM9IjIuODM2LDE0LjcwOCA1LjY2NSwxMS44NzggMTMuNDE1LDE5LjYyOCAyNi4zMzQsNi43MTIgMjkuMTY0LDkuNTQgMTMuNDE1LDI1LjI4OCAiLz48L3N2Zz4")',
24 | buttonColor: '#333',
25 | buttonColorHover: '#555',
26 | buttonTextColor: '#fff',
27 | buttonSecondaryColor: '#333',
28 | buttonSecondaryColorHover: '#555',
29 | buttonSecondaryTextColor: '#fff',
30 | buttonColorDisabled: 'lightgrey',
31 | formMaxWidth: '740px',
32 | formMargin: '0',
33 | };
34 |
35 | theme.requiredColor = theme.baseColor;
36 | theme.spacingUnitFieldset = theme.spacingUnit;
37 | theme.spacingUnitCheckbox = theme.spacingUnit;
38 | theme.spacingUnitRadio = theme.spacingUnit;
39 | theme.buttonSpacingV = theme.spacingUnit;
40 | theme.buttonSpacingH = theme.spacingUnit;
41 |
42 | export default theme;
43 |
--------------------------------------------------------------------------------
/stories/RemoteForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { FetchForm } from '../src';
4 |
5 | const style = {
6 | label: {
7 | width: 100,
8 | },
9 | input: {
10 | width: 'calc(99% - 110px)',
11 | },
12 | refresh: {
13 | marginLeft: 108,
14 | },
15 | formWrapper: {
16 | overflow: 'hidden',
17 | backgroundColor: '#a1a1a1',
18 | },
19 | form: {
20 | padding: '16px',
21 | boxSizing: 'border-box',
22 | },
23 | hidden: {
24 | display: 'none',
25 | },
26 | collapsed: {
27 | maxHeight: 0,
28 | },
29 | expand: {
30 | position: 'fixed',
31 | top: 0,
32 | right: '30px',
33 | cursor: 'pointer',
34 | fontSize: '30px',
35 | },
36 | expanded: {
37 | top: '120px',
38 | transform: 'rotate(180deg)',
39 | },
40 | };
41 |
42 | class RemoteForm extends React.Component {
43 |
44 | constructor(props) {
45 | super(props);
46 |
47 | this.state = {
48 | baseUrl: 'https://redactie.natuurmonumenten.nl/api/v1',
49 | // baseUrl: 'http://dev.natuurmonumenten.nl/api/v1',
50 | path: 'node/2',
51 | field: 'field_form',
52 | visible: false,
53 | };
54 | }
55 |
56 | render() {
57 | return (
58 |
59 |
60 |
76 |
77 |
this.setState({ visible: !this.state.visible })}
80 | >⇓
81 |
this.form = component}
83 | baseUrl={this.state.baseUrl}
84 | path={this.state.path}
85 | field={this.state.field}
86 | />
87 |
88 | );
89 | }
90 | }
91 |
92 | export default RemoteForm;
93 |
--------------------------------------------------------------------------------
/utils/postinstall.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console,global-require */
2 | const spawn = require('child_process').spawnSync;
3 | const path = require('path');
4 |
5 | if(!require('fs').existsSync(path.join(__dirname, '../node_modules/eslint'))) {
6 | console.log('Installing devDependencies of hn-react-webform..', path.join(__dirname, 'node_modules/eslint/package.json'));
7 | spawn('npm', 'install --ignore-scripts'.split(' '), { cwd: __dirname, stdio: 'inherit' });
8 | } else {
9 | console.log('DevDependencies of hn-react-webform already installed, not installing again.');
10 | }
11 |
12 | if(!require('fs').existsSync(path.join(__dirname, '../lib'))) {
13 | console.log('Building hn-react-webform..');
14 | spawn('npm', 'run build'.split(' '), { cwd: __dirname, stdio: 'inherit' });
15 |
16 | console.log('Building done, hn-react-webform is ready to go!');
17 | } else {
18 | console.log('Already built hn-react-webform, not building again.');
19 | }
20 |
--------------------------------------------------------------------------------
/utils/prepublish.js:
--------------------------------------------------------------------------------
1 | const pkg = require('../package.json');
2 |
3 | delete pkg.scripts.postinstall;
4 | require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
5 |
--------------------------------------------------------------------------------