├── .editorconfig ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── formio-editor │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── npm-publish.md │ ├── package.json │ ├── src │ ├── lib │ │ ├── clone-utils.ts │ │ ├── formio-editor-options.ts │ │ ├── formio-editor.component.css │ │ ├── formio-editor.component.html │ │ ├── formio-editor.component.spec.ts │ │ ├── formio-editor.component.ts │ │ ├── formio-editor.module.ts │ │ ├── formio-json-schema │ │ │ ├── _.ts │ │ │ ├── component-conditional-show_loose.ts │ │ │ ├── component-conditional-show_strict.ts │ │ │ ├── component-conditional.ts │ │ │ ├── component-logic-action.ts │ │ │ ├── component-logic-trigger.ts │ │ │ ├── component-logic.ts │ │ │ ├── component_loose.ts │ │ │ ├── component_strict.ts │ │ │ ├── components.ts │ │ │ ├── components │ │ │ │ ├── columns.ts │ │ │ │ ├── table.ts │ │ │ │ └── tabs.ts │ │ │ └── index.ts │ │ ├── json-change-panel │ │ │ ├── json-change-panel.component.html │ │ │ └── json-change-panel.component.ts │ │ ├── json-editor │ │ │ ├── json-editor-shapes.ts │ │ │ ├── json-editor.component.spec.ts │ │ │ └── json-editor.component.ts │ │ └── resource-json-schema │ │ │ └── json-schema-generator.js │ ├── public-api.ts │ └── test.ts │ ├── styles.css │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ └── initial-form.json ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── formio-config.ts ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── styles │ └── bootstrap │ │ └── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: node_js 4 | node_js: 5 | - "12.16.3" 6 | 7 | # Unfortunately we cannot cache node_modules directory because 8 | # the formio-editor libray would not be found, since it's taken 9 | # from the dist (see optionalDependencies in package.json) 10 | # 11 | #cache: 12 | # directories: 13 | # - ./node_modules 14 | 15 | install: 16 | - npm install 17 | 18 | script: 19 | - npm run build-all-prod 20 | 21 | deploy: 22 | provider: pages 23 | skip_cleanup: true 24 | github_token: $GITHUB_TOKEN 25 | local_dir: dist/formio-editor-demo 26 | on: 27 | branch: master 28 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Angular Form.io Editor - History 2 | 3 | https://github.com/davebaol/angular-formio-editor 4 | 5 | 6 | ## YYYY-MM-DD, version 0.9.6-snapshot 7 | 8 | - Fix issue preventing from using the formio-editor as a formio custom component 9 | - Fix json schema for select component with angular-formio 4.8.x 10 | - Fix json schema conditional properties by properly casting comparison value 11 | - Add json schema support for data grid and tree components 12 | - Don't generate json schema for client-only components 13 | - Use ngx-bootstrap tabset 14 | 15 | ## 2020-06-11, version 0.9.5 16 | 17 | - Show the resource submitted by the renderer and the generated json schema 18 | - Improve the json schema for form validation 19 | 20 | 21 | ## 2020-06-03, version 0.9.4 22 | 23 | - Support high level of configurability 24 | 25 | 26 | ## 2020-06-01, version 0.9.3 27 | 28 | - Fix json editor refresh in view mode 29 | 30 | 31 | ## 2020-05-30, version 0.9.2 32 | 33 | - Minor changes 34 | - Fix usage and add troubleshooting in README.md 35 | 36 | 37 | ## 2020-05-28, version 0.9.1 38 | 39 | - First fully usable version 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Dineshkumar Pandiyan 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Form.io Editor Component 2 | 3 | [![npm version](https://badge.fury.io/js/%40davebaol%2Fangular-formio-editor.svg)](https://badge.fury.io/js/%40davebaol%2Fangular-formio-editor) [![Build Status](https://travis-ci.com/davebaol/angular-formio-editor.svg?branch=master)](https://travis-ci.com/davebaol/angular-formio-editor) [![dependencies Status](https://david-dm.org/davebaol/angular-formio-editor/status.svg)](https://david-dm.org/davebaol/angular-formio-editor) [![devDependencies Status](https://david-dm.org/davebaol/angular-formio-editor/dev-status.svg)](https://david-dm.org/davebaol/angular-formio-editor?type=dev) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | 5 | This Angular component provides [Form.io](https://www.form.io/) builder and renderer integrated with json editor. 6 | 7 | It works with latest Angular 9. 8 | 9 | Example: 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | ## Try the Live Demos: [Demo Dev](https://davebaol.github.io/angular-formio-editor/) and [Demo Stable](https://davebaol.github.io/angular-formio-editor-demo/) 16 | 17 | In case the live demo goes down for whatever reason, the component is supposed to look somewhat like this (click any image to enlarge it): 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
formio-editor-builderformio-editor-json-code
formio-editor-json-treeformio-editor-renderer
28 | 29 | ## Installation 30 | 31 | To install this library with npm, run below command: 32 | ``` 33 | $ npm install --save angular-formio jsoneditor ngx-bootstrap @angular/elements @davebaol/angular-formio-editor 34 | ``` 35 | Yes, you have to install 5 packages!!! :scream: 36 | 37 | Having in mind the dependency graph can be useful for choosing the version of the various packages for your application. :wink: 38 |
39 | Peer dependency graph 👈 40 | 41 |

42 | Peer dependencies graph 43 |

44 | 60 | 61 |
62 | 63 | 64 | ## Usage 65 | 66 | To use this component in your Angular application follow the steps below: 67 | 68 | :one: Import Angular module `FormioEditorModule` as below: 69 | 70 | ```ts 71 | import { FormioEditorModule } from '@davebaol/angular-formio-editor'; 72 | 73 | @NgModule({ 74 | declarations: [ 75 | AppComponent 76 | ], 77 | imports: [ 78 | ...., 79 | FormioEditorModule 80 | ], 81 | providers: [], 82 | bootstrap: [AppComponent] 83 | }) 84 | export class AppModule { } 85 | ``` 86 | :two: Setup your component models as below: 87 | 88 | ```ts 89 | import { Component } from '@angular/core'; 90 | import { FormioEditorOptions } from '@davebaol/angular-formio-editor'; 91 | 92 | @Component({ 93 | selector: 'app-root', 94 | template: ` 95 |
96 |
97 | 98 |
99 |
100 | `, 101 | styleUrls: ['./app.component.css'] 102 | }) 103 | export class AppComponent { 104 | form: any; 105 | options: FormioEditorOptions; 106 | 107 | constructor() { 108 | this.form = { 109 | display: 'form', 110 | components: [] 111 | }; 112 | this.options = { 113 | builder: { 114 | hideDisplaySelect: true, 115 | output: { 116 | change: (event) => console.log('Demo: builder change event:', event), 117 | } 118 | }, 119 | json: { 120 | changePanelLocations: ['top', 'bottom'], 121 | input: { 122 | options: { 123 | modes: ['code', 'tree', 'view'], // set allowed modes 124 | mode: 'view' // set default mode 125 | } 126 | } 127 | }, 128 | renderer: { 129 | input: { 130 | options: { 131 | src: 'http://localhost:8765/api/v1/documents', 132 | renderOptions: { breadcrumbSettings: { clickable: true } } 133 | } 134 | } 135 | } 136 | }; 137 | } 138 | } 139 | ``` 140 | :three: To properly style this component, import the `.css` files below into your main `style.css` file 141 | ```css 142 | @import "./styles/bootstrap/css/bootstrap.min.css"; 143 | @import '~font-awesome/css/font-awesome.min.css'; 144 | @import "~jsoneditor/dist/jsoneditor.min.css"; 145 | @import "~@davebaol/angular-formio-editor/styles.css"; 146 | ``` 147 | Note that this library only needs the `.css` from bootstrap, not the `.js`, since `ngx-bootstrap` is used internally. 148 | So you don't have necessarily to add bootstrap and its peer dependency jQuery. 149 | 150 | :four: Troubleshooting 151 | 152 | - If during `ng build` execution you encounter this error 153 | ``` 154 | Generating ES5 bundles for differential loading... 155 | An unhandled exception occurred: Call retries were exceeded 156 | ``` 157 | make sure you're using node 12+. If this does not work for you then try the other possible solutions mentioned [here](https://github.com/angular/angular-cli/issues/15493). 158 | 159 | ## Documentation 160 | 161 | The component supports the input arguments `form`, `options` and `reset` described below: 162 | 163 | - **form**
164 | This is a regular form defined by the form.io framework. The component modifies this argument in place. 165 | - **options**
166 | The options have 3 properties, one for each tab of the component: `builder`, `json`, `renderer`. 167 | Open the spoilers to see the details. 168 | -
options.builder 👈 169 | 170 | ```javascript 171 | { 172 | // Whether to hide the builder tab or not. Defaults to false. 173 | hideTab: false, 174 | // Specify if the builder is the active tab at component startup. Defaults to true. 175 | defaultTab: true, 176 | // Whether to hide or not the embedded select to change the form display. Defaults to false. 177 | hideDisplaySelect: false, 178 | 179 | // Input and output arguments of the component . 180 | // Refer to the official documentation. 181 | input: {}, 182 | output: {} 183 | } 184 | ``` 185 |
186 | -
options.json 👈 187 | 188 | ```javascript 189 | { 190 | // Whether to hide the json tab or not. Defaults to false. 191 | hideTab: false, 192 | // Specify if json is the active tab at component startup. Defaults to false. 193 | defaultTab: false, 194 | // The locations relative to the json editor where to show the panel 195 | // for applying json changes to the form. Defaults to ['top', 'bottom']. 196 | changePanelLocations: ['top', 'bottom'], 197 | 198 | // Input arguments of the component . 199 | input: { 200 | // Note that these options are only intended as a component setup at creation-time. 201 | options: { 202 | // Whether to expand or not all nodes in tree mode. This is an additional option 203 | // not supported by the original jsoneditor. Defaults to false. 204 | expandAll: false, 205 | 206 | // Other options supported by the original jsoneditor. 207 | // See jsoneditor API documentation at the link below 208 | // https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options 209 | ... 210 | } 211 | }, 212 | // Output arguments of the component . 213 | output: { 214 | dataChange: (event: any) => {} 215 | dataError: (event: any) => {} 216 | } 217 | } 218 | ``` 219 |
220 | -
options.renderer 👈 221 | 222 | ```javascript 223 | { 224 | // Whether to hide the renderer tab or not. Defaults to false. 225 | hideTab: false, 226 | // Specify if renderer is the active tab at component startup. Defaults to false. 227 | defaultTab: false, 228 | // Configuration of the submission panel. 229 | submissionPanel: { 230 | // Whether to show the submission panel or not. Default to false. 231 | disabled: false, 232 | // Whether to initially show full or partial submission. Default to false. 233 | fullSubmission: false, 234 | // The json editor of the submitted resource. 235 | resourceJsonEditor: { 236 | // Input and output arguments of this component . 237 | // See options.json.input and options.json.output above. 238 | input: {}, 239 | output: {} 240 | }, 241 | // The json editor of the json schema for the submitted resource 242 | schemaJsonEditor: { 243 | // Whether to show or not the schema json editor. Defaults to false. 244 | enabled: true, 245 | // Input and output arguments of this component . 246 | // See options.json.input and options.json.output above. 247 | input: {}, 248 | output: {} 249 | } 250 | } 251 | }, 252 | // Input and output arguments of the component that renders the form. 253 | // Refer to the official documentation. 254 | input: {}, 255 | output: {} 256 | } 257 | ``` 258 |
259 | - **reset**
260 | An `Observable` to reset the component. 261 | 262 | 263 | 264 | ## License 265 | 266 | This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. 267 | 268 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "formio-editor-demo": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/formio-editor-demo", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": true, 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/styles.css" 28 | ], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true, 47 | "baseHref": "https://davebaol.github.io/angular-formio-editor/", 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | }, 54 | { 55 | "type": "anyComponentStyle", 56 | "maximumWarning": "6kb", 57 | "maximumError": "10kb" 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "formio-editor-demo:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "formio-editor-demo:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "formio-editor-demo:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "tsconfig.spec.json", 86 | "karmaConfig": "karma.conf.js", 87 | "assets": [ 88 | "src/favicon.ico", 89 | "src/assets" 90 | ], 91 | "styles": [ 92 | "src/styles.css" 93 | ], 94 | "scripts": [] 95 | } 96 | }, 97 | "lint": { 98 | "builder": "@angular-devkit/build-angular:tslint", 99 | "options": { 100 | "tsConfig": [ 101 | "tsconfig.app.json", 102 | "tsconfig.spec.json", 103 | "e2e/tsconfig.json" 104 | ], 105 | "exclude": [ 106 | "**/node_modules/**" 107 | ] 108 | } 109 | }, 110 | "e2e": { 111 | "builder": "@angular-devkit/build-angular:protractor", 112 | "options": { 113 | "protractorConfig": "e2e/protractor.conf.js", 114 | "devServerTarget": "formio-editor-demo:serve" 115 | }, 116 | "configurations": { 117 | "production": { 118 | "devServerTarget": "formio-editor-demo:serve:production" 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "formio-editor": { 125 | "projectType": "library", 126 | "root": "projects/formio-editor", 127 | "sourceRoot": "projects/formio-editor/src", 128 | "prefix": "davebaol", 129 | "architect": { 130 | "build": { 131 | "builder": "@angular-devkit/build-ng-packagr:build", 132 | "options": { 133 | "tsConfig": "projects/formio-editor/tsconfig.lib.json", 134 | "project": "projects/formio-editor/ng-package.json" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "tsConfig": "projects/formio-editor/tsconfig.lib.prod.json" 139 | } 140 | } 141 | }, 142 | "test": { 143 | "builder": "@angular-devkit/build-angular:karma", 144 | "options": { 145 | "main": "projects/formio-editor/src/test.ts", 146 | "tsConfig": "projects/formio-editor/tsconfig.spec.json", 147 | "karmaConfig": "projects/formio-editor/karma.conf.js" 148 | } 149 | }, 150 | "lint": { 151 | "builder": "@angular-devkit/build-angular:tslint", 152 | "options": { 153 | "tsConfig": [ 154 | "projects/formio-editor/tsconfig.lib.json", 155 | "projects/formio-editor/tsconfig.spec.json" 156 | ], 157 | "exclude": [ 158 | "**/node_modules/**" 159 | ] 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | "defaultProject": "formio-editor-demo", 166 | "cli": { 167 | "analytics": false 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('formio-editor-demo app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/formio-editor-demo'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@davebaol/angular-formio-editor-demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "clean": "shx rm -rf dist && shx rm -rf node_modules", 8 | "build": "ng build", 9 | "build-prod": "ng build --prod", 10 | "build-all-prod": "npm run lint && npm run formio-editor:build-prod && npm run test-headless && npm run build-prod -- formio-editor-demo", 11 | "test": "ng test --watch=false", 12 | "test-headless": "ng test --watch=false --browsers=ChromeHeadless", 13 | "lint": "ng lint", 14 | "e2e": "ng e2e", 15 | "formio-editor:build": "ng build formio-editor && shx cp README.md HISTORY.md LICENSE dist/formio-editor", 16 | "formio-editor:build-prod": "ng build formio-editor --prod && shx cp README.md HISTORY.md LICENSE dist/formio-editor" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "~9.1.11", 21 | "@angular/common": "~9.1.11", 22 | "@angular/compiler": "~9.1.11", 23 | "@angular/core": "~9.1.11", 24 | "@angular/elements": "~9.1.11", 25 | "@angular/forms": "~9.1.11", 26 | "@angular/platform-browser": "~9.1.11", 27 | "@angular/platform-browser-dynamic": "~9.1.11", 28 | "@angular/router": "~9.1.11", 29 | "angular-formio": "^4.8.4", 30 | "font-awesome": "^4.7.0", 31 | "jsoneditor": "^9.0.1", 32 | "ngx-bootstrap": "^5.6.1", 33 | "rxjs": "~6.5.4", 34 | "tslib": "^1.10.0", 35 | "zone.js": "~0.10.2" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "~0.901.9", 39 | "@angular-devkit/build-ng-packagr": "~0.901.9", 40 | "@angular/cli": "~9.1.9", 41 | "@angular/compiler-cli": "~9.1.11", 42 | "@angular/language-service": "~9.1.11", 43 | "@types/jasmine": "~3.5.0", 44 | "@types/jasminewd2": "~2.0.3", 45 | "@types/node": "^12.11.1", 46 | "codelyzer": "^5.1.2", 47 | "jasmine-core": "~3.5.0", 48 | "jasmine-spec-reporter": "~4.2.1", 49 | "karma": "~5.0.0", 50 | "karma-chrome-launcher": "~3.1.0", 51 | "karma-coverage-istanbul-reporter": "~2.1.0", 52 | "karma-jasmine": "~3.0.1", 53 | "karma-jasmine-html-reporter": "^1.4.2", 54 | "ng-packagr": "^9.0.0", 55 | "protractor": "~5.4.3", 56 | "shx": "^0.3.2", 57 | "ts-node": "~8.3.0", 58 | "tslint": "~6.1.0", 59 | "typescript": "~3.8.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /projects/formio-editor/README.md: -------------------------------------------------------------------------------- 1 | # FormioEditor 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.6. 4 | 5 | ## Code scaffolding 6 | 7 | Run `ng generate component component-name --project formio-editor` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project formio-editor`. 8 | > Note: Don't forget to add `--project formio-editor` or else it will be added to the default project in your `angular.json` file. 9 | 10 | ## Build 11 | 12 | Run `ng build formio-editor` to build the project. The build artifacts will be stored in the `dist/` directory. 13 | 14 | ## Publishing 15 | 16 | After building your library with `ng build formio-editor`, go to the dist folder `cd dist/formio-editor` and run `npm publish`. 17 | 18 | ## Running unit tests 19 | 20 | Run `ng test formio-editor` to execute the unit tests via [Karma](https://karma-runner.github.io). 21 | 22 | ## Further help 23 | 24 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 25 | -------------------------------------------------------------------------------- /projects/formio-editor/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/formio-editor'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/formio-editor/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/formio-editor", 4 | "assets": [ 5 | "styles.css" 6 | ], 7 | "lib": { 8 | "entryFile": "src/public-api.ts", 9 | "umdModuleIds": { 10 | "angular-formio": "angular-formio", 11 | "jsoneditor": "jsoneditor", 12 | "ngx-bootstrap": "ngx-bootstrap", 13 | "ngx-bootstrap/alert": "ngx-bootstrap/alert", 14 | "ngx-bootstrap/modal": "ngx-bootstrap/modal", 15 | "ngx-bootstrap/tabs": "ngx-bootstrap/tabs" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/formio-editor/npm-publish.md: -------------------------------------------------------------------------------- 1 | To publish a new npm package follow the steps below: 2 | - Prepare for publishing: 3 | - Update version inside `projects/formio-editor/package.json`, for instance from `0.0.0` to `0.1.0` 4 | ```json 5 | { 6 | "name": "@davebaol/angular-formio-editor", 7 | "version": "0.1.0", 8 | ... 9 | } 10 | ``` 11 | - Update the file `HISTORY.md` with the changes of the new version. 12 | - Commit changes to github with a message specifying the new version, for instance `Release 0.1.0` 13 | - Build the entire project to check that everything is ok 14 | ```bash 15 | npm run build-all-prod 16 | ``` 17 | - Test the package possibly by installing it in a new angular app 18 | ```bash 19 | npm install dist/formio-editor 20 | ``` 21 | - Unfortunately, before publishing the package we have to rebuild the library. This is needed because for some reason the tests and the build of the app dirt `dist/formio-editor` with ivy files that npm doesn't like for publishing. 22 | ```bash 23 | npm run formio-editor:build-prod 24 | ``` 25 | - Publish the package 26 | ```bash 27 | cd dist/formio-editor 28 | npm publish --access public 29 | ``` 30 | - Visit the [public package page](https://www.npmjs.com/package/@davebaol/angular-formio-editor). Public packages will say `public` below the package name on the npm website. 31 | - Create a **github release**, for intance `0.1.0` 32 | - Iterate lib version to the new snapshot, for intance 0.1.1-snapshot: 33 | - Update version inside `projects/formio-editor/package.json`, for instance from `0.1.0` to `0.1.1-snapshot` 34 | ```json 35 | { 36 | "name": "@davebaol/angular-formio-editor", 37 | "version": "0.1.1-snapshot", 38 | ... 39 | } 40 | ``` 41 | - Update `HISTORY.md` by adding these lines at the top 42 | ``` 43 | ## YYYY-MM-DD, version 0.1.1-snapshot 44 | 45 | - ... 46 | 47 | ``` 48 | - Commit changes to github with message `Iterate lib version to 0.1.1-snapshot` 49 | -------------------------------------------------------------------------------- /projects/formio-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@davebaol/angular-formio-editor", 3 | "version": "0.9.6-snapshot", 4 | "description": "Angular component integrating Form.io builder and renderer with a json editor", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/davebaol/angular-formio-editor.git" 8 | }, 9 | "keywords": [ 10 | "angular", 11 | "angular-library", 12 | "angular-component", 13 | "formio", 14 | "formio-builder", 15 | "formio-renderer", 16 | "json-editor" 17 | ], 18 | "author": "Davide Sessi ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/davebaol/angular-formio-editor/issues" 22 | }, 23 | "homepage": "https://github.com/davebaol/angular-formio-editor#readme", 24 | "peerDependencies": { 25 | "@angular/common": "^9.1.6", 26 | "@angular/core": "^9.1.6", 27 | "@angular/forms": "^9.1.6", 28 | "angular-formio": "^4.7.9", 29 | "ngx-bootstrap": "^5.6.1", 30 | "jsoneditor": ">=7.4.0" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/clone-utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This is heavily inspired by https://github.com/schnittstabil/merge-options 3 | */ 4 | const { hasOwnProperty, toString } = Object.prototype; 5 | const { propertyIsEnumerable } = Object; 6 | const globalThis = this; 7 | const defaultMergeOpts = { ignoreUndefined: false }; 8 | 9 | type propertyKey = string | number | symbol; 10 | 11 | const isPlainObject = (value: any) => { 12 | if (toString.call(value) !== '[object Object]') { 13 | return false; 14 | } 15 | 16 | const prototype = Object.getPrototypeOf(value); 17 | return prototype === null || prototype === Object.prototype; 18 | }; 19 | 20 | const defineProperty = (obj: object, name: propertyKey, value: any) => { 21 | Object.defineProperty(obj, name, { 22 | value, 23 | writable: true, 24 | enumerable: true, 25 | configurable: true 26 | }); 27 | }; 28 | 29 | const getEnumerableOwnPropertyKeys = (value: object) => { 30 | const keys: propertyKey[] = []; 31 | 32 | for (const key in value) { 33 | if (hasOwnProperty.call(value, key)) { 34 | keys.push(key); 35 | } 36 | } 37 | 38 | if (Object.getOwnPropertySymbols) { 39 | const symbols = Object.getOwnPropertySymbols(value); 40 | 41 | for (const symbol of symbols) { 42 | if (propertyIsEnumerable.call(value, symbol)) { 43 | keys.push(symbol); 44 | } 45 | } 46 | } 47 | 48 | return keys; 49 | }; 50 | 51 | export const clone = (value: any) => { 52 | if (Array.isArray(value)) { 53 | return cloneArray(value); 54 | } 55 | 56 | if (isPlainObject(value)) { 57 | return clonePlainObject(value); 58 | } 59 | 60 | return value; 61 | }; 62 | 63 | const cloneArray = (array: any[]) => { 64 | const result = array.slice(0, 0); 65 | 66 | getEnumerableOwnPropertyKeys(array).forEach(key => { 67 | defineProperty(result, key, clone(array[key])); 68 | }); 69 | 70 | return result; 71 | }; 72 | 73 | const clonePlainObject = (obj: object) => { 74 | const result: object = Object.getPrototypeOf(obj) === null ? Object.create(null) : {}; 75 | 76 | getEnumerableOwnPropertyKeys(obj).forEach(key => { 77 | defineProperty(result, key, clone(obj[key])); 78 | }); 79 | 80 | return result; 81 | }; 82 | 83 | const mergeKeys = (merged, source, keys: propertyKey[], config) => { 84 | keys.forEach(key => { 85 | if (typeof source[key] === 'undefined' && config.ignoreUndefined) { 86 | return; 87 | } 88 | 89 | // Do not recurse into prototype chain of merged 90 | if (key in merged && merged[key] !== Object.getPrototypeOf(merged)) { 91 | defineProperty(merged, key, _merge(merged[key], source[key], config)); 92 | } else { 93 | defineProperty(merged, key, clone(source[key])); 94 | } 95 | }); 96 | 97 | return merged; 98 | }; 99 | 100 | // tslint:disable-next-line:variable-name 101 | const _merge = (merged, source, config) => { 102 | if (!isPlainObject(source) || !isPlainObject(merged)) { 103 | return clone(source); 104 | } 105 | 106 | return mergeKeys(merged, source, getEnumerableOwnPropertyKeys(source), config); 107 | }; 108 | 109 | export const merge = (...options: any[]) => { 110 | const config = _merge(clone(defaultMergeOpts), (this !== globalThis && this) || {}, defaultMergeOpts); 111 | let merged = { _: {} }; 112 | 113 | for (const option of options) { 114 | if (option === undefined) { 115 | continue; 116 | } 117 | 118 | if (!isPlainObject(option)) { 119 | throw new TypeError('`' + option + '` is not a plain Object'); 120 | } 121 | 122 | merged = _merge(merged, { _: option }, config); 123 | } 124 | 125 | return merged._; 126 | }; 127 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-editor-options.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { FormioService, FormioOptions, FormioBaseComponent, FormioRefreshValue } from 'angular-formio'; 4 | import { JsonEditorOptions } from './json-editor/json-editor-shapes'; 5 | 6 | export type FormioEditorTab = 'builder' | 'json' | 'renderer'; 7 | 8 | interface FormioEditorTabOptions { 9 | hideTab?: boolean; 10 | defaultTab?: boolean; 11 | } 12 | 13 | export interface FormioEditorBuilderOptions extends FormioEditorTabOptions { 14 | hideDisplaySelect?: boolean; 15 | input?: { 16 | options?: FormioOptions; 17 | formbuilder?: any; 18 | noeval?: boolean; 19 | refresh?: Observable; 20 | }; 21 | output?: { 22 | change?: (event: object) => void; 23 | }; 24 | } 25 | 26 | interface JsonEditorInputOutputArguments { 27 | input?: { 28 | options?: JsonEditorOptions; 29 | }; 30 | output?: { 31 | dataChange?: (event: any) => void; 32 | dataError?: (event: any) => void; 33 | }; 34 | } 35 | 36 | export interface FormioEditorJsonOptions extends FormioEditorTabOptions, JsonEditorInputOutputArguments { 37 | changePanelLocations?: ('top' | 'bottom')[]; 38 | } 39 | 40 | export interface FormioEditorRendererOptions extends FormioEditorTabOptions { 41 | submissionPanel?: { 42 | disabled?: boolean; 43 | fullSubmission: boolean; 44 | resourceJsonEditor: JsonEditorInputOutputArguments; 45 | schemaJsonEditor: JsonEditorInputOutputArguments & { enabled?: boolean }; 46 | }; 47 | input?: { 48 | submission?: any; 49 | src?: string; 50 | url?: string; 51 | service?: FormioService; 52 | options?: FormioOptions; 53 | noeval?: boolean; 54 | formioOptions?: any; 55 | renderOptions?: any; 56 | readOnly?: boolean; 57 | viewOnly?: boolean; 58 | hideComponents?: string[]; 59 | refresh?: EventEmitter; 60 | error?: EventEmitter; 61 | success?: EventEmitter; 62 | language?: EventEmitter; 63 | hooks?: any; 64 | renderer?: any; 65 | }; 66 | output?: { 67 | render?: (event: object) => void; 68 | customEvent?: (event: object) => void; 69 | submit?: (event: object) => void; 70 | prevPage?: (event: object) => void; 71 | nextPage?: (event: object) => void; 72 | beforeSubmit?: (event: object) => void; 73 | change?: (event: object) => void; 74 | invalid?: (event: boolean) => void; 75 | errorChange?: (event: any) => void; 76 | formLoad?: (event: any) => void; 77 | submissionLoad?: (event: any) => void; 78 | ready?: (event: FormioBaseComponent) => void; 79 | }; 80 | } 81 | 82 | export interface FormioEditorOptions { 83 | builder?: FormioEditorBuilderOptions; 84 | json?: FormioEditorJsonOptions; 85 | renderer?: FormioEditorRendererOptions; 86 | } 87 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-editor.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davebaol/angular-formio-editor/b761ac9b4e06e58348a81d38ce22bb5bff5748b6/projects/formio-editor/src/lib/formio-editor.component.css -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-editor.component.html: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 24 |
25 |
26 | 27 | 32 |
33 | 40 |
41 |
42 | 44 |
45 | 51 | 56 | 62 |
63 |
64 | 66 |
67 | 98 |
99 |
100 |
101 |
102 |

103 | Summission ( 104 | 105 | 106 | ) 107 |

108 | 113 |
114 |
115 |

Json Schema Validator

116 | 121 |
122 |
123 | 124 | 125 | 126 |
127 |
128 |
129 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FormioEditorComponent } from './formio-editor.component'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { FormioModule } from 'angular-formio'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { Component, ViewChild } from '@angular/core'; 8 | 9 | describe('FormioEditorComponent', () => { 10 | let component: TestHostComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ FormioEditorComponent ], 16 | imports: [ 17 | BrowserModule, 18 | FormioModule, 19 | FormsModule 20 | ] 21 | }) 22 | .compileComponents(); 23 | })); 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.createComponent(TestHostComponent); 27 | component = fixture.componentInstance; 28 | fixture.detectChanges(); 29 | }); 30 | 31 | it('should create', () => { 32 | expect(component).toBeTruthy(); 33 | }); 34 | 35 | @Component({ 36 | selector: `davebaol-host-component`, 37 | template: `` 38 | }) 39 | class TestHostComponent { 40 | @ViewChild(FormioEditorComponent) 41 | public testComponent: FormioEditorComponent; 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit, ViewChild, Input, TemplateRef, OnDestroy} from '@angular/core'; 2 | import { Observable, Subscription } from 'rxjs'; 3 | import { TabsetComponent } from 'ngx-bootstrap/tabs'; 4 | import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; 5 | import { FormioEditorOptions, FormioEditorTab } from './formio-editor-options'; 6 | import { JsonEditorComponent } from './json-editor/json-editor.component'; 7 | import { JsonEditorValidationError, JsonEditorOptions } from './json-editor/json-editor-shapes'; 8 | import { merge, clone } from './clone-utils'; 9 | import { generateFormJsonSchema } from './resource-json-schema/json-schema-generator'; 10 | import { loose as formioJsonSchema } from './formio-json-schema'; 11 | 12 | const defaultJsonEditorOptions: JsonEditorOptions = { 13 | enableSort: true, 14 | enableTransform: true, 15 | escapeUnicode: false, 16 | expandAll: false, 17 | history: true, 18 | indentation: 2, 19 | limitDragging: false, 20 | mode: 'view', // set default mode 21 | modes: ['code', 'tree', 'view'], // set allowed modes 22 | schema: formioJsonSchema.schema, 23 | schemaRefs: formioJsonSchema.schemaRefs, 24 | search: true, 25 | sortObjectKeys: false 26 | }; 27 | 28 | const defaultRendererResourceJsonEditorOptions: JsonEditorOptions = merge(defaultJsonEditorOptions, { 29 | mode: 'tree', // set default mode 30 | modes: ['code', 'tree'], // set allowed modes 31 | schema: undefined, 32 | schemaRefs: undefined 33 | }); 34 | 35 | const defaultRendererSchemaJsonEditorOptions: JsonEditorOptions = clone(defaultRendererResourceJsonEditorOptions); 36 | 37 | @Component({ 38 | // tslint:disable-next-line:component-selector 39 | selector: 'formio-editor', 40 | templateUrl: './formio-editor.component.html', 41 | styleUrls: ['./formio-editor.component.css'] 42 | }) 43 | export class FormioEditorComponent implements OnInit, AfterViewInit, OnDestroy { 44 | @Input() form: any; 45 | builderDisplayChanged = false; 46 | 47 | @Input() reset?: Observable; 48 | private resetSubscription: Subscription; 49 | 50 | // tslint:disable-next-line:variable-name 51 | private _options: FormioEditorOptions; 52 | get options() { return this._options; } 53 | @Input() set options(options: FormioEditorOptions) { this.setOptions(options); } 54 | jsonEditorOptions: JsonEditorOptions; 55 | jsonEditorChanged = false; 56 | @ViewChild('jsoneditor', {static: false}) jsonEditor: JsonEditorComponent; 57 | 58 | @ViewChild('renderer_resource_jsoneditor', {static: false}) rendererResourceJsonEditor: JsonEditorComponent; 59 | @ViewChild('renderer_schema_jsoneditor', {static: false}) rendererSchemaJsonEditor: JsonEditorComponent; 60 | rendererResourceJsonEditorOptions: JsonEditorOptions; 61 | rendererSchemaJsonEditorOptions: JsonEditorOptions; 62 | submissionPanel: boolean; 63 | showResourceSchema: boolean; 64 | submission: any; 65 | fullSubmission: boolean; 66 | 67 | @ViewChild('formioEditorTabs', {static: true}) tabset: TabsetComponent; 68 | activeTab: FormioEditorTab; 69 | 70 | modalRef: BsModalRef; 71 | 72 | // tslint:disable-next-line:variable-name 73 | private _jsonEditorErrors: JsonEditorValidationError[] = []; 74 | get jsonEditorErrors() { 75 | return this._jsonEditorErrors; 76 | } 77 | set jsonEditorErrors(errors: JsonEditorValidationError[]) { 78 | this._jsonEditorErrors = errors; 79 | this.jsonEditorWarningCounter = 0; 80 | errors.forEach((error) => { 81 | if (error.type === 'validation') { 82 | this.jsonEditorWarningCounter++; 83 | } 84 | }); 85 | } 86 | 87 | jsonEditorWarningCounter = 0; 88 | 89 | constructor(private modalService: BsModalService) { 90 | } 91 | 92 | ngOnInit(): void { 93 | if (!this.options) { 94 | this.setOptions(); // Set default options 95 | } 96 | 97 | if (this.reset) { 98 | this.resetSubscription = this.reset.subscribe(() => this.resetFormBuilder()); 99 | } 100 | } 101 | 102 | ngAfterViewInit() { 103 | // Activate initial tab specified by options 104 | setTimeout(() => { 105 | // We have to run this inside a setTimeout to prevent the error below: 106 | // 107 | // ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. 108 | // Previous value for 'active': 'true'. Current value: 'false'. 109 | const tabId = (['builder', 'json', 'renderer'] as FormioEditorTab[]) 110 | .find(t => this.options && this.options[t]?.defaultTab ? t : undefined) || 'builder'; 111 | this.selectTab(tabId); 112 | }); 113 | 114 | this.refreshJsonEditor(); 115 | } 116 | 117 | ngOnDestroy() { 118 | if (this.resetSubscription) { 119 | this.resetSubscription.unsubscribe(); 120 | } 121 | } 122 | 123 | private setOptions(options: FormioEditorOptions = {}) { 124 | this._options = options; 125 | this.jsonEditorOptions = merge( 126 | defaultJsonEditorOptions, 127 | options?.json?.input?.options 128 | ); 129 | this.rendererResourceJsonEditorOptions = merge( 130 | defaultRendererResourceJsonEditorOptions, 131 | options?.renderer?.submissionPanel?.resourceJsonEditor?.input?.options 132 | ); 133 | this.rendererSchemaJsonEditorOptions = merge( 134 | defaultRendererSchemaJsonEditorOptions, 135 | options?.renderer?.submissionPanel?.schemaJsonEditor?.input?.options 136 | ); 137 | this.fullSubmission = options?.renderer?.submissionPanel?.fullSubmission; 138 | } 139 | 140 | findTab(tabId: FormioEditorTab) { 141 | // console.log('>>>>>>>>>>>>>>>findTab.this.tabset.tabs', this.tabset.tabs); 142 | return this.tabset.tabs.find(t => t.id === tabId); 143 | } 144 | 145 | selectTab(tabId: FormioEditorTab) { 146 | const tab = this.findTab(tabId); 147 | if (tab) { 148 | // console.log('>>>>>>>>>>>>>>>selectTab.tab', tab); 149 | tab.active = true; // This automatically triggers the onSelectTab() below 150 | } 151 | } 152 | 153 | onSelectTab(event: any) { 154 | // console.log(">>>>>>>>>>onSelectTab:", event) 155 | this.activeTab = event.id; 156 | if (event.id === 'renderer') { 157 | // Disable submission panel when the renderer tab becomes active 158 | this.submissionPanel = false; 159 | } 160 | } 161 | 162 | // 163 | // Form Builder Tab 164 | // 165 | 166 | resetFormBuilder(fromJsonEditor: boolean = false) { 167 | console.log('resetFormBuilder'); 168 | // Here we have to reset builder component through *ngIf="!builderDisplayChanged" 169 | // See https://github.com/formio/angular-formio/issues/172#issuecomment-401876490 170 | this.builderDisplayChanged = true; 171 | setTimeout(() => { 172 | this.builderDisplayChanged = false; 173 | if (!fromJsonEditor) { 174 | this.refreshJsonEditor(true); 175 | } 176 | this.resetFormRendererIfActive(); 177 | }); 178 | } 179 | 180 | onBuilderDiplayChange(event) { 181 | console.log('onBuilderDiplayChange'); 182 | this.resetFormBuilder(); 183 | } 184 | 185 | onBuilderChange(event) { 186 | console.log('onBuilderChange: event', event); 187 | this.refreshJsonEditor(true); 188 | } 189 | 190 | // 191 | // JSON Tab 192 | // 193 | 194 | onJsonEditorError(errors: any[]) { 195 | console.log('onJsonEditorError: found', errors.length, 'validation errors:'); 196 | this.jsonEditorErrors = errors; 197 | } 198 | 199 | onJsonEditorChange(event) { 200 | console.log('onJsonEditorChange'); 201 | this.jsonEditorChanged = true; 202 | } 203 | 204 | onJsonEditorApply(template: TemplateRef) { 205 | console.log('Errors: ', this.jsonEditorErrors.length - this.jsonEditorWarningCounter, 'Warnings: ', this.jsonEditorWarningCounter); 206 | if (this.jsonEditorWarningCounter === 0) { 207 | this.jsonEditorApplyChanges(); 208 | } else { 209 | this.modalRef = this.modalService.show(template); 210 | } 211 | } 212 | 213 | jsonEditorApplyChanges() { 214 | console.log('jsonEditorApplyChanges'); 215 | this.jsonEditorChanged = false; 216 | // Remove all properties from this form 217 | // then copy the properties of the edited json to this form 218 | // and reset the builder 219 | Object.getOwnPropertyNames(this.form).forEach(p => delete this.form[p]); 220 | Object.assign(this.form, this.jsonEditor.get()); 221 | this.resetFormBuilder(true); 222 | } 223 | 224 | jsonEditorDiscardChanges() { 225 | console.log('jsonEditorDiscardChanges'); 226 | this.refreshJsonEditor(); 227 | } 228 | 229 | refreshJsonEditor(forceReset: boolean = false) { 230 | if (this.jsonEditor) { 231 | console.log('refreshJsonEditor'); 232 | if (forceReset) { 233 | this.jsonEditor.reset(true); 234 | } 235 | // Here we use update instead of set to preserve the editor status 236 | this.jsonEditor.update(this.form); 237 | this.jsonEditorChanged = false; 238 | } 239 | } 240 | 241 | // 242 | // Form Renderer Tab 243 | // 244 | 245 | resetFormRendererIfActive() { 246 | console.log('resetFormRenderer'); 247 | // Here we recreate the renderer component through *ngIf="activeTab='renderer'" 248 | // by changing the active tab and then restoring it. 249 | // Although this is a rather dirty hack it is hardly noticeable to the eye :) 250 | if (this.activeTab === 'renderer') { 251 | this.selectTab('builder'); 252 | setTimeout(() => this.selectTab('renderer')); 253 | } 254 | } 255 | 256 | showSubmissionPanel(submission: any) { 257 | this.submissionPanel = !this.options.renderer?.submissionPanel?.disabled; 258 | if (this.submissionPanel) { 259 | if (submission) { 260 | this.submission = submission; 261 | } 262 | setTimeout(() => { 263 | let jsonSchema: any = generateFormJsonSchema(this.form); 264 | if (this.fullSubmission) { 265 | // Wrap json schema moving definitions to the root 266 | const schemaDefs = jsonSchema.definitions; 267 | delete jsonSchema.definitions; 268 | jsonSchema = { definitions: schemaDefs, required: ['data'], type: 'object', properties: { data: jsonSchema } }; 269 | } 270 | this.rendererResourceJsonEditor.setSchema(undefined); 271 | this.rendererResourceJsonEditor.set(this.fullSubmission ? this.submission : this.submission.data); 272 | this.rendererSchemaJsonEditor.set(jsonSchema as JSON); 273 | this.rendererResourceJsonEditor.setSchema(jsonSchema); 274 | }); 275 | } 276 | } 277 | applyResourceJsonSchema() { 278 | const schema = this.rendererSchemaJsonEditor.get(); 279 | this.rendererResourceJsonEditor.setSchema(schema); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { FormioEditorComponent } from './formio-editor.component'; 5 | import { FormioModule } from 'angular-formio'; 6 | import { AlertModule } from 'ngx-bootstrap/alert'; 7 | import { ModalModule } from 'ngx-bootstrap/modal'; 8 | import { TabsModule } from 'ngx-bootstrap/tabs'; 9 | import { JsonEditorComponent } from './json-editor/json-editor.component'; 10 | import { JsonChangePanelComponent } from './json-change-panel/json-change-panel.component'; 11 | 12 | 13 | @NgModule({ 14 | declarations: [FormioEditorComponent, JsonEditorComponent, JsonChangePanelComponent], 15 | imports: [ 16 | BrowserModule, 17 | FormioModule, 18 | FormsModule, 19 | AlertModule.forRoot(), 20 | ModalModule.forRoot(), 21 | TabsModule.forRoot() 22 | ], 23 | exports: [FormioEditorComponent] 24 | }) 25 | export class FormioEditorModule { } 26 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/_.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Form", 4 | "description": "Object containing a form.io form", 5 | "type": "object", 6 | "required": ["components"], 7 | "properties": { 8 | "display": { 9 | "title": "Display mode", 10 | "description": "The given name.", 11 | "enum": ["form", "wizard"] 12 | }, 13 | "components": { 14 | "$ref": "components" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component-conditional-show_loose.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Show", 4 | "description": "If the field should show if the condition is true.", 5 | "enum": [true, false, "true", "false", "", null] 6 | } 7 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component-conditional-show_strict.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Show", 4 | "description": "If the field should show if the condition is true.", 5 | "type": ["boolean", "null"] 6 | } 7 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component-conditional.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Conditional", 4 | "description": "Determines when this component should be added to the form for both processing and input.", 5 | "type": "object", 6 | "properties": { 7 | "show": { 8 | "$ref": "show", 9 | }, 10 | "when": { 11 | "title": "When", 12 | "description": "The field API key that it should compare its value against to determine if the condition is triggered.", 13 | "type": ["string", "null"] 14 | }, 15 | "eq": { 16 | "title": "Eq", 17 | "description": "The value that should be checked against the comparison component.", 18 | "type": "string" 19 | }, 20 | "json": { 21 | "title": "Json", 22 | "description": "The JSON Logic to determine if this component is conditionally available.", 23 | "type": "string" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component-logic-action.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Action", 4 | "description": "An action to perform when the logic is triggered", 5 | "required": ["type"], 6 | "type": "object", 7 | "properties": { 8 | "type": { 9 | "title": "Type", 10 | "description": "The type of the action.", 11 | "enum": ["property", "value"] 12 | } 13 | }, 14 | "allOf": [ 15 | { 16 | "if": { 17 | "properties": { "type": { "const": "property" } } 18 | }, 19 | "then": { 20 | "properties": { 21 | "property": { 22 | "title": "Property", 23 | "description": "The property action.", 24 | "required": ["type", "value"], 25 | "type": "object", 26 | "properties": { 27 | "type": { 28 | "title": "Property", 29 | "description": "The type of the property action (either 'boolean' or 'string').", 30 | "enum": ["boolean", "string"] 31 | }, 32 | "value": { 33 | "title": "Value", 34 | "description": "The path to the property on the component definition.", 35 | "type": "string" 36 | } 37 | } 38 | } 39 | }, 40 | "allOf": [ 41 | { 42 | "if": { 43 | "properties": { "type": { "const": "boolean" } } 44 | }, 45 | "then": { 46 | "properties": { 47 | "state": { 48 | "title": "Boolean State", 49 | "description": "Used if the type of the property action is boolean.", 50 | "type": "boolean" 51 | } 52 | } 53 | } 54 | }, 55 | { 56 | "if": { 57 | "properties": { "type": { "const": "string" } } 58 | }, 59 | "then": { 60 | "properties": { 61 | "text": { 62 | "title": "String Text", 63 | "description": "Used if the type of the property action is string.", 64 | "type": "string" 65 | } 66 | } 67 | } 68 | } 69 | ] 70 | } 71 | }, 72 | { 73 | "if": { 74 | "properties": { "type": { "const": "value" } } 75 | }, 76 | "then": { 77 | "properties": { 78 | "value": { 79 | "title": "Value", 80 | "description": "The javascript that returns the new value. It Will be evaluated with available variables of row, data, component and result (returned from trigger).", 81 | "type": "string" 82 | } 83 | } 84 | } 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component-logic-trigger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Trigger", 4 | "description": "Determines when the logic should be triggered", 5 | "type": "object", 6 | "required": ["type"], 7 | "properties": { 8 | "type": { 9 | "title": "Type", 10 | "description": "The type of the trigger.", 11 | "enum": ["simple", "javascript", "json", "event"] 12 | } 13 | }, 14 | "allOf": [ 15 | { 16 | "if": { 17 | "properties": { "type": { "const": "simple" } } 18 | }, 19 | "then": { 20 | "required": ["simple"], 21 | "properties": { 22 | "simple": { 23 | "title": "Simple", 24 | "description": "Defines a simple trigger.", 25 | "required": ["when", "eq", "show"], 26 | "type": "object", 27 | "properties": { 28 | "when": { 29 | "title": "When", 30 | "description": "The trigger field key.", 31 | "type": "string" 32 | }, 33 | "eq": { 34 | "title": "Eq", 35 | "description": "The value to equal.", 36 | "type": "string" 37 | }, 38 | "show": { 39 | "title": "Show", 40 | "description": "Whether to trigger or not when the value is equal.", 41 | "type": "boolean" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | { 49 | "if": { 50 | "properties": { "type": { "const": "javascript" } } 51 | }, 52 | "then": { 53 | "required": ["javascript"], 54 | "properties": { 55 | "javascript": { 56 | "title": "Javascript", 57 | "description": "Javascript logic.", 58 | "type": "string" 59 | } 60 | } 61 | } 62 | }, 63 | { 64 | "if": { 65 | "properties": { "type": { "const": "json" } } 66 | }, 67 | "then": { 68 | "required": ["json"], 69 | "properties": { 70 | "json": { 71 | "title": "Json", 72 | "description": "JSON Logic object that returns true or false.", 73 | "type": "object" 74 | } 75 | } 76 | } 77 | }, 78 | { 79 | "if": { 80 | "properties": { "type": { "const": "event" } } 81 | }, 82 | "then": { 83 | "required": ["event"], 84 | "properties": { 85 | "event": { 86 | "title": "Event", 87 | "description": "The name of the event that will trigger this logic.", 88 | "type": "string" 89 | } 90 | } 91 | } 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component-logic.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Logic", 4 | "description": "...", 5 | "type": "object", 6 | "required": ["trigger", "actions"], 7 | "properties": { 8 | "trigger": { 9 | "$ref": "trigger" 10 | }, 11 | "actions": { 12 | "title": "Actions", 13 | "description": "The actions to perform when the logic is triggered", 14 | "type": "array", 15 | "items": { 16 | "$ref": "action" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component_loose.ts: -------------------------------------------------------------------------------- 1 | import component from './component_strict'; 2 | 3 | export default Object.assign({}, component, { required: ['type'] }); 4 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/component_strict.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Component", 4 | "description": "Object containing a form.io component", 5 | "type": "object", 6 | "required": ["type", "key", "input"], 7 | "properties": { 8 | "type": { 9 | "title": "Component Type", 10 | "description": "The type of this component", 11 | "type": "string" 12 | }, 13 | "key": { 14 | "title": "Component Key", 15 | "description": "The API key for this component", 16 | "type": "string" 17 | }, 18 | "label": { 19 | "title": "Component Label", 20 | "description": "The HTML label to give this component", 21 | "type": "string" 22 | }, 23 | "placeholder": { 24 | "title": "Component Placeholder", 25 | "description": "The text to show in the input before they type", 26 | "type": "string" 27 | }, 28 | "input": { 29 | "title": "User Input?", 30 | "description": "Determines if this is an input from the user", 31 | "type": "boolean" 32 | }, 33 | "tableView": { 34 | "title": "Component TableView", 35 | "description": "Determines if this field will show in the data tables output", 36 | "type": "boolean" 37 | }, 38 | "multiple": { 39 | "title": "Component Multiple", 40 | "description": "If this field should collect multiple values, creating an array of values", 41 | "type": "boolean" 42 | }, 43 | "protected": { 44 | "title": "Component Protected", 45 | "description": "If the value of this field should be shown to the end user via API once it is saved", 46 | "type": "boolean" 47 | }, 48 | "prefix": { 49 | "title": "Component Prefix", 50 | "description": "The prefix text to put in front of the input", 51 | "type": "string" 52 | }, 53 | "suffix": { 54 | "title": "Component Suffix", 55 | "description": "The suffix text to put after the input", 56 | "type": "string" 57 | }, 58 | "defaultValue": { 59 | "title": "Default Value", 60 | "description": "The default value to provide to this component. Its type depends on the specific component" 61 | }, 62 | "clearOnHide": { 63 | "title": "Clear on Hide", 64 | "description": "If the value of this field should be cleared when it is conditionally hidden", 65 | "type": "boolean" 66 | }, 67 | "unique": { 68 | "title": "Unique", 69 | "description": "Validates if this field should be unique amongst other submissions in the same form", 70 | "type": "boolean" 71 | }, 72 | "persistent": { 73 | "title": "Persistent", 74 | "description": "Determines if the value of this field should be saved as persistent", 75 | "type": "boolean" 76 | }, 77 | "hidden": { 78 | "title": "Hidden", 79 | "description": "Determines if this field should be hidden from view by default. This can be overridden with the conditionals.", 80 | "type": "boolean" 81 | }, 82 | "validate": { 83 | "title": "Validate", 84 | "description": "Determines validation criteria for this component.", 85 | "type": "object", 86 | "properties": { 87 | "required": { 88 | "title": "Required", 89 | "description": "Specifies if the field is required.", 90 | "type": "boolean" 91 | }, 92 | "minLength": { 93 | "title": "Min Lenngth", 94 | "description": "For text input, this checks the minimum length of text for valid input.", 95 | "type": ["number", "string"] 96 | }, 97 | "maxLength": { 98 | "title": "Max Lenngth", 99 | "description": "For text input, this checks the maximum length of text for valid input.", 100 | "type": ["number", "string"] 101 | }, 102 | "pattern": { 103 | "title": "Pattern", 104 | "description": "For text input, this checks the text agains a Regular expression pattern.", 105 | "type": "string" 106 | }, 107 | "custom": { 108 | "title": "Custom", 109 | "description": "A custom javascript based validation or a JSON object for using JSON Logic.", 110 | "type": ["string", "object"] 111 | } 112 | } 113 | }, 114 | "conditional": { 115 | "$ref": "conditional" 116 | }, 117 | "errors": { 118 | "title": "Errors", 119 | "description": "Allows customizable errors to be displayed for each component when an error occurs.", 120 | "type": "object", 121 | "properties": { 122 | "required": { 123 | "title": "Required", 124 | "description": "Error message for error 'required'.", 125 | "type": "string" 126 | }, 127 | "min": { 128 | "title": "Min", 129 | "description": "Error message for error 'min'.", 130 | "type": "string" 131 | }, 132 | "max": { 133 | "title": "Min", 134 | "description": "Error message for error 'max'.", 135 | "type": "string" 136 | }, 137 | "minLength": { 138 | "title": "Min Length", 139 | "description": "Error message for error 'minLength'.", 140 | "type": "string" 141 | }, 142 | "maxLength": { 143 | "title": "Max Length", 144 | "description": "Error message for error 'maxLength'.", 145 | "type": "string" 146 | }, 147 | "invalid_email": { 148 | "title": "Invalid Email", 149 | "description": "Error message for error 'invalid_email'.", 150 | "type": "string" 151 | }, 152 | "invalid_date": { 153 | "title": "Invalid Date", 154 | "description": "Error message for error 'invalid_date'.", 155 | "type": "string" 156 | }, 157 | "pattern": { 158 | "title": "Pattern", 159 | "description": "Error message for error 'pattern'.", 160 | "type": "string" 161 | }, 162 | "custom": { 163 | "title": "Custom", 164 | "description": "Error message for error 'custom'.", 165 | "type": ["string", "object"] 166 | } 167 | } 168 | }, 169 | "logic": { 170 | "title": "Logic", 171 | "description": "Allows changing the component definition in reaction to data entered in a form. For example, changing a field to required, disabled or hidden when a value is entered.", 172 | "type": "array", 173 | "items": { 174 | "$ref": "logic" 175 | } 176 | } 177 | }, 178 | "allOf": [ 179 | { 180 | "if": { "properties": { "type": { "const": "columns" } } }, 181 | "then": { "$ref": "columns" } 182 | }, 183 | { 184 | "if": { "properties": { "type": { "const": "table" } } }, 185 | "then": { "$ref": "table" } 186 | }, 187 | { 188 | "if": { "properties": { "type": { "const": "tabs" } } }, 189 | "then": { "$ref": "tabs" }, 190 | "else": { 191 | "properties": { 192 | "components": { 193 | "$ref": "components" 194 | } 195 | } 196 | } 197 | } 198 | ] 199 | } 200 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/components.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "title": "Component List", 4 | "description": "The list of all components", 5 | "type": "array", 6 | "items": { 7 | "$ref": "component" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/components/columns.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "required": [ "columns" ], 4 | "not": { "required": [ "components"] }, 5 | "properties": { 6 | "columns": { 7 | "type": "array", 8 | "items": { 9 | "required": [ "components" ], 10 | "type": "object", 11 | "properties": { 12 | "components": { 13 | "$ref": "components" 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/components/table.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "required": [ "rows", "numRows", "numCols"], 4 | "not": { "required": [ "components"] }, 5 | "properties": { 6 | "numRows": { "type": "integer" }, 7 | "numCols": { "type": "integer" }, 8 | "rows": { 9 | "type": "array", 10 | "items": { 11 | "type": "array", 12 | "items": { 13 | "required": [ "components" ], 14 | "type": "object", 15 | "properties": { 16 | "components": { 17 | "$ref": "components" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/components/tabs.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:object-literal-key-quotes quotemark semicolon 2 | export default { 3 | "required": [ "components" ], 4 | "properties": { 5 | "components": { 6 | "type": "array", 7 | "items": { 8 | "type": "object", 9 | "required": ["key", "components"], 10 | "not": { "required": ["type"] }, 11 | "properties": { 12 | "key": { 13 | "title": "Component Key", 14 | "description": "The API key for this component", 15 | "type": "string" 16 | }, 17 | "label": { 18 | "title": "Component Label", 19 | "description": "The HTML label to give this component", 20 | "type": "string" 21 | }, 22 | "components": { 23 | "$ref": "components" 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/formio-json-schema/index.ts: -------------------------------------------------------------------------------- 1 | import schema from './_'; 2 | import components from './components'; 3 | import componentStrict from './component_strict'; 4 | import componentLoose from './component_loose'; 5 | import logic from './component-logic'; 6 | import trigger from './component-logic-trigger'; 7 | import action from './component-logic-action'; 8 | import conditional from './component-conditional'; 9 | import showStrict from './component-conditional-show_strict'; 10 | import showLoose from './component-conditional-show_loose'; 11 | import columns from './components/columns'; 12 | import table from './components/table'; 13 | import tabs from './components/tabs'; 14 | 15 | export const strict = { 16 | schema, 17 | schemaRefs: { 18 | columns, 19 | components, 20 | component: componentStrict, 21 | logic, 22 | trigger, 23 | action, 24 | conditional, 25 | show: showStrict, 26 | table, 27 | tabs 28 | } 29 | }; 30 | 31 | export const loose = { 32 | schema, 33 | schemaRefs: Object.assign({}, strict.schemaRefs, { 34 | component: componentLoose, 35 | show: showLoose 36 | }) 37 | }; 38 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/json-change-panel/json-change-panel.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 9 |
10 | 11 | Json is not well-formed. You cannot apply changes. 12 | 13 |
14 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/json-change-panel/json-change-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter} from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | // tslint:disable-next-line:component-selector 6 | selector: 'json-change-panel', 7 | templateUrl: './json-change-panel.component.html' 8 | }) 9 | export class JsonChangePanelComponent { 10 | @Input() applyDisabled: boolean; 11 | @Input() alertOpen: boolean; 12 | @Output() apply = new EventEmitter(); 13 | @Output() discard = new EventEmitter(); 14 | 15 | constructor() { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/json-editor/json-editor-shapes.ts: -------------------------------------------------------------------------------- 1 | 2 | export type JsonEditorMode = 'code' | 'form' | 'preview' | 'text' | 'tree' | 'view'; 3 | 4 | export const JSON_EDITOR_TREE_MODES: readonly JsonEditorMode[] = Object.freeze(['code', 'preview', 'text']); 5 | export const JSON_EDITOR_TEXT_MODES: readonly JsonEditorMode[] = Object.freeze(['form', 'tree', 'view']); 6 | 7 | export type JsonEditorNodePath = (string | number)[]; 8 | 9 | export interface JsonEditorTreeNode { 10 | field: string; 11 | path: JsonEditorNodePath; 12 | value?: string; 13 | } 14 | 15 | export interface JsonEditorError { 16 | path: JsonEditorNodePath; 17 | message: string; 18 | } 19 | 20 | export type JsonEditorValidationError = JsonEditorSchemaValidationError | JsonEditorParseError | JsonEditorCustomValidationError; 21 | 22 | export interface JsonEditorSchemaValidationError { 23 | type: 'validation'; 24 | data: any; 25 | dataPath: string; 26 | keyword: string; 27 | message: string; 28 | params: object; 29 | parentSchema: object; 30 | schema: object; 31 | schemaPath: string; 32 | } 33 | 34 | export interface JsonEditorParseError { 35 | type: 'error'; 36 | message: string; 37 | line: number; 38 | } 39 | 40 | export interface JsonEditorCustomValidationError { 41 | type: 'customValidation'; 42 | message: string; 43 | dataPath: string; 44 | } 45 | 46 | export interface JsonEditorTextPosition { 47 | row: number; 48 | column: number; 49 | } 50 | 51 | export interface JsonEditorTextSelection { 52 | start: JsonEditorTextPosition; 53 | end: JsonEditorTextPosition; 54 | text: string; 55 | } 56 | 57 | export interface JsonEditorSerializableNode { 58 | value: any; 59 | path: JsonEditorNodePath; 60 | } 61 | 62 | export interface JsonEditorSelection { 63 | start: JsonEditorSerializableNode; 64 | end: JsonEditorSerializableNode; 65 | } 66 | 67 | export interface JsonEditorEvent { 68 | type: string; 69 | target: HTMLElement; 70 | } 71 | 72 | export interface JsonEditorMenuItem { 73 | text?: string; 74 | title?: string; 75 | className?: string; 76 | click?: () => void; 77 | submenu?: JsonEditorMenuItem[]; 78 | submenuTitle?: string; 79 | type?: 'separator'; 80 | } 81 | 82 | export interface JsonEditorMenuNode { 83 | type: 'single' | 'multiple' | 'append'; 84 | path: JsonEditorNodePath; 85 | paths: JsonEditorNodePath[]; 86 | } 87 | 88 | export interface JsonEditorQueryOptions { 89 | filter?: { 90 | field: string | '@' 91 | relation: '==' | '!=' | '<' | '<=' | '>' | '>=' 92 | value: string 93 | }; 94 | sort?: { 95 | field: string | '@' 96 | direction: 'asc' | 'desc' 97 | }; 98 | projection?: { 99 | fields: string[] 100 | }; 101 | } 102 | 103 | interface JsonEditorAdditionalOptions { 104 | expandAll?: boolean; 105 | } 106 | 107 | /** 108 | * Interface for the native options of jsoneditor. For the documentation see 109 | * https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options 110 | */ 111 | interface JsonEditorNativeOptions { 112 | ace?: object; 113 | ajv?: object; 114 | onChange?: () => void; 115 | onChangeJSON?: (json: JSON) => void; 116 | onChangeText?: (json: string) => void; 117 | onClassName?: (node: JsonEditorTreeNode) => string | undefined; 118 | onEditable?: (node: JsonEditorTreeNode) => boolean | { field: boolean, value: boolean }; 119 | onError?: (error: Error) => void; 120 | onModeChange?: (newMode: JsonEditorMode, oldMode: JsonEditorMode) => void; 121 | onNodeName?: (node: { path: JsonEditorNodePath, type: 'object' | 'array', size: number }) => string | undefined; 122 | onValidate?: (json: object) => JsonEditorError[] | null | Promise; 123 | onValidationError?: (errors: JsonEditorValidationError[]) => void; 124 | onCreateMenu?: (items: JsonEditorMenuItem[], node: JsonEditorMenuNode) => JsonEditorMenuItem[]; 125 | onSelectionChange?: (start: JsonEditorSerializableNode, end: JsonEditorSerializableNode) => void; 126 | onTextSelectionChange?: (start: JsonEditorTextPosition, end: JsonEditorTextPosition, text: string) => void; 127 | onEvent?: (node: JsonEditorTreeNode, event: Event) => void; 128 | onFocus?: (event: JsonEditorEvent) => void; 129 | onBlur?: (event: JsonEditorEvent) => void; 130 | enableSort?: boolean; 131 | enableTransform?: boolean; 132 | escapeUnicode?: boolean; 133 | history?: boolean; 134 | indentation?: number; 135 | limitDragging?: boolean; 136 | mode?: JsonEditorMode; 137 | modes?: JsonEditorMode[]; 138 | name?: string; 139 | schema?: object; 140 | schemaRefs?: object; 141 | search?: boolean; 142 | sortObjectKeys?: boolean; 143 | templates?: object[]; 144 | theme?: string; 145 | language?: string; 146 | languages?: object; 147 | autocomplete?: object; 148 | mainMenuBar?: boolean; 149 | navigationBar?: boolean; 150 | statusBar?: boolean; 151 | colorPicker?: boolean; 152 | onColorPicker?: (parent: HTMLElement, color: any, onChange: (color: any) => void) => void; 153 | timestampTag?: boolean | ((node: JsonEditorTreeNode) => boolean); 154 | timestampFormat?: (node: JsonEditorTreeNode) => string | null; 155 | modalAnchor?: HTMLElement; 156 | popupAnchor?: HTMLElement; 157 | maxVisibleChilds?: number; 158 | createQuery?: (json: JSON, queryOptions: JsonEditorQueryOptions) => string; 159 | executeQuery?: (json: JSON, query: string) => JSON; 160 | queryDescription?: string; 161 | } 162 | 163 | export type JsonEditorOptions = JsonEditorNativeOptions & JsonEditorAdditionalOptions; 164 | 165 | // See https://stackoverflow.com/a/54308812 166 | type KeysEnum = { [P in keyof Required]: true }; 167 | 168 | function getKeys(kObj: KeysEnum) { 169 | return Object.freeze(Object.keys(kObj)); 170 | } 171 | 172 | export const JSON_EDITOR_ADDITIONAL_OPTIONS: readonly string[] = getKeys({ 173 | expandAll: true 174 | }); 175 | 176 | export const JSON_EDITOR_NATIVE_OPTIONS: readonly string[] = getKeys({ 177 | ace: true, 178 | ajv: true, 179 | onChange: true, 180 | onChangeJSON: true, 181 | onChangeText: true, 182 | onClassName: true, 183 | onEditable: true, 184 | onError: true, 185 | onModeChange: true, 186 | onNodeName: true, 187 | onValidate: true, 188 | onValidationError: true, 189 | onCreateMenu: true, 190 | onSelectionChange: true, 191 | onTextSelectionChange: true, 192 | onEvent: true, 193 | onFocus: true, 194 | onBlur: true, 195 | enableSort: true, 196 | enableTransform: true, 197 | escapeUnicode: true, 198 | history: true, 199 | indentation: true, 200 | limitDragging: true, 201 | mode: true, 202 | modes: true, 203 | name: true, 204 | schema: true, 205 | schemaRefs: true, 206 | search: true, 207 | sortObjectKeys: true, 208 | templates: true, 209 | theme: true, 210 | language: true, 211 | languages: true, 212 | autocomplete: true, 213 | mainMenuBar: true, 214 | navigationBar: true, 215 | statusBar: true, 216 | colorPicker: true, 217 | onColorPicker: true, 218 | timestampTag: true, 219 | timestampFormat: true, 220 | modalAnchor: true, 221 | popupAnchor: true, 222 | maxVisibleChilds: true, 223 | createQuery: true, 224 | executeQuery: true, 225 | queryDescription: true 226 | }); 227 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/json-editor/json-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { JsonEditorComponent } from './json-editor.component'; 4 | 5 | describe('JsonEditorComponent', () => { 6 | let component: JsonEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ JsonEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(JsonEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/json-editor/json-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef, Input, Output, EventEmitter, OnDestroy } from '@angular/core'; 2 | import JsonEditor from 'jsoneditor'; 3 | import { 4 | JsonEditorOptions, JsonEditorMode, JsonEditorSelection, JsonEditorValidationError, 5 | JsonEditorTextPosition, JsonEditorTextSelection, JsonEditorSerializableNode, 6 | JSON_EDITOR_NATIVE_OPTIONS, JSON_EDITOR_ADDITIONAL_OPTIONS, JSON_EDITOR_TREE_MODES 7 | } from './json-editor-shapes'; 8 | 9 | // Check unsupported options 10 | let unsupportedOptions = JsonEditor.VALID_OPTIONS.filter(p => !JSON_EDITOR_NATIVE_OPTIONS.includes(p)); 11 | if (unsupportedOptions.length > 0) { 12 | console.log('You\'re probably using a recent version of jsoneditor and the following options are not yet defined in JsonEditorNativeOptions', unsupportedOptions); 13 | console.log('However, you can still pass these options using TypeScript type assertion \'as JsonEditorOptions\''); 14 | } 15 | unsupportedOptions = JSON_EDITOR_NATIVE_OPTIONS.filter(p => !JsonEditor.VALID_OPTIONS.includes(p)); 16 | if (unsupportedOptions.length > 0) { 17 | console.log('You\'re probably using an old version of jsoneditor that doesn\'t support the following options', unsupportedOptions); 18 | } 19 | 20 | @Component({ 21 | // tslint:disable-next-line:component-selector 22 | selector: 'json-editor', 23 | template: `
` 24 | }) 25 | export class JsonEditorComponent implements OnInit, OnDestroy { 26 | private editor: any; 27 | public id = 'jsoneditor' + Math.floor(Math.random() * 1000000); 28 | 29 | @ViewChild('jsonEditorContainer', { static: true }) jsonEditorContainer: ElementRef; 30 | 31 | // tslint:disable-next-line:variable-name 32 | private _options: JsonEditorOptions; 33 | get options() { return this._options; } 34 | @Input() set options(options: JsonEditorOptions) { this.createEditor(options); } 35 | 36 | @Output() dataChange: EventEmitter = new EventEmitter(); 37 | @Output() dataError: EventEmitter = new EventEmitter(); 38 | 39 | constructor() { } 40 | 41 | ngOnInit() { 42 | if (!this.editor) { 43 | this.createEditor({}); // creates the editor with default options 44 | } 45 | } 46 | 47 | ngOnDestroy(): void { 48 | if (this.editor) { 49 | this.editor.destroy(); 50 | this.editor = undefined; 51 | } 52 | } 53 | 54 | private createEditor(options: JsonEditorOptions, mode?: JsonEditorMode) { 55 | // Store original options passed in 56 | this._options = options; 57 | 58 | // Create actual options for the editor 59 | const patchedOptions: JsonEditorOptions = { 60 | onChange: this.onChangeData.bind(this), 61 | onValidationError: this.onValidationError.bind(this) 62 | }; 63 | const editorOptions = Object.assign({}, options, patchedOptions); 64 | 65 | // Extract additional options not supported by the original jsoneditor 66 | const additionalOptions: JsonEditorOptions = JSON_EDITOR_ADDITIONAL_OPTIONS.reduce((opts, k) => { 67 | opts[k] = editorOptions[k]; 68 | delete editorOptions[k]; 69 | return opts; 70 | }, {}); 71 | 72 | // (Re)create the editor 73 | if (!this.jsonEditorContainer.nativeElement) { 74 | console.error(`Can't find the ElementRef reference for jsoneditor)`); 75 | } 76 | if (this.editor) { 77 | this.editor.destroy(); 78 | } 79 | this.editor = new JsonEditor(this.jsonEditorContainer.nativeElement, editorOptions, {}); 80 | 81 | if (mode) { 82 | this.setMode(mode); 83 | } 84 | 85 | if (additionalOptions.expandAll && JSON_EDITOR_TREE_MODES.includes(this.getMode())) { 86 | this.editor.expandAll(); 87 | } 88 | } 89 | 90 | public onChangeData(e: any) { 91 | if (this.editor) { 92 | this.dataChange.emit(this.editor.get()); 93 | if (this.options.onChange) { 94 | this.options.onChange(); 95 | } 96 | } 97 | } 98 | 99 | public onValidationError(errors: JsonEditorValidationError[]) { 100 | if (this.editor) { 101 | this.dataError.emit(errors); 102 | if (this.options.onValidationError) { 103 | this.options.onValidationError(errors); 104 | } 105 | } 106 | } 107 | 108 | public reset(preserveMode = false) { 109 | const mode = preserveMode ? this.getMode() : undefined; 110 | this.createEditor(this.options, mode); 111 | } 112 | 113 | public isWellFormedJson() { 114 | try { 115 | JSON.parse(this.getText()); 116 | return true; 117 | } catch (e) { 118 | return false; 119 | } 120 | } 121 | 122 | /** 123 | * JSON EDITOR FUNCTIONS 124 | */ 125 | 126 | public collapseAll() { 127 | this.editor.collapseAll(); 128 | } 129 | 130 | public destroy() { 131 | this.editor.destroy(); 132 | } 133 | 134 | public expandAll() { 135 | this.editor.expandAll(); 136 | } 137 | 138 | public focus() { 139 | this.editor.focus(); 140 | } 141 | 142 | public get(): JSON { 143 | return this.editor.get(); 144 | } 145 | 146 | public getMode(): JsonEditorMode { 147 | return this.editor.getMode(); 148 | } 149 | 150 | public getName(): string { 151 | return this.editor.getName(); 152 | } 153 | 154 | public getText(): string { 155 | return this.editor.getText(); 156 | } 157 | 158 | public getSelection(): JsonEditorSelection { 159 | return this.editor.getSelection(); 160 | } 161 | 162 | public getTextSelection(): JsonEditorTextSelection { 163 | return this.editor.getSelection(); 164 | } 165 | 166 | public getValidateSchema(): any { 167 | return this.editor.validateSchema; 168 | } 169 | 170 | public refresh() { 171 | this.editor.refresh(); 172 | } 173 | 174 | public set(json: JSON) { 175 | this.editor.set(json); 176 | } 177 | 178 | public setMode(mode: JsonEditorMode) { 179 | this.editor.setMode(mode); 180 | } 181 | 182 | public setName(name: string) { 183 | this.editor.setName(name); 184 | } 185 | 186 | public setSchema(schema: any, schemaRefs?: any) { 187 | this.editor.setSchema(schema, schemaRefs); 188 | } 189 | 190 | public setSelection(start: JsonEditorSerializableNode, end: JsonEditorSerializableNode) { 191 | this.editor.setSelection(start, end); 192 | } 193 | 194 | public setTextSelection(start: JsonEditorTextPosition, end: JsonEditorTextPosition) { 195 | this.editor.setSelection(start, end); 196 | } 197 | 198 | public search(query: string): any[] { 199 | return this.editor.search(query); 200 | } 201 | 202 | public update(json: JSON) { 203 | this.editor.update(json); 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /projects/formio-editor/src/lib/resource-json-schema/json-schema-generator.js: -------------------------------------------------------------------------------- 1 | const { hasOwnProperty } = Object.prototype; 2 | 3 | // ------------------------------- 4 | // SCHEMAS 5 | // ------------------------------- 6 | 7 | class JsonSchemaBuilder { 8 | constructor(component, rootJSB) { 9 | this.component = component; 10 | this.conditions = []; 11 | this.required = false; 12 | this.dataType = {}; 13 | // if (!rootJSB) { 14 | // this.definitions = {}; 15 | // } 16 | if (component && component.formioComponent) { 17 | if (component.formioComponent.conditional) { 18 | // Add non-empty condition from the component 19 | const cnd = component.formioComponent.conditional; 20 | if (typeof cnd.show === 'boolean' && cnd.when) { 21 | // console.log(component.formioComponent.type, "Pushing condition") 22 | const c = { show: cnd.show, when: cnd.when, eq: cnd.eq }; 23 | c.key = this.generateSingleConditionKey(c); 24 | this.conditions.push(c); 25 | } 26 | } 27 | if (component.formioComponent.validate) { 28 | this.required = component.formioComponent.validate.required; 29 | } 30 | } 31 | } 32 | addDefinition(name, jsb) { 33 | this.definitions = this.definitions || {}; // Make sure definitions is an object 34 | this.definitions[name] = jsb; 35 | return this; 36 | } 37 | /*private*/ generateSingleConditionKey(condition) { 38 | return JSON.stringify(condition); 39 | } 40 | /*private*/ generateConditionsKey() { 41 | return JSON.stringify(this.conditions, (k, v) => k === 'key' ? undefined : v); 42 | } 43 | prepareConditions() { 44 | // Ensure the array has unique conditions 45 | this.conditions = this.conditions.sort((c1, c2) => c1.key.compare(c2.key)) 46 | .filter((x, i, a) => i === 0 || x.key !== a[i - 1].key); 47 | this.conditions.key = this.generateConditionsKey(); 48 | } 49 | shrink() { 50 | for (const k in this.properties) { 51 | if (hasOwnProperty.call(this.properties, k)) { 52 | const propSchema = this.properties[k]; 53 | if (propSchema instanceof MergeableObjectJSB && propSchema.component && propSchema.component.shrinkable()) { 54 | // console.log('Shrink', propSchema.component.formioComponent.type); 55 | propSchema.shrink(); 56 | delete this.properties[k]; // Remove shribkable schema from parent 57 | this.merge(propSchema); // merge its properties and conditions with parent 58 | } 59 | } 60 | } 61 | return this; 62 | } 63 | // Subclasses overriding this method MUST call super.build() 64 | build() { 65 | // Start creating json schema from builder's dataType 66 | const jsonSchema = Object.assign({}, this.dataType); 67 | 68 | // Compile builder's definitions (if any) 69 | if (this.definitions) { 70 | const defKeys = Object.keys(this.definitions); 71 | if (defKeys.length > 0) { 72 | jsonSchema.definitions = defKeys.reduce((defs, dk) => { 73 | defs[dk] = this.definitions[dk].build(); 74 | return defs; 75 | }, {}); 76 | } 77 | } 78 | return jsonSchema; 79 | } 80 | // Convert the specified string value to the type of this json schema 81 | fromString(val) { 82 | try { 83 | return JSON.parse(val); 84 | } catch (e) { 85 | return val; 86 | } 87 | } 88 | } 89 | class ObjectJSB extends JsonSchemaBuilder { 90 | constructor(component, rootJSB) { 91 | super(component, rootJSB); 92 | this.dataType.type = 'object'; 93 | this.properties = {}; 94 | } 95 | addProperty(name, jsb, required) { 96 | this.properties[name] = jsb; 97 | jsb.required = required; 98 | return this; 99 | } 100 | /*private*/ splitProperties() { 101 | const unconditionalProperties = {}; 102 | const condPropMap = {}; 103 | const conditionsMap = {}; 104 | for (const pk in this.properties) { 105 | if (hasOwnProperty.call(this.properties, pk)) { 106 | const childJSB = this.properties[pk]; 107 | if (childJSB.conditions.length === 0) { 108 | // Unconditional property 109 | unconditionalProperties[pk] = childJSB; 110 | continue; 111 | } 112 | // Conditional property 113 | childJSB.prepareConditions(); 114 | const ck = childJSB.conditions.key 115 | conditionsMap[ck] = childJSB.conditions; 116 | if (!(ck in condPropMap)) { 117 | condPropMap[ck] = {}; 118 | } 119 | condPropMap[ck][pk] = childJSB; 120 | } 121 | } 122 | return { unconditionalProperties, condPropMap, conditionsMap }; 123 | } 124 | /*private*/ buildConditionalProperties(condPropMap, conditionsMap, unconditionalProperties) { 125 | // Generate allOf from conditional properties 126 | const allOf = []; 127 | for (const ck in condPropMap) { 128 | if (hasOwnProperty.call(condPropMap, ck)) { 129 | const conds = conditionsMap[ck]; 130 | let condPropCounter = 0; 131 | const _if = { 132 | properties: conds.reduce((acc, c) => { 133 | const whenProp = this.properties[c.when]; 134 | if (whenProp) { 135 | condPropCounter++; 136 | const eq = whenProp.fromString(c.eq); 137 | acc[c.when] = c.show ? { const: eq } : { not: { const: eq } }; 138 | } 139 | return acc; 140 | }, {}) 141 | }; 142 | if (condPropCounter === 0) { 143 | // Move these conditional properties back to unconditional properties 144 | Object.assign(unconditionalProperties, condPropMap[ck]); 145 | delete condPropMap[ck]; 146 | continue; 147 | } 148 | const then = { required: [], properties: {} }; 149 | for (const pk in condPropMap[ck]) { 150 | if (hasOwnProperty.call(condPropMap[ck], pk)) { 151 | const childJSB = condPropMap[ck][pk]; 152 | if (childJSB.required) { 153 | then.required.push(pk); 154 | } 155 | then.properties[pk] = childJSB.build(); 156 | } 157 | } 158 | // Remove empty required 159 | if (then.required.length === 0) { 160 | delete then.required; 161 | } 162 | // Add if/then to allOf 163 | allOf.push({ if: _if, then: then }); 164 | } 165 | } 166 | return allOf; 167 | } 168 | /*private*/ buildUnconditionalProperties(unconditionalProperties) { 169 | const out = { required: [], properties: {} }; 170 | for (const pk in unconditionalProperties) { 171 | if (hasOwnProperty.call(unconditionalProperties, pk)) { 172 | const childJSB = unconditionalProperties[pk]; 173 | if (childJSB.required) { 174 | out.required.push(pk); 175 | } 176 | out.properties[pk] = childJSB.build(); 177 | } 178 | } 179 | return out; 180 | } 181 | build() { 182 | const jsonSchema = super.build(); 183 | 184 | // Split conditional and unconditional properties 185 | const { condPropMap, conditionsMap, unconditionalProperties } = this.splitProperties(); 186 | 187 | // Generate allOf from conditional properties. 188 | // Note that we have to process conditional properties first, because some 189 | // of them may still be moved back to unconditional properties for some reason. 190 | const allOf = this.buildConditionalProperties(condPropMap, conditionsMap, unconditionalProperties); 191 | 192 | // Build unconditional properties and add them to the json schema 193 | const builtUncondProps = this.buildUnconditionalProperties(unconditionalProperties); 194 | if (builtUncondProps.required.length > 0) { 195 | jsonSchema.required = builtUncondProps.required; // Add non-empty required to the json schema 196 | } 197 | jsonSchema.properties = builtUncondProps.properties; 198 | 199 | // Add not empty allOf (with conditional properties) to the json schema 200 | if (allOf.length > 0) { 201 | jsonSchema.allOf = allOf; 202 | } 203 | 204 | return jsonSchema; 205 | } 206 | } 207 | class MergeableObjectJSB extends ObjectJSB { 208 | constructor(component, rootJSB) { 209 | super(component, rootJSB); 210 | } 211 | merge(...sources) { 212 | const targetProps = this.properties; 213 | for (let i = 0, len = sources.length; i < len; i++) { 214 | const source = sources[i]; 215 | if (source instanceof MergeableObjectJSB) { 216 | // merge properties 217 | const sourceProps = source.properties; 218 | for (const key in sourceProps) { 219 | if (hasOwnProperty.call(sourceProps, key)) { 220 | // Append source schema conditions to the conditions of its sub-schemas 221 | Array.prototype.push.apply(sourceProps[key].conditions, source.conditions); 222 | // Merge properties recursively 223 | if (targetProps[key] && sourceProps[key] instanceof MergeableObjectJSB) { 224 | targetProps[key].merge(sourceProps[key]); 225 | } else { 226 | targetProps[key] = sourceProps[key]; 227 | } 228 | } 229 | } 230 | } 231 | } 232 | return this; 233 | } 234 | } 235 | class ArrayJSB extends JsonSchemaBuilder { 236 | constructor(component, items, rootJSB) { 237 | super(component, rootJSB); 238 | this.dataType.type = 'array'; 239 | this.dataType.items = items; 240 | } 241 | build() { 242 | const jsonSchema = super.build(); 243 | jsonSchema.items = this.dataType.items.build(); 244 | return jsonSchema; 245 | } 246 | } 247 | class PrimitiveJSB extends JsonSchemaBuilder { 248 | constructor(component, type, rootJSB) { 249 | super(component, rootJSB); 250 | this.dataType.type = type; 251 | } 252 | } 253 | class BooleanJSB extends PrimitiveJSB { 254 | constructor(component, rootJSB) { 255 | super(component, 'boolean', rootJSB); 256 | } 257 | } 258 | class NumberJSB extends PrimitiveJSB { 259 | constructor(component, rootJSB) { 260 | super(component, 'number', rootJSB); 261 | if (component && component.formioComponent && component.formioComponent.validate) { 262 | const validate = component.formioComponent.validate; 263 | if (validate.min || typeof validate.min === 'number') this.dataType.minimum = Number(validate.min); 264 | if (validate.max || typeof validate.max === 'number') this.dataType.maximum = Number(validate.max); 265 | if (validate.integer) this.dataType.type = 'integer'; 266 | } 267 | } 268 | } 269 | class StringJSB extends PrimitiveJSB { 270 | constructor(component, rootJSB) { 271 | super(component, 'string', rootJSB); 272 | if (component && component.formioComponent && component.formioComponent.validate) { 273 | const validate = component.formioComponent.validate; 274 | if (validate.minLength) this.dataType.minLength = Number(validate.minLength); 275 | if (validate.maxLength) this.dataType.maxLength = Number(validate.maxLength); 276 | if (validate.pattern) this.dataType.pattern = validate.pattern; 277 | } 278 | } 279 | fromString(val) { 280 | return val; 281 | } 282 | } 283 | 284 | class EnumJSB extends JsonSchemaBuilder { 285 | constructor(component, values, rootJSB) { 286 | super(component, rootJSB); 287 | this.dataType.enum = values; 288 | } 289 | fromString(val) { 290 | if (!this.dataType.enum.includes(val)) { 291 | const v = super.fromString(val); 292 | if (v !== val && this.dataType.enum.includes(v)) { 293 | return v; 294 | } 295 | } 296 | return val; 297 | } 298 | } 299 | 300 | // ------------------------------- 301 | // COMPONENT BASE CLASSES 302 | // ------------------------------- 303 | 304 | class Component { 305 | constructor(formioComponent) { 306 | this.formioComponent = formioComponent; 307 | } 308 | jsonSchemaBuilder(rootJSB) { 309 | throw new Error('Subclasses of \'Component\' have to implement the method \'jsonSchemaBuilder\'!'); 310 | } 311 | // Layout components are view-only components. From resource perspective, they are to be 312 | // shrinked, because they don't have any value neither implicit nor expressed by user. 313 | // So they don't contribute to the underlying resource because their 'API key' does not 314 | // match any field inside the resource itself. 315 | // Howewer, shrink process propagates component's condition (show/when/eq) to child components. 316 | shrinkable() { 317 | return !(this.formioComponent && this.formioComponent.input); 318 | } 319 | } 320 | 321 | class AtomicComponent extends Component { 322 | jsonSchemaBuilder(rootJSB) { 323 | const jsb = this.baseJsonSchemaBuilder(); 324 | if (this.formioComponent.multiple) { 325 | if (this.formioComponent.validate && !this.formioComponent.validate.required) { 326 | // With multiple values enabled the component can generate null items if required is false 327 | jsb.dataType = { anyOf: [jsb.dataType, { type: 'null' }] }; 328 | } 329 | return new ArrayJSB(this, jsb, rootJSB); 330 | } 331 | return jsb; 332 | } 333 | baseJsonSchemaBuilder() { 334 | throw new Error('Subclasses of \'AtomicComponent\' have to implement the method \'baseJsonSchemaBuilder\'!'); 335 | } 336 | isDefaultCastToString() { 337 | return false; // cast defaults to 'auto' 338 | } 339 | cast(val, to) { 340 | switch (to) { 341 | case 'boolean': 342 | return val.toLowerCase() === 'true'; 343 | case 'number': 344 | return Number(val); 345 | case 'object': 346 | return JSON.parse(val); 347 | case 'string': 348 | return val; 349 | case 'auto': 350 | default: // Either autotype or string 351 | if (to !== 'auto' && this.isDefaultCastToString()) return val; 352 | if (val === "true") return true; 353 | if (val === "false") return false; 354 | if (val === "") return val; 355 | const v = Number(val); 356 | return isNaN(v) ? val : v; 357 | } 358 | } 359 | } 360 | 361 | class CompoundComponent extends Component { 362 | jsonSchemaBuilder(rootJSB) { 363 | const jsb = new MergeableObjectJSB(this, rootJSB); 364 | this.childrenJsonSchemaBuilder(jsb, rootJSB); 365 | return jsb.shrink(); 366 | } 367 | /*prorected*/ children() { 368 | return this.formioComponent.components; 369 | } 370 | // Subclasses can override this method to provide a default class 371 | // for children that don't have a type 372 | /*prorected*/ defaultChildClass() { 373 | return undefined; 374 | } 375 | /*prorected*/ childrenJsonSchemaBuilder(parentJSB, rootJSB) { 376 | const children = this.children(); 377 | for (let i = 0, len = children.length; i < len; i++) { 378 | const c = children[i]; 379 | // console.log(this.formioComponent.type, 'child', c) 380 | if (c.persistent === 'client-only') { 381 | // console.log(c.type, ': skipped because persistent ===', c.persistent); 382 | continue; 383 | } 384 | const type = MAP[c.type] || this.defaultChildClass(); 385 | if (type) { 386 | let jsb = new (type)(c).jsonSchemaBuilder(rootJSB); 387 | const required = c.validate && c.validate.required; 388 | // Dotted key means nested schema 389 | const keyParts = c.key.split('.'); 390 | for (let j = keyParts.length - 1; j > 0; j--) { 391 | jsb = new MergeableObjectJSB(undefined).addProperty(keyParts[j], jsb, required); 392 | } 393 | parentJSB.merge(new MergeableObjectJSB(undefined).addProperty(keyParts[0], jsb, required)) 394 | } 395 | else { 396 | // console.log(this.formioComponent.type, ": skipping child with unknown type", c.type); 397 | } 398 | } 399 | return parentJSB; 400 | } 401 | } 402 | 403 | // ------------------------------- 404 | // ABSTRACT COMPONENT CLASSES 405 | // ------------------------------- 406 | 407 | class StringComponent extends AtomicComponent { 408 | baseJsonSchemaBuilder() { 409 | return new StringJSB(this); 410 | } 411 | } 412 | class EnumComponent extends AtomicComponent { 413 | constructor(formioComponent, ...additionalValuesIfNotRequired) { 414 | super(formioComponent); 415 | this.additionalValuesIfNotRequired = additionalValuesIfNotRequired; 416 | } 417 | // This is needed because different components take values from different path 418 | values() { 419 | throw new Error('Subclasses of \'EnumComponent\' have to implement the method \'values\'!'); 420 | } 421 | baseJsonSchemaBuilder() { 422 | const values = this.values().map(v => this.cast(v.value, this.formioComponent.dataType)); 423 | if (this.formioComponent && this.formioComponent.validate && !this.formioComponent.validate.required) { 424 | Array.prototype.push.apply(values, this.additionalValuesIfNotRequired); 425 | } 426 | return new EnumJSB(this, values); 427 | } 428 | } 429 | 430 | // ------------------------------- 431 | // BASIC COMPONENTS 432 | // ------------------------------- 433 | 434 | class CheckboxComponent extends AtomicComponent { 435 | baseJsonSchemaBuilder() { 436 | return new BooleanJSB(this); 437 | } 438 | } 439 | 440 | class NumberComponent extends AtomicComponent { 441 | baseJsonSchemaBuilder() { 442 | return new NumberJSB(this); 443 | } 444 | } 445 | 446 | class PasswordComponent extends StringComponent {} 447 | 448 | class RadioComponent extends EnumComponent { 449 | constructor(formioComponent) { 450 | // Empty string and null are valid values if the component is not required 451 | super(formioComponent, '', null); 452 | } 453 | values() { 454 | return this.formioComponent.values; 455 | } 456 | } 457 | 458 | class SelectComponent extends EnumComponent { 459 | constructor(formioComponent) { 460 | // Empty string and null are valid values if the component is not required 461 | super(formioComponent, '', null); 462 | } 463 | values() { 464 | return this.formioComponent.data.values; 465 | } 466 | jsonSchemaBuilder() { 467 | const jsb = super.jsonSchemaBuilder(); 468 | // If multiple values are enabled ensure uniqueness 469 | if (jsb instanceof ArrayJSB) { 470 | jsb.dataType.uniqueItems = true; 471 | } 472 | return jsb; 473 | } 474 | // This has changed with formio 4.10.x used by angular-formio 4.8.x 475 | // Now cast defaults to 'auto' 476 | // isDefaultCastToString() { 477 | // return true; // cast defaults to 'string' 478 | // } 479 | } 480 | 481 | class SelectBoxesComponent extends AtomicComponent { 482 | baseJsonSchemaBuilder() { 483 | const jsb = new ObjectJSB(this); 484 | jsb.dataType.additionalProperties = false; 485 | const values = this.formioComponent.values 486 | .forEach(v => jsb.addProperty(v.value, new BooleanJSB(undefined), true)); 487 | if (this.formioComponent.validate && !this.formioComponent.validate.required) { 488 | // This is needed for compatibility. 489 | // Formio adds a boolean property with name "" when the component is not required. 490 | // The property itself must not be required 491 | jsb.addProperty('', new BooleanJSB(undefined), false); 492 | } 493 | return jsb; 494 | } 495 | } 496 | 497 | class TextAreaComponent extends StringComponent {} 498 | 499 | class TextFieldComponent extends StringComponent {} 500 | 501 | // ------------------------------- 502 | // ADVANCED COMPONENTS 503 | // ------------------------------- 504 | 505 | class DateTimeComponent extends StringComponent {} 506 | class EmailComponent extends StringComponent {} 507 | class UrlComponent extends StringComponent {} 508 | 509 | class TagsComponent extends AtomicComponent { 510 | jsonSchemaBuilder() { 511 | return this.formioComponent.storeas === 'array' ? 512 | new ArrayJSB(this, this.baseJsonSchemaBuilder()) 513 | : this.baseJsonSchemaBuilder(); 514 | } 515 | baseJsonSchemaBuilder() { 516 | return new StringJSB(this); 517 | } 518 | } 519 | 520 | // ------------------------------- 521 | // LAYOUT COMPONENTS 522 | // ------------------------------- 523 | 524 | class ColumnsComponent extends CompoundComponent { 525 | // Determines children from columns. 526 | // Children are calculated lazily and cached into this instance. 527 | children() { 528 | if (!this.components) { 529 | this.components = []; 530 | this.formioComponent.columns.forEach(col => Array.prototype.push.apply(this.components, col.components)); 531 | } 532 | return this.components; 533 | } 534 | } 535 | 536 | class ContentComponent extends CompoundComponent { 537 | children() { 538 | return []; // This component never has children 539 | } 540 | } 541 | class FieldSetComponent extends CompoundComponent {} 542 | class PanelComponent extends CompoundComponent {} 543 | 544 | class TableComponent extends CompoundComponent { 545 | // Determines children from table's rows and columns. 546 | // Children are calculated lazily and cached into this instance. 547 | children() { 548 | if (!this.components) { 549 | this.components = []; 550 | this.formioComponent.rows.forEach(row => { 551 | row.forEach(col => Array.prototype.push.apply(this.components, col.components)); 552 | }); 553 | } 554 | return this.components; 555 | } 556 | } 557 | class TabsComponent extends CompoundComponent { 558 | /*prorected*/ defaultChildClass() { 559 | return CompoundComponent; // Needed because children don't have a type :( 560 | 561 | } 562 | } 563 | 564 | class WellComponent extends CompoundComponent {} 565 | 566 | // ------------------------------- 567 | // DATA COMPONENTS 568 | // ------------------------------- 569 | 570 | class ContainerComponent extends CompoundComponent {} 571 | 572 | class DataGridComponent extends CompoundComponent { 573 | jsonSchemaBuilder(rootJSB) { 574 | return new ArrayJSB(this, super.jsonSchemaBuilder(rootJSB)); 575 | } 576 | } 577 | 578 | class EditGridComponent extends CompoundComponent { 579 | jsonSchemaBuilder(rootJSB) { 580 | return new ArrayJSB(this, super.jsonSchemaBuilder(rootJSB)); 581 | } 582 | } 583 | 584 | /* 585 | definitions: { 586 | tree_1234567890: { 587 | type: 'object', 588 | properties: { 589 | data: { 590 | type: 'object', 591 | properties: { 592 | item: {type: 'string'}, 593 | price: {type: 'number'}, 594 | } 595 | }, 596 | children: { 597 | type: 'array', 598 | items: { $ref: "#/definitions/tree_1234567890"} 599 | } 600 | } 601 | } 602 | }, 603 | $ref: "#/definitions/tree_1234567890"" 604 | */ 605 | class TreeComponent extends CompoundComponent { 606 | jsonSchemaBuilder(rootJSB) { 607 | return new TreeJSB(this, super.jsonSchemaBuilder(rootJSB), rootJSB); 608 | } 609 | } 610 | class RefJSB extends JsonSchemaBuilder { 611 | constructor(component, ref, rootJSB) { 612 | super(component, rootJSB); 613 | this.dataType.$ref = ref; 614 | } 615 | } 616 | class TreeJSB extends RefJSB { 617 | constructor(component, dataJSB, rootJSB) { 618 | super(component, 'tree_' + Math.floor(Math.random() * 1000000), rootJSB); 619 | const schemaId = this.dataType.$ref; 620 | this.dataType.$ref = "#/definitions/" + schemaId; 621 | 622 | const treeJSB = new ObjectJSB(component) 623 | .addProperty('data', dataJSB, true) 624 | .addProperty('children', new ArrayJSB(component, new RefJSB(undefined, this.dataType.$ref)), true); 625 | 626 | rootJSB.addDefinition(schemaId, treeJSB); 627 | } 628 | } 629 | 630 | // ------------------------------- 631 | // FORM COMPONENT 632 | // ------------------------------- 633 | class FormComponent extends CompoundComponent { 634 | /*prorected*/ childrenJsonSchemaBuilder(parentJSB, rootJSB) { 635 | // For form's children parent and root are the same 636 | return super.childrenJsonSchemaBuilder(parentJSB, parentJSB); 637 | } 638 | } 639 | 640 | const MAP = { 641 | checkbox: CheckboxComponent, 642 | columns: ColumnsComponent, 643 | container: ContainerComponent, 644 | content: ContentComponent, 645 | datagrid: DataGridComponent, 646 | datetime: DateTimeComponent, 647 | editgrid: EditGridComponent, 648 | email: EmailComponent, 649 | fieldset: FieldSetComponent, 650 | number: NumberComponent, 651 | password: PasswordComponent, 652 | panel: PanelComponent, 653 | radio: RadioComponent, 654 | select: SelectComponent, 655 | selectboxes: SelectBoxesComponent, 656 | table: TableComponent, 657 | tabs: TabsComponent, 658 | tags: TagsComponent, 659 | textarea: TextAreaComponent, 660 | textfield: TextFieldComponent, 661 | tree: TreeComponent, 662 | url: UrlComponent, 663 | well: WellComponent 664 | }; 665 | 666 | export function generateFormJsonSchema(form) { 667 | return new FormComponent(form).jsonSchemaBuilder().build(); 668 | } -------------------------------------------------------------------------------- /projects/formio-editor/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of formio-editor 3 | */ 4 | 5 | export * from './lib/formio-editor.component'; 6 | export * from './lib/formio-editor.module'; 7 | export * from './lib/formio-editor-options'; 8 | export * from './lib/json-editor/json-editor-shapes'; 9 | -------------------------------------------------------------------------------- /projects/formio-editor/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting() 22 | ); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /projects/formio-editor/styles.css: -------------------------------------------------------------------------------- 1 | .jsoneditor-outer.has-main-menu-bar.has-status-bar{ 2 | height: 70vh; 3 | } 4 | 5 | .alert { 6 | padding: .375rem .75rem; 7 | margin-bottom: 0; 8 | } 9 | -------------------------------------------------------------------------------- /projects/formio-editor/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ], 13 | "allowJs": true 14 | }, 15 | "angularCompilerOptions": { 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "enableResourceInlining": true 19 | }, 20 | "exclude": [ 21 | "src/test.ts", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /projects/formio-editor/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "angularCompilerOptions": { 4 | "enableIvy": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/formio-editor/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/formio-editor/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "davebaol", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "davebaol", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davebaol/angular-formio-editor/b761ac9b4e06e58348a81d38ce22bb5bff5748b6/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Angular Form.io Editor Demo

4 |

Welcome to the demo of the Angular component <formio-editor> v{{formioEditorVersion}}, the one in the blue box.

5 |
6 |
7 | 8 |
9 | 10 | 11 |
12 |
13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { FormioModule } from 'angular-formio'; 5 | import { FormioEditorModule } from '@davebaol/angular-formio-editor'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { ButtonsModule } from 'ngx-bootstrap/buttons'; 8 | 9 | describe('AppComponent', () => { 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ 13 | AppComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | FormioModule, 18 | FormioEditorModule, 19 | FormsModule, 20 | ButtonsModule.forRoot() 21 | ] 22 | }).compileComponents(); 23 | })); 24 | 25 | it('should create the app', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | const app = fixture.componentInstance; 28 | expect(app).toBeTruthy(); 29 | }); 30 | 31 | it('should use component formio-editor', () => { 32 | const fixture = TestBed.createComponent(AppComponent); 33 | fixture.detectChanges(); 34 | const compiled = fixture.nativeElement; 35 | expect(compiled.querySelector('formio-editor')).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { FormioEditorOptions } from '@davebaol/angular-formio-editor'; 4 | import { version as formioEditorVersion} from '@davebaol/angular-formio-editor/package.json'; 5 | import form from './initial-form.json'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.css'] 11 | }) 12 | export class AppComponent { 13 | formioEditorVersion = formioEditorVersion; 14 | form: any; 15 | options: FormioEditorOptions; 16 | resetFormioEditor$ = new Subject(); 17 | 18 | constructor() { 19 | this.form = form; 20 | 21 | this.options = { 22 | builder: { 23 | hideDisplaySelect: false, 24 | output: { 25 | change: (event) => console.log('Demo: builder change event:', event), 26 | } 27 | }, 28 | json: {}, 29 | renderer: { 30 | input: { 31 | src: 'http://localhost:8383/api/v1/documents', 32 | renderOptions: { breadcrumbSettings: { clickable: true } } 33 | }, 34 | submissionPanel: { 35 | disabled: false, 36 | fullSubmission: true, 37 | resourceJsonEditor: { 38 | input: { 39 | options: {} 40 | } 41 | }, 42 | schemaJsonEditor: { 43 | enabled: true, 44 | input: { 45 | options: {} 46 | } 47 | } 48 | } 49 | } 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { FormioModule, FormioAppConfig } from 'angular-formio'; 6 | import { FormioEditorModule } from '@davebaol/angular-formio-editor'; 7 | import { ButtonsModule } from 'ngx-bootstrap/buttons'; 8 | import { AppComponent } from './app.component'; 9 | import { AppConfig } from '../formio-config'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | FormioModule, 18 | FormioEditorModule, 19 | FormsModule, 20 | BrowserAnimationsModule, 21 | ButtonsModule.forRoot() 22 | ], 23 | providers: [ 24 | { provide: FormioAppConfig, useValue: AppConfig } 25 | ], 26 | bootstrap: [AppComponent] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davebaol/angular-formio-editor/b761ac9b4e06e58348a81d38ce22bb5bff5748b6/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davebaol/angular-formio-editor/b761ac9b4e06e58348a81d38ce22bb5bff5748b6/src/favicon.ico -------------------------------------------------------------------------------- /src/formio-config.ts: -------------------------------------------------------------------------------- 1 | export const AppConfig = { 2 | // See https://github.com/formio/angular-formio/blob/master/src/formio.config.ts 3 | 4 | apiUrl: 'http://localhost/api', 5 | appUrl: 'http://localhost/app', 6 | 7 | // icons: '...', 8 | // formOnly: false 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | formio-editor-demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import "./styles/bootstrap/css/bootstrap.min.css"; 2 | @import '~font-awesome/css/font-awesome.min.css'; 3 | @import "~jsoneditor/dist/jsoneditor.min.css"; 4 | /* 5 | In your app the next import should become 6 | @import "~@davebaol/angular-formio-editor/styles.css"; 7 | */ 8 | @import "../dist/formio-editor/styles.css"; 9 | 10 | .center { 11 | margin: auto; 12 | width: 75%; 13 | } 14 | 15 | @media (max-width: 1023px){ 16 | .center { 17 | margin: 0; 18 | padding: 0; 19 | width: 100%; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | } 159 | 160 | figure { 161 | margin: 0 0 1rem; 162 | } 163 | 164 | img { 165 | vertical-align: middle; 166 | border-style: none; 167 | } 168 | 169 | svg { 170 | overflow: hidden; 171 | vertical-align: middle; 172 | } 173 | 174 | table { 175 | border-collapse: collapse; 176 | } 177 | 178 | caption { 179 | padding-top: 0.75rem; 180 | padding-bottom: 0.75rem; 181 | color: #6c757d; 182 | text-align: left; 183 | caption-side: bottom; 184 | } 185 | 186 | th { 187 | text-align: inherit; 188 | } 189 | 190 | label { 191 | display: inline-block; 192 | margin-bottom: 0.5rem; 193 | } 194 | 195 | button { 196 | border-radius: 0; 197 | } 198 | 199 | button:focus { 200 | outline: 1px dotted; 201 | outline: 5px auto -webkit-focus-ring-color; 202 | } 203 | 204 | input, 205 | button, 206 | select, 207 | optgroup, 208 | textarea { 209 | margin: 0; 210 | font-family: inherit; 211 | font-size: inherit; 212 | line-height: inherit; 213 | } 214 | 215 | button, 216 | input { 217 | overflow: visible; 218 | } 219 | 220 | button, 221 | select { 222 | text-transform: none; 223 | } 224 | 225 | select { 226 | word-wrap: normal; 227 | } 228 | 229 | button, 230 | [type="button"], 231 | [type="reset"], 232 | [type="submit"] { 233 | -webkit-appearance: button; 234 | } 235 | 236 | button:not(:disabled), 237 | [type="button"]:not(:disabled), 238 | [type="reset"]:not(:disabled), 239 | [type="submit"]:not(:disabled) { 240 | cursor: pointer; 241 | } 242 | 243 | button::-moz-focus-inner, 244 | [type="button"]::-moz-focus-inner, 245 | [type="reset"]::-moz-focus-inner, 246 | [type="submit"]::-moz-focus-inner { 247 | padding: 0; 248 | border-style: none; 249 | } 250 | 251 | input[type="radio"], 252 | input[type="checkbox"] { 253 | box-sizing: border-box; 254 | padding: 0; 255 | } 256 | 257 | input[type="date"], 258 | input[type="time"], 259 | input[type="datetime-local"], 260 | input[type="month"] { 261 | -webkit-appearance: listbox; 262 | } 263 | 264 | textarea { 265 | overflow: auto; 266 | resize: vertical; 267 | } 268 | 269 | fieldset { 270 | min-width: 0; 271 | padding: 0; 272 | margin: 0; 273 | border: 0; 274 | } 275 | 276 | legend { 277 | display: block; 278 | width: 100%; 279 | max-width: 100%; 280 | padding: 0; 281 | margin-bottom: .5rem; 282 | font-size: 1.5rem; 283 | line-height: inherit; 284 | color: inherit; 285 | white-space: normal; 286 | } 287 | 288 | progress { 289 | vertical-align: baseline; 290 | } 291 | 292 | [type="number"]::-webkit-inner-spin-button, 293 | [type="number"]::-webkit-outer-spin-button { 294 | height: auto; 295 | } 296 | 297 | [type="search"] { 298 | outline-offset: -2px; 299 | -webkit-appearance: none; 300 | } 301 | 302 | [type="search"]::-webkit-search-decoration { 303 | -webkit-appearance: none; 304 | } 305 | 306 | ::-webkit-file-upload-button { 307 | font: inherit; 308 | -webkit-appearance: button; 309 | } 310 | 311 | output { 312 | display: inline-block; 313 | } 314 | 315 | summary { 316 | display: list-item; 317 | cursor: pointer; 318 | } 319 | 320 | template { 321 | display: none; 322 | } 323 | 324 | [hidden] { 325 | display: none !important; 326 | } 327 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/styles/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/styles/bootstrap/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACkBA,ECTA,QADA,SDaE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGlBF,0CH+BE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KC9CF,0BDyDA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCnDF,GDsDA,GCvDA,GD0DE,WAAA,EACA,cAAA,KAGF,MCtDA,MACA,MAFA,MD2DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECvDA,ODyDE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC5DA,ID8DE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,cACE,MAAA,QACA,gBAAA,KI/LA,oBJkME,MAAA,QACA,gBAAA,KC7DJ,KACA,IDqEA,ICpEA,KDwEE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBCxGF,OD2GA,MCzGA,SADA,OAEA,SD6GE,OAAA,EACA,YAAA,QErPE,UAAA,QFuPF,YAAA,QAGF,OC3GA,MD6GE,SAAA,QAGF,OC3GA,OD6GE,eAAA,KAMF,OACE,UAAA,OC3GF,cACA,aACA,cDgHA,OAIE,mBAAA,OC/GF,6BACA,4BACA,6BDkHE,sBAKI,OAAA,QClHN,gCACA,+BACA,gCDsHA,yBAIE,QAAA,EACA,aAAA,KCrHF,qBDwHA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCxHA,2BACA,kBAFA,iBDkIE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MEjSI,UAAA,OFmSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGvIF,yCFGA,yCD0IE,OAAA,KGxIF,cHgJE,eAAA,KACA,mBAAA,KG5IF,yCHoJE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KGzJF,SH+JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","/*!\n * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "resolveJsonModule": true, 19 | "esModuleInterop": true, 20 | "paths": { 21 | "@davebaol/angular-formio-editor": [ 22 | "dist/formio-editor" 23 | ], 24 | "@davebaol/angular-formio-editor/*": [ 25 | "dist/formio-editor/*" 26 | ] 27 | } 28 | }, 29 | "angularCompilerOptions": { 30 | "fullTemplateTypeCheck": true, 31 | "strictInjectionParameters": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-return-shorthand": true, 12 | "curly": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "directive-class-suffix": true, 19 | "directive-selector": [ 20 | true, 21 | "attribute", 22 | "app", 23 | "camelCase" 24 | ], 25 | "component-selector": [ 26 | true, 27 | "element", 28 | "app", 29 | "kebab-case" 30 | ], 31 | "eofline": true, 32 | "import-blacklist": [ 33 | true, 34 | "rxjs/Rx" 35 | ], 36 | "import-spacing": true, 37 | "indent": { 38 | "options": [ 39 | "spaces" 40 | ] 41 | }, 42 | "max-classes-per-file": false, 43 | "max-line-length": [ 44 | true, 45 | 140 46 | ], 47 | "member-ordering": [ 48 | true, 49 | { 50 | "order": [ 51 | "static-field", 52 | "instance-field", 53 | "static-method", 54 | "instance-method" 55 | ] 56 | } 57 | ], 58 | "no-console": [ 59 | true, 60 | "debug", 61 | "info", 62 | "time", 63 | "timeEnd", 64 | "trace" 65 | ], 66 | "no-empty": false, 67 | "no-inferrable-types": [ 68 | true, 69 | "ignore-params" 70 | ], 71 | "no-non-null-assertion": true, 72 | "no-redundant-jsdoc": true, 73 | "no-switch-case-fall-through": true, 74 | "no-var-requires": false, 75 | "object-literal-key-quotes": [ 76 | true, 77 | "as-needed" 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single" 82 | ], 83 | "semicolon": { 84 | "options": [ 85 | "always" 86 | ] 87 | }, 88 | "space-before-function-paren": { 89 | "options": { 90 | "anonymous": "never", 91 | "asyncArrow": "always", 92 | "constructor": "never", 93 | "method": "never", 94 | "named": "never" 95 | } 96 | }, 97 | "typedef-whitespace": { 98 | "options": [ 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | }, 106 | { 107 | "call-signature": "onespace", 108 | "index-signature": "onespace", 109 | "parameter": "onespace", 110 | "property-declaration": "onespace", 111 | "variable-declaration": "onespace" 112 | } 113 | ] 114 | }, 115 | "variable-name": { 116 | "options": [ 117 | "ban-keywords", 118 | "check-format", 119 | "allow-pascal-case" 120 | ] 121 | }, 122 | "whitespace": { 123 | "options": [ 124 | "check-branch", 125 | "check-decl", 126 | "check-operator", 127 | "check-separator", 128 | "check-type", 129 | "check-typecast" 130 | ] 131 | }, 132 | "no-conflicting-lifecycle": true, 133 | "no-host-metadata-property": true, 134 | "no-input-rename": true, 135 | "no-inputs-metadata-property": true, 136 | "no-output-native": true, 137 | "no-output-on-prefix": true, 138 | "no-output-rename": true, 139 | "no-outputs-metadata-property": true, 140 | "template-banana-in-box": true, 141 | "template-no-negated-async": true, 142 | "use-lifecycle-interface": true, 143 | "use-pipe-transform-interface": true 144 | }, 145 | "rulesDirectory": [ 146 | "codelyzer" 147 | ] 148 | } --------------------------------------------------------------------------------