├── .npmignore ├── .gitignore ├── content ├── form1.png └── reforms.png ├── .github ├── dependabot.yml ├── workflows │ ├── build.yml │ └── npm-publish.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── src ├── validator │ ├── BoolValidator.js │ ├── RequiredValidator.js │ └── StringValidator.js ├── output │ ├── ReformsHiddenOutput.vue │ ├── ReformsHtmlOutput.vue │ ├── ReformsPasswordOutput.vue │ ├── ReformsStringOutput.vue │ ├── ReformsUrlOutput.vue │ ├── ReformsTelOutput.vue │ ├── ReformsEmailOutput.vue │ ├── ReformsRadioOutput.vue │ ├── ReformsCheckboxOutput.vue │ ├── ReformsBooleanOutput.vue │ ├── ReformsSelectOutput.vue │ ├── ReformsGroupOutput.vue │ └── ReformsFileOutput.vue ├── ReformsInputMixin.js ├── ReformsValidators.js ├── ReformsCard.vue ├── input │ ├── ReformsHiddenInput.vue │ ├── ReformsBooleanInput.vue │ ├── ReformsTextInput.vue │ ├── ReformsTelInput.vue │ ├── ReformsUrlInput.vue │ ├── ReformsEmailInput.vue │ ├── ReformsGroupInput.vue │ ├── ReformsRadioInput.vue │ ├── ReformsNumberInput.vue │ ├── ReformsCheckboxInput.vue │ ├── ReformsSelectInput.vue │ ├── ReformsStringInput.vue │ ├── ReformsHtmlInput.vue │ ├── ReformsDateInput.vue │ ├── ReformsPasswordInput.vue │ └── ReformsFileInput.vue ├── ReformsLabel.vue ├── ReformsFileMixin.js ├── lang │ ├── en.js │ └── ru.js ├── ReformsSchema.js ├── Reforms.js ├── ReformsForm.vue ├── ReformsOutput.vue ├── ReformsContainerMixin.js ├── ReformsTypes.js ├── Util.js └── ReformsInput.vue ├── examples ├── index.js ├── InputsOutputs.vue ├── types │ ├── NumberType.vue │ └── StringType.vue └── ExamplesIndex.vue ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | content/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /content/form1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malikzh/reforms/HEAD/content/form1.png -------------------------------------------------------------------------------- /content/reforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malikzh/reforms/HEAD/content/reforms.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /src/validator/BoolValidator.js: -------------------------------------------------------------------------------- 1 | import {createValidator, createMatcher} from "../Util"; 2 | 3 | export default { 4 | checked: createValidator(createMatcher( 5 | (value) => value === true, 6 | 'not_checked', 7 | ), 'checked', false), 8 | }; 9 | -------------------------------------------------------------------------------- /src/output/ReformsHiddenOutput.vue: -------------------------------------------------------------------------------- 1 | 4 | 15 | -------------------------------------------------------------------------------- /src/ReformsInputMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | id: { 4 | type: String, 5 | default: undefined, 6 | }, 7 | isValid: { 8 | default: null, 9 | }, 10 | inputClass: [String, Object, Array], 11 | name: String, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/ReformsValidators.js: -------------------------------------------------------------------------------- 1 | import RequiredValidator from './validator/RequiredValidator' 2 | import StringValidator from "./validator/StringValidator"; 3 | import BoolValidator from "./validator/BoolValidator"; 4 | 5 | export default { 6 | ...RequiredValidator, 7 | ...StringValidator, 8 | ...BoolValidator, 9 | }; 10 | -------------------------------------------------------------------------------- /src/output/ReformsHtmlOutput.vue: -------------------------------------------------------------------------------- 1 | 4 | 17 | -------------------------------------------------------------------------------- /src/output/ReformsPasswordOutput.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | -------------------------------------------------------------------------------- /src/output/ReformsStringOutput.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue'; 2 | import ExamplesIndex from './ExamplesIndex' 3 | import Reforms from "../src/Reforms"; 4 | 5 | // Initialize bootstrap 5 6 | import 'bootstrap/dist/js/bootstrap.bundle'; 7 | import 'bootstrap/dist/css/bootstrap.css'; 8 | 9 | const app = createApp(ExamplesIndex); 10 | app.use(Reforms); 11 | app.mount('#app'); 12 | -------------------------------------------------------------------------------- /examples/InputsOutputs.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | -------------------------------------------------------------------------------- /src/output/ReformsUrlOutput.vue: -------------------------------------------------------------------------------- 1 | 8 | 19 | -------------------------------------------------------------------------------- /src/output/ReformsTelOutput.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | -------------------------------------------------------------------------------- /src/output/ReformsEmailOutput.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | -------------------------------------------------------------------------------- /src/output/ReformsRadioOutput.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | -------------------------------------------------------------------------------- /examples/types/NumberType.vue: -------------------------------------------------------------------------------- 1 | 13 | 23 | -------------------------------------------------------------------------------- /src/output/ReformsCheckboxOutput.vue: -------------------------------------------------------------------------------- 1 | 8 | 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 15.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run build 26 | -------------------------------------------------------------------------------- /src/ReformsCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 27 | -------------------------------------------------------------------------------- /src/input/ReformsHiddenInput.vue: -------------------------------------------------------------------------------- 1 | 6 | 26 | -------------------------------------------------------------------------------- /src/ReformsLabel.vue: -------------------------------------------------------------------------------- 1 | 13 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/output/ReformsBooleanOutput.vue: -------------------------------------------------------------------------------- 1 | 14 | 37 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import vue from 'rollup-plugin-vue'; 2 | import PostCSS from 'rollup-plugin-postcss' 3 | import NodeResolve from '@rollup/plugin-node-resolve' 4 | import commonjs from "rollup-plugin-commonjs"; 5 | 6 | export default { 7 | input: 'src/Reforms.js', 8 | output: [ 9 | { 10 | file: 'dist/reforms.esm.js', 11 | format: 'es', 12 | }, 13 | { 14 | name: 'reforms', 15 | file: 'dist/reforms.umd.js', 16 | format: 'umd', 17 | }, 18 | { 19 | file: 'dist/reforms.min.js', 20 | format: 'iife', 21 | }, 22 | ], 23 | plugins: [ 24 | NodeResolve(), 25 | commonjs(), 26 | vue({ 27 | cssModulesOptions: { 28 | generateScopedName: '[local]___[hash:base64:5]', 29 | }, 30 | }), 31 | PostCSS() 32 | ], 33 | external: ['vue'], 34 | }; 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | - run: npm ci 19 | - run: npm run build 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 14 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm run build 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /src/input/ReformsBooleanInput.vue: -------------------------------------------------------------------------------- 1 | 8 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 EMPLA GROUP, LLP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/output/ReformsSelectOutput.vue: -------------------------------------------------------------------------------- 1 | 13 | 47 | -------------------------------------------------------------------------------- /src/output/ReformsGroupOutput.vue: -------------------------------------------------------------------------------- 1 | 8 | 48 | -------------------------------------------------------------------------------- /src/input/ReformsTextInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 48 | -------------------------------------------------------------------------------- /src/input/ReformsTelInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 47 | -------------------------------------------------------------------------------- /src/input/ReformsUrlInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 47 | -------------------------------------------------------------------------------- /src/input/ReformsEmailInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 47 | -------------------------------------------------------------------------------- /src/ReformsFileMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | fileSizeUnit: { 4 | type: String, 5 | default: 'iec', 6 | }, 7 | }, 8 | methods: { 9 | isImage(name) { 10 | return name.match(/\.(?:png|jpe?g|gif|svg|webp)$/); 11 | }, 12 | roundSize(x) { 13 | return Math.round(x * 100) / 100; 14 | }, 15 | fileSize(size) { 16 | const unitSize = (this.fileSizeUnit === 'si' ? 1000 : 1024); 17 | 18 | const kib = this.roundSize(size / unitSize); 19 | 20 | if (kib < unitSize) { 21 | return kib + (this.fileSizeUnit === 'si' ? ' KB' : ' KiB'); 22 | } 23 | 24 | const mib = this.roundSize(kib / unitSize); 25 | 26 | if (mib < unitSize) { 27 | return mib + (this.fileSizeUnit === 'si' ? ' MB' : ' MiB'); 28 | } 29 | 30 | const gib = this.roundSize(mib / unitSize); 31 | 32 | if (gib < unitSize) { 33 | return gib + (this.fileSizeUnit === 'si' ? ' GB' : ' GiB'); 34 | } 35 | 36 | const tib = this.roundSize(gib / unitSize); 37 | return tib + (this.fileSizeUnit === 'si' ? ' TB' : ' TiB'); 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/input/ReformsGroupInput.vue: -------------------------------------------------------------------------------- 1 | 8 | 50 | -------------------------------------------------------------------------------- /src/input/ReformsRadioInput.vue: -------------------------------------------------------------------------------- 1 | 11 | 46 | -------------------------------------------------------------------------------- /examples/types/StringType.vue: -------------------------------------------------------------------------------- 1 | 30 | 44 | -------------------------------------------------------------------------------- /src/input/ReformsNumberInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 53 | -------------------------------------------------------------------------------- /src/input/ReformsCheckboxInput.vue: -------------------------------------------------------------------------------- 1 | 11 | 47 | -------------------------------------------------------------------------------- /src/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | validation: { 3 | required: { 4 | not_specified: 'This field must be filled', 5 | specified: 'Field is filled', 6 | }, 7 | string: { 8 | must_be_string: 'The value must be a string', 9 | }, 10 | email: { 11 | must_be_email: 'Incorrect E-mail specified' 12 | }, 13 | url: { 14 | must_be_url: 'Invalid URL specified', 15 | }, 16 | alpha: { 17 | not_alpha: 'The specified value must contain only Latin letters', 18 | not_alphanum: 'The specified value must contain only Latin letters and numbers' 19 | }, 20 | 'in': { 21 | must_be_in: 'The specified value can only be: :values', 22 | must_not_be_in: 'The specified value should not be: :values', 23 | }, 24 | regex: { 25 | not_match: 'The specified value does not match the pattern', 26 | }, 27 | starts_with: { 28 | not_starts: 'The specified value must start with: ":value"' 29 | }, 30 | ends_with: { 31 | not_ends: 'The specified value must end with: ":value"' 32 | }, 33 | contains: { 34 | not_contains: 'The specified value does not contain: ":value"', 35 | }, 36 | confirmation: { 37 | not_confirmed: 'The fields don\'t match', 38 | }, 39 | checked: { 40 | not_checked: 'Need to confirm', 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/input/ReformsSelectInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 49 | -------------------------------------------------------------------------------- /src/lang/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | validation: { 3 | required: { 4 | not_specified: 'Это поле должно быть заполнено', 5 | specified: 'Поле заполнено', 6 | }, 7 | string: { 8 | must_be_string: 'Значение должно быть строкой', 9 | }, 10 | email: { 11 | must_be_email: 'Указан некорректный E-mail' 12 | }, 13 | url: { 14 | must_be_url: 'Указан некорректный URL', 15 | }, 16 | alpha: { 17 | not_alpha: 'Указанное значение должно содержать только латинские буквы', 18 | not_alphanum: 'Указанное значение должно содержать только латинские буквы и цифры' 19 | }, 20 | 'in': { 21 | must_be_in: 'Указанное значение может быть только: :values', 22 | must_not_be_in: 'Указанное значение не должно быть: :values', 23 | }, 24 | regex: { 25 | not_match: 'Указанное значение не совпадает с шаблоном', 26 | }, 27 | starts_with: { 28 | not_starts: 'Указанное значение должно начинаться с ":value"' 29 | }, 30 | ends_with: { 31 | not_ends: 'Указанное значение должно заканчиваться строкой ":value"' 32 | }, 33 | contains: { 34 | not_contains: 'Указанное значение не содержит строку: ":value"', 35 | }, 36 | confirmation: { 37 | not_confirmed: 'Поля не совпадают', 38 | }, 39 | checked: { 40 | not_checked: 'Необходимо подтвердить', 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/input/ReformsStringInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@empla/reforms", 3 | "version": "0.2.3", 4 | "description": "Vue 3 and Bootstrap 5 forms generator", 5 | "main": "dist/reforms.umd.js", 6 | "module": "dist/reforms.esm.js", 7 | "unpkg": "dist/reforms.min.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "dev": "vue-cli-service serve --port=9000 examples/index.js", 11 | "build": "rollup -c" 12 | }, 13 | "browser": { 14 | "./sfc": "src/Reforms.js" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/empla/reforms.git" 22 | }, 23 | "keywords": [ 24 | "vue", 25 | "vue3", 26 | "forms", 27 | "generator", 28 | "ui", 29 | "bootstrap5" 30 | ], 31 | "author": "EMPLA GROUP, LLP", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/empla/reforms/issues" 35 | }, 36 | "homepage": "https://github.com/empla/reforms#readme", 37 | "devDependencies": { 38 | "@babel/core": "^7.13.8", 39 | "@babel/preset-env": "^7.13.9", 40 | "@rollup/plugin-node-resolve": "^11.2.0", 41 | "@vue/cli-service": "^5.0.0-alpha.4", 42 | "@vue/compiler-sfc": "^3.0.5", 43 | "bootstrap": "^5.0.0-beta2", 44 | "cross-env": "^7.0.3", 45 | "rollup": "^2.40.0", 46 | "rollup-plugin-commonjs": "^10.1.0", 47 | "rollup-plugin-postcss": "^4.0.0", 48 | "rollup-plugin-vue": "^6.0.0-beta.10", 49 | "vue": "^3.0.5" 50 | }, 51 | "dependencies": { 52 | "flatpickr": "^4.6.9", 53 | "flex-js": "^1.0.5", 54 | "inputmask": "^5.0.5", 55 | "jodit": "^3.6.1", 56 | "lodash": "^4.17.20", 57 | "mitt": "^2.1.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/validator/RequiredValidator.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {createValidator} from "../Util"; 3 | 4 | function isEmpty(value) { 5 | return value === '' || value === null || value === undefined 6 | || (_.isArray(value) && value.length < 1); 7 | } 8 | 9 | export default { 10 | required: createValidator((value, params, lang) => { 11 | const empty = isEmpty(value); 12 | 13 | return { 14 | isValid: !empty, 15 | messages: [ 16 | empty ? lang.not_specified : lang.specified, 17 | ], 18 | }; 19 | }, 'required', false), 20 | required_with: createValidator((value, params, lang, withoutMode) => { 21 | if (!params.form) { 22 | return; 23 | } 24 | 25 | if (!_.isArray(params.options) || params.options.length < 1) { 26 | throw new Error('Validator "required_if" requires option'); 27 | } 28 | 29 | const form = params.form.container; 30 | 31 | 32 | for (const requiredWithFieldName of params.options) { 33 | if ((isEmpty(form[requiredWithFieldName]) && !withoutMode) || (!isEmpty(form[requiredWithFieldName]) && withoutMode)) { 34 | return { 35 | isValid: true, 36 | messages: [ 37 | lang.specified 38 | ], 39 | }; 40 | } 41 | } 42 | 43 | const empty = isEmpty(value); 44 | const isValid = Boolean(!empty); 45 | return { 46 | isValid: isValid, 47 | messages: [ 48 | isValid ? lang.specified : lang.not_specified, 49 | ], 50 | }; 51 | 52 | }, 'required', false), 53 | 54 | required_without(params) { 55 | return this.required_with(params, true); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/input/ReformsHtmlInput.vue: -------------------------------------------------------------------------------- 1 | 6 | 67 | 72 | -------------------------------------------------------------------------------- /src/ReformsSchema.js: -------------------------------------------------------------------------------- 1 | import {h, toRefs, reactive} from 'vue'; 2 | import _ from 'lodash'; 3 | import ReformsInput from "./ReformsInput.vue"; 4 | import ReformsOutput from "./ReformsOutput.vue"; 5 | 6 | function renderTree(schema, component, output, prevName) { 7 | let ret = []; 8 | 9 | for (const key of Object.keys(schema)) { 10 | if (!_.isString(schema[key].type)) { 11 | console.warn('Invalid parameter `type` in schema field: ' + key); 12 | continue; 13 | } 14 | 15 | const attrs = _.isObject(schema[key].attrs) ? schema[key].attrs : {}; 16 | 17 | let props = reactive({ 18 | ...toRefs(component.$attrs), 19 | ...attrs, 20 | type: schema[key].type, 21 | name: !prevName ? key : (prevName + '[' + key + ']'), 22 | value: component.value && component.value[key] ? component.value[key] : null, 23 | }); 24 | 25 | if (!output) { 26 | if (!(!_.isFunction(schema[key].showIf) 27 | || (_.isFunction(schema[key].showIf) 28 | && schema[key].showIf(component.$parent.container)))) { 29 | props.shown = false; 30 | } 31 | } 32 | 33 | ret.push(h(output ? ReformsOutput : ReformsInput, props, _.isObject(schema[key].children) 34 | ? (() => renderTree(schema[key].children, component, output, props.name)) 35 | : undefined)); 36 | } 37 | 38 | return ret; 39 | } 40 | 41 | 42 | export default { 43 | name: 'ReformsSchema', 44 | props: { 45 | schema: { 46 | type: Object, 47 | default: null, 48 | }, 49 | outputMode: Boolean, 50 | value: { 51 | type: Object, 52 | default: {}, 53 | } 54 | }, 55 | render() { 56 | if (!this.schema) { 57 | return; 58 | } 59 | 60 | return renderTree(this.schema, this, this.outputMode); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/input/ReformsDateInput.vue: -------------------------------------------------------------------------------- 1 | 6 | 71 | 76 | -------------------------------------------------------------------------------- /src/output/ReformsFileOutput.vue: -------------------------------------------------------------------------------- 1 | 48 | 63 | -------------------------------------------------------------------------------- /src/input/ReformsPasswordInput.vue: -------------------------------------------------------------------------------- 1 | 22 | 62 | 71 | -------------------------------------------------------------------------------- /src/Reforms.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import ReformsInput from "./ReformsInput.vue"; 3 | import ReformsOutput from "./ReformsOutput.vue"; 4 | import ReformsForm from "./ReformsForm.vue"; 5 | import ReformsCard from "./ReformsCard.vue"; 6 | import ReformsTypes from "./ReformsTypes"; 7 | import ReformsValidators from './ReformsValidators'; 8 | 9 | // Plugin main initializer 10 | export default { 11 | /** 12 | * Initialize reforms vue plugin 13 | * 14 | * @param {Object} app 15 | * @param {Object?} options 16 | */ 17 | install(app, options) { 18 | let types = {}; 19 | 20 | Object.keys(ReformsTypes).forEach((key) => { 21 | types[key] = { 22 | input: ReformsTypes[key].input ? ReformsTypes[key].input.name : null, 23 | output: ReformsTypes[key].output ? ReformsTypes[key].output.name : null, 24 | }; 25 | }); 26 | 27 | const $reforms = { 28 | types: types, 29 | validators: ReformsValidators, 30 | }; 31 | 32 | 33 | if (_.isObject(options) && _.isObject(options.types)) { 34 | options.types.keys().forEach((k) => !void($reforms.types[k] = options.types[k])); 35 | } 36 | 37 | let registeredInputs = []; 38 | let registeredOutputs = []; 39 | 40 | for (const v of Object.values(ReformsTypes)) { 41 | if (v.input && registeredInputs.indexOf(v.input) === -1) { 42 | app.component(v.input.name, v.input); 43 | registeredInputs.push(v.input); 44 | } 45 | 46 | if (v.output && registeredOutputs.indexOf(v.output) === -1) { 47 | app.component(v.output.name, v.output); 48 | registeredOutputs.push(v.output); 49 | } 50 | } 51 | 52 | // Register system properties and components 53 | app.config.globalProperties.$reforms = $reforms; 54 | 55 | app.component(ReformsInput.name, ReformsInput); 56 | app.component(ReformsForm.name, ReformsForm); 57 | app.component(ReformsOutput.name, ReformsOutput); 58 | app.component(ReformsCard.name, ReformsCard); 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /examples/ExamplesIndex.vue: -------------------------------------------------------------------------------- 1 | 61 | 75 | -------------------------------------------------------------------------------- /src/ReformsForm.vue: -------------------------------------------------------------------------------- 1 | 14 | 93 | -------------------------------------------------------------------------------- /src/ReformsOutput.vue: -------------------------------------------------------------------------------- 1 | 15 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | Reforms.js 4 | 5 | # Reforms.js 6 | 7 | Vue 3 and Bootstrap 5 forms and cards generator 8 | 9 | --- 10 | 11 | [![Build](https://github.com/empla/reforms/actions/workflows/build.yml/badge.svg)](https://github.com/empla/reforms/actions/workflows/build.yml) 12 | ![GitHub](https://img.shields.io/github/license/empla/reforms) 13 | ![npm](https://img.shields.io/npm/v/@empla/reforms) 14 | ![npm](https://img.shields.io/npm/dt/@empla/reforms) 15 | ![GitHub Repo stars](https://img.shields.io/github/stars/empla/reforms?style=social) 16 | 17 | --- 18 | 19 |
20 | 21 | ## Features 22 | 23 | - Info cards 24 | - Forms 25 | - 15+ inputs types and output types 26 | - Forms schemas 27 | - Plugin system 28 | - Internationalization 29 | - Input & Output groups 30 | - Form validation 31 | - Server side validation 32 | - Multiple and sortable fields 33 | 34 | # Demo 35 | 36 | You can [try it online](https://codesandbox.io/s/reforms-demo-jbpyv) 37 | 38 | 39 | ## Installation 40 | 41 | ```sh 42 | # Install before 43 | npm i vue@next bootstrap@next 44 | 45 | # Install reforms 46 | npm i @empla/reforms 47 | ``` 48 | 49 | ## Example 50 | 51 | ### Example with markup 52 | 53 | ```vue 54 | 55 | 56 | 57 | 58 | 61 | 62 | ``` 63 | 64 | It creates form: 65 | 66 | ![Form](content/form1.png) 67 | 68 | ### Example with schema 69 | 70 | You can create form with schema: 71 | 72 | ```js 73 | const schema = { 74 | firstname: { 75 | type: 'string', 76 | attrs: { 77 | validation: 'required', 78 | label: 'First name' 79 | } 80 | }, 81 | lastname: { 82 | type: 'string', 83 | attrs: { 84 | validation: 'required', 85 | label: 'Last name' 86 | } 87 | }, 88 | notes: { 89 | type: 'html', 90 | attrs: { 91 | label: 'Notes', 92 | } 93 | } 94 | }; 95 | ``` 96 | 97 | And pass it to form: 98 | 99 | ```vue 100 | 101 | 104 | 105 | ``` 106 | 107 | ## Documentation 108 | 109 | https://reforms.js.org/documentation/ 110 | 111 | ## License 112 | 113 | [MIT](LICENSE) 114 | 115 | --- 116 | 117 | Copyright © 2021 EMPLA GROUP, LLP 118 | 119 | Made with ❤️ 120 | by [Malik Zharykov](https://github.com/malikzh)️ 121 | -------------------------------------------------------------------------------- /src/ReformsContainerMixin.js: -------------------------------------------------------------------------------- 1 | import {toRef, watch} from 'vue'; 2 | import _ from 'lodash'; 3 | 4 | export default { 5 | data() { 6 | return { 7 | containerInputs: {}, 8 | container: {}, 9 | containerWatchers: {}, 10 | containerListeners: {}, 11 | }; 12 | }, 13 | methods: { 14 | registerInput(input) { 15 | if (!this.container) { 16 | this.container = {}; 17 | } 18 | 19 | this.containerListeners[input.name] = { 20 | modelValue: (modelValue) => { 21 | if (!input.$props.shown) { 22 | this.container[input.name] = undefined; 23 | return 24 | } 25 | 26 | this.container[input.name] = modelValue; 27 | }, 28 | shown: (shown) => { 29 | if (!shown) { 30 | this.container[input.name] = undefined; 31 | } else { 32 | input.updateValues(); 33 | } 34 | }, 35 | } 36 | 37 | input.events.on('out:modelValue', this.containerListeners[input.name].modelValue); 38 | input.events.on('out:shown', this.containerListeners[input.name].shown); 39 | 40 | this.containerWatchers[input.name] = { 41 | containerWatch: watch(toRef(this.container, input.name), (value) => { 42 | if (!input.$props.shown) { 43 | return; 44 | } 45 | 46 | input.events.emit('in:modelValue', value); 47 | }, {deep: true, immediate: true}), 48 | validationWatch: watch(toRef(this.$props, 'validationResult'), (validationResult) => { 49 | if (_.isArray(validationResult[input.name])) { 50 | input.inputValidation = validationResult[input.name]; 51 | } 52 | }, {deep: true, immediate: true}), 53 | validatorsWatch: watch(toRef(this.$props, 'validation'), (validation) => { 54 | if (_.isArray(validation)) { 55 | input.validators = validation; 56 | } 57 | }, {deep: true, immediate: true}), 58 | }; 59 | 60 | this.containerInputs[input.name] = input; 61 | }, 62 | unregisterInput(name) { 63 | if (!this.container[name]) { 64 | return; 65 | } 66 | 67 | this.container[name] = undefined; 68 | this.containerWatchers[name].containerWatch(); 69 | this.containerWatchers[name].validationWatch(); 70 | this.containerWatchers[name].validatorsWatch(); 71 | this.containerWatchers[name] = undefined; 72 | this.containerInputs[name].events.off('out:modelValue', this.containerListeners[name].modelValue); 73 | this.containerInputs[name].events.off('out:shown', this.containerListeners[name].shown); 74 | this.containerInputs[name] = undefined; 75 | this.containerListeners[name] = undefined; 76 | } 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@empla.group. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/ReformsTypes.js: -------------------------------------------------------------------------------- 1 | import ReformsStringInput from "./input/ReformsStringInput.vue"; 2 | import ReformsStringOutput from "./output/ReformsStringOutput.vue"; 3 | import ReformsGroupInput from "./input/ReformsGroupInput.vue"; 4 | import ReformsGroupOutput from './output/ReformsGroupOutput.vue'; 5 | import ReformsNumberInput from "./input/ReformsNumberInput.vue"; 6 | import ReformsTextInput from "./input/ReformsTextInput.vue"; 7 | import ReformsEmailInput from "./input/ReformsEmailInput.vue"; 8 | import ReformsEmailOutput from "./output/ReformsEmailOutput.vue"; 9 | import ReformsTelInput from "./input/ReformsTelInput.vue"; 10 | import ReformsTelOutput from "./output/ReformsTelOutput.vue"; 11 | import ReformsUrlInput from "./input/ReformsUrlInput.vue"; 12 | import ReformsUrlOutput from "./output/ReformsUrlOutput.vue"; 13 | import ReformsHiddenInput from "./input/ReformsHiddenInput.vue"; 14 | import ReformsHiddenOutput from "./output/ReformsHiddenOutput.vue"; 15 | import ReformsRadioInput from "./input/ReformsRadioInput.vue"; 16 | import ReformsRadioOutput from "./output/ReformsRadioOutput.vue"; 17 | import ReformsCheckboxInput from "./input/ReformsCheckboxInput.vue"; 18 | import ReformsCheckboxOutput from "./output/ReformsCheckboxOutput.vue"; 19 | import ReformsBooleanInput from "./input/ReformsBooleanInput.vue"; 20 | import ReformsBooleanOutput from "./output/ReformsBooleanOutput.vue"; 21 | import ReformsSelectInput from "./input/ReformsSelectInput.vue"; 22 | import ReformsSelectOutput from "./output/ReformsSelectOutput.vue"; 23 | import ReformsDateInput from "./input/ReformsDateInput.vue"; 24 | import ReformsHtmlInput from "./input/ReformsHtmlInput.vue"; 25 | import ReformsHtmlOutput from "./output/ReformsHtmlOutput.vue"; 26 | import ReformsFileInput from "./input/ReformsFileInput.vue"; 27 | import ReformsFileOutput from "./output/ReformsFileOutput.vue"; 28 | import ReformsPasswordInput from "./input/ReformsPasswordInput.vue"; 29 | import ReformsPasswordOutput from "./output/ReformsPasswordOutput.vue"; 30 | 31 | // 32 | // Reforms default components 33 | // 34 | export default { 35 | 'group': { 36 | input: ReformsGroupInput, 37 | output: ReformsGroupOutput, 38 | }, 39 | 'string': { 40 | input: ReformsStringInput, 41 | output: ReformsStringOutput, 42 | }, 43 | 'number': { 44 | input: ReformsNumberInput, 45 | output: ReformsStringOutput, 46 | }, 47 | 'text': { 48 | input: ReformsTextInput, 49 | output: ReformsStringOutput, 50 | }, 51 | 'email': { 52 | input: ReformsEmailInput, 53 | output: ReformsEmailOutput, 54 | }, 55 | 'tel': { 56 | input: ReformsTelInput, 57 | output: ReformsTelOutput, 58 | }, 59 | 'url': { 60 | input: ReformsUrlInput, 61 | output: ReformsUrlOutput, 62 | }, 63 | 'hidden': { 64 | input: ReformsHiddenInput, 65 | output: ReformsHiddenOutput, 66 | }, 67 | 'radio': { 68 | input: ReformsRadioInput, 69 | output: ReformsRadioOutput, 70 | }, 71 | 'checkbox': { 72 | input: ReformsCheckboxInput, 73 | output: ReformsCheckboxOutput, 74 | }, 75 | 'bool': { 76 | input: ReformsBooleanInput, 77 | output: ReformsBooleanOutput, 78 | }, 79 | 'select': { 80 | input: ReformsSelectInput, 81 | output: ReformsSelectOutput, 82 | }, 83 | 'datetime': { 84 | input: ReformsDateInput, 85 | output: ReformsStringOutput, 86 | }, 87 | 'html': { 88 | input: ReformsHtmlInput, 89 | output: ReformsHtmlOutput, 90 | }, 91 | 'file': { 92 | input: ReformsFileInput, 93 | output: ReformsFileOutput, 94 | }, 95 | 'password': { 96 | input: ReformsPasswordInput, 97 | output: ReformsPasswordOutput, 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /src/validator/StringValidator.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {createValidator, createMatcher} from "../Util"; 3 | 4 | export default { 5 | string: createValidator(createMatcher((v) => _.isString(v), 6 | 'must_be_string'), 'string', true), 7 | email: createValidator(createMatcher( 8 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/gi, 9 | 'must_be_email'), 'email', true), 10 | url: createValidator(createMatcher( 11 | /^(https?):\/\/(-\.)?([^\s\/?\.#-]+\.?)+(\/[^\s]*)?$/gi, 12 | 'must_be_url'), 'url', true), 13 | alpha: createValidator(createMatcher( 14 | /^[a-z]+$/gi, 15 | 'not_alpha', 16 | ), 'alpha', true), 17 | alphanum: createValidator(createMatcher(/^[a-z0-9]+$/gi,'not_alphanum'), 'alpha', true), 18 | 'in': createValidator(createMatcher( 19 | (value, params, lang, langParams) => { 20 | langParams['values'] = params.options.join(', '); 21 | return !_.isArray(params.options) || params.options.includes(value); 22 | }, 23 | 'must_be_in'), 'in', true), 24 | not_in: createValidator(createMatcher( 25 | (value, params, lang, langParams) => { 26 | langParams['values'] = params.options.join(', '); 27 | return !_.isArray(params.options) || !params.options.includes(value); 28 | }, 29 | 'must_not_be_in'), 'in', true), 30 | regex: createValidator(createMatcher((value, params) => 31 | !_.isArray(params.options) || params.options.length < 1 || Boolean(String(value).match(new RegExp(params.options[0], params.options.length > 1 ? params.options[1] : undefined))), 'not_match'), 'regex', true), 32 | starts_with: createValidator(createMatcher((value, params, lang, langParams) => { 33 | langParams['value'] = _.isArray(params.options) && params.options.length > 0 ? params.options[0] : ''; 34 | 35 | return !_.isArray(params.options) || params.options.length < 1 || String(value).startsWith(params.options[0]); 36 | }, 37 | 'not_starts' 38 | ), 'starts_with', true), 39 | ends_with: createValidator(createMatcher((value, params, lang, langParams) => { 40 | langParams['value'] = _.isArray(params.options) && params.options.length > 0 ? params.options[0] : ''; 41 | 42 | return !_.isArray(params.options) || params.options.length < 1 || String(value).endsWith(params.options[0]); 43 | }, 44 | 'not_ends' 45 | ), 'ends_with', true), 46 | contains: createValidator(createMatcher((value, params, lang, langParams) => { 47 | langParams['value'] = _.isArray(params.options) && params.options.length > 0 ? params.options[0] : ''; 48 | 49 | return !_.isArray(params.options) || params.options.length < 1 || String(value).includes(params.options[0]); 50 | }, 51 | 'not_contains' 52 | ), 'contains', true), 53 | confirmation: createValidator(createMatcher((value, params, lang) => { 54 | if (!_.isArray(params.options) || params.options.length < 1) { 55 | return true; 56 | } 57 | 58 | 59 | const values = (params.form && params.form.container) || {}; 60 | const valid = values[params.options[0]] === value; 61 | 62 | 63 | (params.form && params.form.containerInputs && params.form.containerInputs[params.options[0]] && (() => { 64 | const input = params.form.containerInputs[params.options[0]]; 65 | 66 | if (valid) { 67 | input.inputValidation = [ 68 | { 69 | isValid: true, 70 | messages: [], 71 | } 72 | ]; 73 | } else { 74 | input.inputValidation = [ 75 | { 76 | isValid: false, 77 | messages: [ 78 | lang['not_confirmed'], 79 | ], 80 | } 81 | ]; 82 | } 83 | })()); 84 | 85 | return valid; 86 | }, 'not_confirmed'), 'confirmation', true), 87 | }; 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /src/input/ReformsFileInput.vue: -------------------------------------------------------------------------------- 1 | 70 | 208 | -------------------------------------------------------------------------------- /src/Util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | const Flex = require('flex-js'); 3 | 4 | export const CHARS_ALPHA_LO = 'abcdefghijklmnopqrstuvwxyz'; 5 | export const CHARS_ALPHA_HI = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 6 | export const CHARS_NUMBER = '0123456789'; 7 | 8 | /** 9 | * Create getter & setter for v-model value 10 | * 11 | * @param {String?} propName v-model Property name 12 | * @returns {{set(*=): void, get(): *}|*} 13 | */ 14 | export function modelValue(propName, castTo) { 15 | propName = propName || 'modelValue'; 16 | castTo = castTo || ((value) => value); 17 | 18 | 19 | return { 20 | get() { 21 | return castTo(this[propName]); 22 | }, 23 | set(value) { 24 | this.$emit('update:' + propName, castTo(value)); 25 | }, 26 | }; 27 | } 28 | 29 | /** 30 | * Merge classes for vue v-bind:class={} 31 | * 32 | * @param {String|Object|String[]}classes 33 | * @returns {Object} 34 | */ 35 | export function mergeClasses(...classes) { 36 | let result = {}; 37 | 38 | const stringArrayToObject = (stringArray, result) => { 39 | stringArray.forEach((className) => { 40 | result[className] = true; 41 | }); 42 | }; 43 | 44 | for (const clazz of classes) { 45 | if (_.isString(clazz)) { 46 | stringArrayToObject(clazz.split(' ').filter((k) => Boolean(k)), result); 47 | } else if (_.isArray(clazz)) { 48 | stringArrayToObject(clazz.filter((k) => Boolean(k)), result); 49 | } else if (_.isObject(clazz)) { 50 | _.merge(result, clazz); 51 | } 52 | } 53 | 54 | return result; 55 | } 56 | 57 | /** 58 | * Generate random string from characters 59 | * 60 | * @param {number} length 61 | * @param {string} characters 62 | * @returns {string} 63 | */ 64 | export function randomStringFromChars(length, characters) { 65 | let result = ''; 66 | for (let i = 0; i < length; i++ ) { 67 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 68 | } 69 | return result; 70 | } 71 | 72 | /** 73 | * Return random id 74 | * 75 | * @param {number} length 76 | * @returns {string} 77 | */ 78 | export function randomId(length) { 79 | return randomStringFromChars(length > 0 ? 1 : 0, CHARS_ALPHA_LO + CHARS_ALPHA_HI) 80 | + randomStringFromChars(length - 1, CHARS_ALPHA_HI + CHARS_ALPHA_LO + CHARS_NUMBER); 81 | } 82 | 83 | /** 84 | * Create validator function 85 | * 86 | * @param {Function} valueValidator 87 | * @param {String} lang 88 | * @param {Boolean} skipNil 89 | * @return {Function} 90 | */ 91 | export function createValidator(valueValidator, lang, skipNil) { 92 | return function(params, ...args) { 93 | let result = []; 94 | 95 | for (const value of (params.multiple ? params.value : [params.value])) { 96 | 97 | if (skipNil && _.isNil(value)) { 98 | result.push({ 99 | isValid: true, 100 | messages: [], 101 | }); 102 | continue; 103 | } 104 | 105 | const v = valueValidator(value, params, params.lang.validation[lang], ...args); 106 | 107 | if (v === false) { 108 | break; 109 | } 110 | 111 | if (_.isObject(v)) { 112 | result.push(v); 113 | } 114 | } 115 | 116 | return result; 117 | }; 118 | } 119 | 120 | export function translate(str, params) { 121 | return Object.entries(params).reduce( 122 | (str, [key, value]) => str.replaceAll(':' + key, value), str); 123 | } 124 | 125 | /** 126 | * Create matcher function for validator 127 | * 128 | * @param {Function|RegExp} matchFunc 129 | * @param {String} langNotOk 130 | * @param {String?} langOk 131 | * @return {function(*=, *=, *=): {isValid: boolean, messages: [*]|[]|[*]}} 132 | */ 133 | export function createMatcher(matchFunc, langNotOk, langOk) { 134 | return function (value, params, lang, ...args) { 135 | let langParams = {}; 136 | 137 | const isValid = Boolean(_.isRegExp(matchFunc) ? String(value).match(matchFunc) : matchFunc(value, params, lang, langParams, ...args)); 138 | 139 | return { 140 | isValid: isValid, 141 | messages: (isValid ? (langOk ? [translate(lang[langOk], langParams)] : []) : [translate(lang[langNotOk], langParams)]), 142 | }; 143 | }; 144 | } 145 | 146 | /** 147 | * Parse validation rules in string 148 | * 149 | * @param {String} validationRules 150 | * @return {Object} 151 | */ 152 | export function parseValidationRules(validationRules) { 153 | let tokens = []; 154 | 155 | const lex = new Flex(); 156 | lex.setIgnoreCase(true); 157 | lex.addState('param', true); 158 | lex.addStateRule(Flex.STATE_INITIAL, /[a-z0-9_\-]+/ 159 | , (lexer) => { 160 | tokens.push({ 161 | id: 'rulename', 162 | value: lexer.text, 163 | }); 164 | 165 | const inp = lexer.input(); 166 | 167 | if (inp === ':') { 168 | lexer.begin('param'); 169 | } else if (inp === '|') { 170 | lexer.begin(Flex.STATE_INITIAL); 171 | } else if (inp === '') { 172 | ; 173 | } else { 174 | throw new Error('Unexpected char: "' + inp + '"'); 175 | } 176 | }); 177 | lex.addStateRule('param', /[^,"'|]+/ 178 | , (lexer) => { 179 | tokens.push({ 180 | id: 'param', 181 | value: lexer.text, 182 | }); 183 | 184 | const inp = lexer.input(); 185 | 186 | if (inp === ',') { 187 | return; 188 | } else if (inp === '|') { 189 | lexer.begin(Flex.STATE_INITIAL); 190 | } 191 | }); 192 | 193 | lex.addStateRule('param', /("|')/, (lexer) => { 194 | let char = ''; 195 | let str = ''; 196 | const quote = lexer.text; 197 | 198 | do { 199 | char = lexer.input(); 200 | 201 | if (char === quote) { 202 | break; 203 | } 204 | 205 | if (char === '\\') { 206 | const esc = lexer.input(); 207 | 208 | if (esc === '"') { 209 | str += '"'; 210 | } else if (esc === '\'') { 211 | str += '\''; 212 | } else if (esc === 't') { 213 | str += '\t'; 214 | } else { 215 | str += '\\'; 216 | } 217 | 218 | continue; 219 | } 220 | 221 | str += char; 222 | 223 | } while (char !== ''); 224 | 225 | tokens.push({ 226 | id: "param", 227 | value: str, 228 | }); 229 | 230 | if (lexer.input() !== ',') { 231 | lexer.terminate(); 232 | } 233 | }); 234 | 235 | lex.setSource(validationRules); 236 | lex.lex(); 237 | 238 | let params = []; 239 | 240 | let result = []; 241 | 242 | for (const token of tokens.reverse()) { 243 | if (token.id === 'param') { 244 | params.push(token); 245 | } else { 246 | result.push({ 247 | name: token.value, 248 | options: params.map((v) => v.value), 249 | }); 250 | params = []; 251 | } 252 | } 253 | 254 | return result.reverse(); 255 | } 256 | 257 | export function passwordStrengthEstimate(password) { 258 | let est = 0; 259 | 260 | if (password.length >= 8) { 261 | ++est; 262 | } 263 | 264 | if ((/[^a-zA-Z0-9]/).test(password)) { 265 | ++est; 266 | } 267 | 268 | if ((/[a-z]/).test(password) && (/[A-Z]/).test(password)) { 269 | ++est; 270 | } 271 | 272 | if ((/[0-9]/).test(password)) { 273 | ++est; 274 | } 275 | 276 | return est; 277 | }; 278 | -------------------------------------------------------------------------------- /src/ReformsInput.vue: -------------------------------------------------------------------------------- 1 | 48 | 351 | --------------------------------------------------------------------------------