├── .vscode └── tasks.json ├── LICENSE ├── package.json ├── v1 ├── README.md ├── test-modes.html ├── demo-modes.html ├── jsonToForm │ ├── jsonToForm.css │ ├── jsonToForm_backup.css │ └── jsonToForm_new.css ├── demo.html └── demo-farsi.html ├── test-modes.html ├── README.md ├── src ├── jsonToForm.plugin.js ├── core │ ├── JsonToForm.js │ └── JsonFormEventHandler.js ├── styles │ ├── jsonToForm.clean.css │ ├── jsonToForm.branded.css │ └── jsonToForm.modern.css ├── utils │ └── JsonFormUtils.js └── validators │ └── JsonFormValidator.js ├── jsonToForm ├── jsonToForm_backup.css ├── jsonToForm.d.ts ├── jsonToForm_new.css └── jsonToForm.css ├── CHANGELOG.md └── demo-simple.html /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "serve-demo-v2", 6 | "type": "shell", 7 | "command": "python", 8 | "args": [ 9 | "-m", 10 | "http.server", 11 | "8080" 12 | ], 13 | "isBackground": true, 14 | "group": "build" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mohsen Mirshahreza 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsontoform", 3 | "version": "2.0.0", 4 | "description": "A modern jQuery plugin for converting JSON schemas to beautiful HTML forms with real-time validation", 5 | "main": "jsonToForm/jsonToForm.v2.js", 6 | "types": "jsonToForm/jsonToForm.d.ts", 7 | "scripts": { 8 | "build": "node build/build.js", 9 | "dev": "python -m http.server 8080", 10 | "serve": "python -m http.server 8080", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "lint": "eslint src/", 13 | "format": "prettier --write src/" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/yourusername/jsonToForm.git" 18 | }, 19 | "keywords": [ 20 | "json", 21 | "form", 22 | "jquery", 23 | "schema", 24 | "validation", 25 | "ui", 26 | "rtl", 27 | "persian", 28 | "farsi", 29 | "typescript", 30 | "css", 31 | "responsive", 32 | "modern" 33 | ], 34 | "author": "JsonToForm Contributors", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/yourusername/jsonToForm/issues" 38 | }, 39 | "homepage": "https://github.com/yourusername/jsonToForm#readme", 40 | "dependencies": { 41 | "jquery": "^3.7.1" 42 | }, 43 | "devDependencies": { 44 | "eslint": "^8.57.0", 45 | "prettier": "^3.2.5" 46 | }, 47 | "peerDependencies": { 48 | "jquery": ">=3.0.0" 49 | }, 50 | "files": [ 51 | "jsonToForm/", 52 | "src/", 53 | "demo-*.html", 54 | "CHANGELOG.md", 55 | "LICENSE" 56 | ], 57 | "engines": { 58 | "node": ">=14.0.0" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions", 63 | "not IE 11" 64 | ], 65 | "jsdelivr": "jsonToForm/jsonToForm.v2.js", 66 | "unpkg": "jsonToForm/jsonToForm.v2.js" 67 | } -------------------------------------------------------------------------------- /v1/README.md: -------------------------------------------------------------------------------- 1 | **Why jsonToForm** 2 | - Fast and easy to use. 3 | - RTL support : just add style (direction:rtl) to the place holder element. 4 | - It just depends on jQuery. 5 | - It can be use in tow mode : property grid(currently implemented) / normal form(road map). 6 | - Easy to customize css. 7 | - Supported inputs : text/checkbox/textarea/html/color/date/number/radio/select. 8 | - Validation support. 9 | - Additional text option for describing inputs. 10 | - Based on schema standard. 11 | 12 | **How to use** 13 | - A demo.html is included that describe the usage. 14 | 15 | **Options** 16 | - schema / default : {} / a json schema 17 | - value / default : {} / a json object 18 | - expandingLevel / default : -1 / tree levels that initially is expanded. by default all levels will be expanded 19 | - renderFirstLevel / default : false / indicates root element renders as a visual container or no 20 | - autoTrimValues / default : true / trims spaces automatically 21 | - indenting / default : 5 / number of spaces for each level of tree 22 | - treeExpandCollapseButton / default : true / show buttons to expand/collapse tree nodes 23 | - selectNullCaption / default : '' / caption for select elements when is null 24 | - selectNullCaption / default : 'null' / caption for radio elements when is null 25 | 26 | **Events** 27 | - afterValueChanged 28 | - afterWidgetCreated 29 | 30 | **Methods** 31 | - isValid() 32 | - getSchema() 33 | - getValue() 34 | - setValue(value) 35 | 36 | **Next step V1.1.1** 37 | - Defaults for schema / reset to default button 38 | - Validation by regular based on schema standards 39 | - Validation for array items based on schema standards 40 | 41 | **Road map** 42 | - Checkbox list (when node is simple array) 43 | - Including some important schemas like schema(for design another schema) / css 44 | - Including some important regulars like email/website/... 45 | - Layout option for switching between property grid mode and normal form 46 | - Auto complete source for inputs by connecting to other API 47 | - Additional item for object nodes 48 | 49 | 50 | **Similar projects** 51 | - https://github.com/jsonform/jsonform 52 | - https://jsonforms.io/ 53 | - https://github.com/jdorn/json-editor 54 | - https://github.com/plantain-00/schema-based-json-editor 55 | - https://github.com/codecombat/treema 56 | - https://json-schema-editor.tangramjs.com/ 57 | - https://github.com/yourtion/vue-json-ui-editor 58 | -------------------------------------------------------------------------------- /v1/test-modes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JsonToForm - Test Render Modes (v1) 5 | 6 | 7 | 8 | 9 | 32 | 33 | 34 | 35 |
36 |

تست حالات مختلف رندر JsonToForm

37 | 38 |
39 | 40 | 45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 |

خروجی JSON:

54 |

 55 |         
56 |
57 | 58 | 103 | 104 | -------------------------------------------------------------------------------- /test-modes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JsonToForm - Test Render Modes 5 | 6 | 7 | 8 | 9 | 32 | 33 | 34 | 35 |
36 |

تست حالات مختلف رندر JsonToForm

37 | 38 |
39 |

تست فرم JsonToForm (حالت پیش‌فرض)

40 |
41 | 42 |
43 |
44 |
45 | 46 |
47 |

خروجی JSON:

48 |

 49 |         
50 |
51 | 52 | 119 | 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # JsonToForm v2.0 🚀 4 | 5 | Modern jQuery plugin that turns JSON Schema-like definitions into beautiful, responsive HTML forms with real-time validation. 6 | 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 8 | ![jQuery](https://img.shields.io/badge/jQuery-3.x+-blue.svg) 9 | [![TypeScript](https://img.shields.io/badge/TypeScript-Definitions-blue.svg)](jsonToForm/jsonToForm.d.ts) 10 | 11 | [Live Demo](demo-v2.html) 12 | 13 |
14 | 15 | ## ✨ Highlights 16 | 17 | - 🎨 Modern UI: clean, responsive (Flexbox/Grid), light/dark ready 18 | - 🔎 Real-time validation: instant feedback with friendly hints 19 | - 🌍 i18n & RTL: Persian/Farsi and other RTL languages supported 20 | - 🧩 Rich inputs: string, number, email, tel, url, date, time, textarea, select, checkbox, radio, color, html, object, array 21 | - 🧱 Modular code: Renderer, Validator, EventHandler, Utils 22 | - 🛡️ TypeScript: bundled `.d.ts` for great IntelliSense 23 | 24 | ## 🚀 Quick Start 25 | 26 | Include jQuery, the compiled plugin, and one of the themes: 27 | 28 | ```html 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 57 | 58 | 59 | ``` 60 | 61 | ## 🧭 API (essentials) 62 | 63 | - `getValue()` → returns the current form value 64 | - `setValue(obj)` → sets/replaces form value 65 | - `isValid()` → boolean validity of the whole form 66 | - `validator.getAllErrors()` → list of validation errors 67 | 68 | Example: 69 | 70 | ```js 71 | const form = $("#myForm").jsonToForm(options); 72 | form.setValue({ name: "John Doe" }); 73 | console.log(form.getValue(), form.isValid()); 74 | console.log(form.validator.getAllErrors()); 75 | ``` 76 | 77 | ## 🎨 Theming & RTL 78 | 79 | - Themes: `src/styles/jsonToForm.clean.css` (simple), `src/styles/jsonToForm.modern.css` (polished) 80 | - Dark mode: set `data-json-form-theme="dark"` on `` 81 | - RTL: add `dir="rtl"` on ``/``/container 82 | 83 | ```html 84 | 85 | 86 | 87 | ``` 88 | 89 | ## 🧱 Project Structure 90 | 91 | - `src/` → modular source (core, renderer, validator, events, utils, styles) 92 | - `jsonToForm/jsonToForm.v2.js` → compiled v2 bundle 93 | - `jsonToForm/jsonToForm.d.ts` → TypeScript definitions 94 | - `v1/` → legacy v1 plugin, styles, and demos 95 | - `demo-v2.html` → v2 demo 96 | 97 | ## 🔁 Migrating from v1.x 98 | 99 | Old usage (v1.x): 100 | 101 | ```js 102 | $('#myForm').jsonToForm({ schema, value }); 103 | ``` 104 | 105 | New usage (v2): 106 | 107 | ```js 108 | $('#myForm').jsonToForm({ schema }); 109 | $('#myForm').jsonToForm('setValue', value); 110 | ``` 111 | 112 | For legacy plugin and original demos, see the `v1/` folder. 113 | 114 | ## 🧪 Try locally 115 | 116 | Run a simple static server and open the demo (PowerShell): 117 | 118 | ```powershell 119 | # From the repo root 120 | python -m http.server 8080 121 | # Open in your browser: 122 | # http://localhost:8080/demo-v2.html 123 | ``` 124 | 125 | ## 📝 License 126 | 127 | MIT © Contributors — see [LICENSE](LICENSE) 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/jsonToForm.plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonToForm v2.0.0 - Modern jQuery Plugin 3 | * 4 | * A powerful jQuery plugin for converting JSON Schema to HTML forms with modern features: 5 | * - Modular architecture with separate concerns 6 | * - Enhanced validation with custom rules 7 | * - Modern CSS with theme support 8 | * - Improved accessibility and UX 9 | * - Performance optimizations 10 | * 11 | * @author JsonToForm Team 12 | * @version 2.0.0 13 | * @license MIT 14 | */ 15 | 16 | (function($) { 17 | 'use strict'; 18 | 19 | // Plugin namespace 20 | const PLUGIN_NAME = 'jsonToForm'; 21 | const PLUGIN_VERSION = '2.0.0'; 22 | 23 | /** 24 | * jQuery plugin entry point 25 | */ 26 | $.fn.jsonToForm = function(options) { 27 | // Handle multiple elements 28 | if (this.length > 1) { 29 | return this.each(function() { 30 | $(this).jsonToForm(options); 31 | }); 32 | } 33 | 34 | const $element = this.first(); 35 | 36 | // Return existing instance if already initialized 37 | const existingInstance = $element.data(PLUGIN_NAME); 38 | if (existingInstance) { 39 | return existingInstance; 40 | } 41 | 42 | // Create and initialize new instance 43 | const instance = new JsonToForm($element, options); 44 | $element.data(PLUGIN_NAME, instance); 45 | 46 | return instance; 47 | }; 48 | 49 | // Plugin version 50 | $.fn.jsonToForm.version = PLUGIN_VERSION; 51 | 52 | // Default configuration 53 | $.fn.jsonToForm.defaults = { 54 | expandingLevel: -1, 55 | value: {}, 56 | schema: {}, 57 | autoTrimValues: true, 58 | indenting: 5, 59 | radioNullCaption: 'null', 60 | selectNullCaption: '', 61 | treeExpandCollapseButton: true, 62 | theme: 'default', 63 | responsive: true, 64 | validation: { 65 | realTime: true, 66 | showHints: true, 67 | customRules: {} 68 | }, 69 | callbacks: { 70 | afterValueChanged: null, 71 | afterWidgetCreated: null, 72 | beforeValidation: null, 73 | afterValidation: null 74 | } 75 | }; 76 | 77 | // Utility method to add custom validation rules globally 78 | $.fn.jsonToForm.addValidationRule = function(name, rule) { 79 | if (window.JsonFormValidator && JsonFormValidator.prototype) { 80 | JsonFormValidator.prototype.validationRules = JsonFormValidator.prototype.validationRules || {}; 81 | JsonFormValidator.prototype.validationRules[name] = rule; 82 | } 83 | }; 84 | 85 | // Utility method to set global theme 86 | $.fn.jsonToForm.setTheme = function(themeName) { 87 | $(document.body).attr('data-json-form-theme', themeName); 88 | }; 89 | 90 | // Plugin initialization 91 | $(document).ready(function() { 92 | // Auto-initialize forms with data-json-schema attribute 93 | $('[data-json-schema]').each(function() { 94 | const $form = $(this); 95 | const schemaUrl = $form.attr('data-json-schema'); 96 | const valueUrl = $form.attr('data-json-value'); 97 | 98 | // Load schema and optionally value from URLs 99 | const loadPromises = [$.getJSON(schemaUrl)]; 100 | if (valueUrl) { 101 | loadPromises.push($.getJSON(valueUrl)); 102 | } 103 | 104 | $.when.apply($, loadPromises).done(function(schema, value) { 105 | const options = { 106 | schema: schema, 107 | value: value || {} 108 | }; 109 | 110 | // Parse additional options from data attributes 111 | const dataOptions = $form.data(); 112 | Object.keys(dataOptions).forEach(key => { 113 | if (key.startsWith('jsonForm')) { 114 | const optionKey = key.replace('jsonForm', '').toLowerCase(); 115 | options[optionKey] = dataOptions[key]; 116 | } 117 | }); 118 | 119 | $form.jsonToForm(options); 120 | }); 121 | }); 122 | }); 123 | 124 | })(jQuery); 125 | 126 | // Expose classes for advanced usage 127 | if (typeof window !== 'undefined') { 128 | window.JsonToFormClasses = { 129 | JsonToForm: window.JsonToForm, 130 | JsonFormRenderer: window.JsonFormRenderer, 131 | JsonFormValidator: window.JsonFormValidator, 132 | JsonFormEventHandler: window.JsonFormEventHandler, 133 | JsonFormUtils: window.JsonFormUtils 134 | }; 135 | } -------------------------------------------------------------------------------- /v1/demo-modes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JsonToForm - نمایش حالات مختلف رندر (v1) 8 | 9 | 10 | 11 | 12 | 13 | 24 | 25 | 26 | 27 |
28 |
29 |

JsonToForm - نمایش حالات مختلف رندر

30 |

این فرم سه حالت رندر مختلف را پشتیبانی می‌کند: حالت فعلی، جدول ویژگی‌ها و فرم استاندارد

31 |
32 | 33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 |
وضعیت اعتبار:
41 |
معتبر
42 |
43 | 44 |
45 |
مقادیر JSON:
46 |
47 |
48 | 49 |
50 |
راهنما:
51 |
• حالت فعلی: طراحی اصلی با ساختار درختی 52 | • جدول ویژگی‌ها: نمایش در قالب جدول مانند Property Grid 53 | • فرم استاندارد: طراحی فرم کلاسیک با fieldset ها 54 | 55 | از منوی بالای فرم می‌توانید بین حالات مختلف تغییر دهید. 56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /jsonToForm/jsonToForm_backup.css: -------------------------------------------------------------------------------- 1 | .j-container { 2 | width: 100%; 3 | max-width: 100%; 4 | border-collapse: collapse; 5 | border-width: 0px; 6 | border-radius: 5px; 7 | box-sizing: border-box; 8 | overflow: hidden; 9 | } 10 | 11 | .j-container td { 12 | padding: 0px; 13 | word-wrap: break-word; 14 | overflow: hidden; 15 | box-sizing: border-box; 16 | } 17 | 18 | .j-action-col { 19 | width: 30px; 20 | min-width: 30px; 21 | max-width: 30px; 22 | text-align: center; 23 | vertical-align: top; 24 | padding: 2px !important; 25 | overflow: visible; 26 | } 27 | 28 | .j-container tr { 29 | outline: 0px solid gainsboro; 30 | } 31 | 32 | .j-title-col { 33 | width: 200px; 34 | max-width: 200px; 35 | min-width: 120px; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | vertical-align: top; 40 | } 41 | 42 | .j-sep-col { 43 | width: 0px; 44 | background-color:#f6f6f6; 45 | } 46 | 47 | .j-spacer-row { 48 | min-height:5px; 49 | background-color:#e8f2ff; 50 | margin:10px 0px 10px 0px; 51 | } 52 | 53 | .j-input-text,.j-input-select,.j-input-textarea,.j-input-date,.j-input-number,.j-input-html,.j-input-email,.j-input-tel { 54 | width: 100%; 55 | max-width: 100%; 56 | border: 2px solid #e6e6e6; 57 | padding: 5px; 58 | box-sizing: border-box; 59 | } 60 | 61 | .j-input-text{ 62 | background-color:#fefefe; 63 | border-radius:5px; 64 | } 65 | 66 | .j-input-text:disabled{ 67 | border-color:#f0f5ff; 68 | background-color:#f5f8ff; 69 | } 70 | 71 | .j-input-radio-label,.j-input-radio { 72 | vertical-align:middle; 73 | } 74 | 75 | .j-input-radio-label{ 76 | margin-left:5px; 77 | } 78 | 79 | .j-input-text::placeholder{ 80 | color: silver; 81 | } 82 | 83 | .j-input-html-div{ 84 | padding: 8px; 85 | margin-top: 3px; 86 | margin-bottom: 3px; 87 | margin-left: 2px; 88 | margin-right: 2px; 89 | } 90 | 91 | .j-input-html{ 92 | width: 0px; 93 | height: 0px; 94 | padding: 0px; 95 | border-width: 0px; 96 | } 97 | 98 | .j-input-textarea{ 99 | margin-top: 2px; 100 | margin-bottom: 2px; 101 | } 102 | 103 | .j-input-text:focus,.j-input-select:focus,.j-input-textarea:focus,.j-input-date:focus,.j-input-number:focus,.j-input-html-div:focus { 104 | outline: 1px solid gainsboro; 105 | } 106 | 107 | 108 | 109 | 110 | .j-oject-title-row,.j-array-title-row, 111 | .j-oject-title-row td,.j-array-title-row td { 112 | font-weight: bold; 113 | font-size: 14px; 114 | background-color: gainsboro; 115 | } 116 | 117 | .j-oject-title-row .j-title-col,.j-array-title-row .j-title-col { 118 | padding: 3px !important; 119 | padding-left: 8px !important; 120 | padding-right: 8px !important; 121 | } 122 | 123 | 124 | .j-oject-value-row td { 125 | font-size: 12px; 126 | } 127 | 128 | .j-oject-value-row .j-title-col { 129 | padding: 6px !important; 130 | font-size: 12px; 131 | } 132 | 133 | 134 | 135 | .j-add-array-item{ 136 | cursor: pointer; 137 | margin-left: 4px; 138 | margin-right: 4px; 139 | font-weight: bolder; 140 | font-size: 12px; 141 | display: inline-block; 142 | width: 18px; 143 | height: 18px; 144 | line-height: 16px; 145 | text-align: center; 146 | border-radius: 50%; 147 | border: 1px solid green; 148 | background: white; 149 | color: green; 150 | vertical-align: middle; 151 | } 152 | 153 | .j-remove-array-item{ 154 | font-family: 'Arial'; 155 | color: red; 156 | cursor: pointer; 157 | font-size: 12px; 158 | font-weight: bold; 159 | display: inline-block; 160 | width: 18px; 161 | height: 18px; 162 | line-height: 16px; 163 | text-align: center; 164 | border-radius: 50%; 165 | border: 1px solid red; 166 | background: white; 167 | margin: 1px; 168 | vertical-align: middle; 169 | } 170 | 171 | .j-ec { 172 | width: 5px; 173 | display: inline-block; 174 | cursor: pointer; 175 | } 176 | 177 | .j-collapsed{ 178 | display: none; 179 | } 180 | 181 | .j-body-col { 182 | font-size: 12px; 183 | width: auto; 184 | overflow: visible; 185 | vertical-align: top; 186 | } 187 | 188 | .j-inline-help { 189 | margin-top: 7px !important; 190 | margin-bottom: 5px !important; 191 | padding-left: 10px; 192 | padding-right: 10px; 193 | font-size: 12px; 194 | color: gray; 195 | } 196 | 197 | .j-validation-help { 198 | margin-top: 7px !important; 199 | margin-bottom: 5px !important; 200 | padding-left: 10px; 201 | padding-right: 10px; 202 | font-size: 12px; 203 | color: orange; 204 | } 205 | 206 | .j-input[data-is-valid="false"] { 207 | outline: 1px solid red !important; 208 | outline-offset: -2px; 209 | } 210 | 211 | .j-input[data-is-valid="true"].j-validation-help { 212 | display: none; 213 | } 214 | 215 | .j-required-star { 216 | color: red; 217 | font-weight: bold; 218 | vertical-align: middle; 219 | } 220 | 221 | /* کنترل کلی overflow */ 222 | body { 223 | overflow-x: hidden; 224 | } 225 | 226 | #jsonEditor { 227 | max-width: 100%; 228 | overflow-x: hidden; 229 | box-sizing: border-box; 230 | } 231 | 232 | .form-panel { 233 | overflow-x: hidden; 234 | box-sizing: border-box; 235 | } 236 | 237 | /* جلوگیری از overflow در تمام سایزها */ 238 | .j-container table { 239 | table-layout: fixed; 240 | width: 100%; 241 | max-width: 100%; 242 | } 243 | 244 | .j-oject-value-row { 245 | width: 100%; 246 | } 247 | 248 | .j-oject-value-row td { 249 | overflow: hidden; 250 | word-wrap: break-word; 251 | } 252 | 253 | /* Responsive Design for Mobile */ 254 | @media (max-width: 768px) { 255 | .j-title-col { 256 | width: 150px !important; 257 | min-width: 100px; 258 | font-size: 11px; 259 | } 260 | 261 | .j-body-col { 262 | width: auto !important; 263 | } 264 | 265 | .j-action-col { 266 | width: 25px !important; 267 | min-width: 25px !important; 268 | } 269 | 270 | .j-container { 271 | font-size: 11px; 272 | } 273 | 274 | .j-input-text, .j-input-select, .j-input-textarea, 275 | .j-input-date, .j-input-number, .j-input-html, 276 | .j-input-email, .j-input-tel { 277 | padding: 3px; 278 | font-size: 12px; 279 | } 280 | } 281 | 282 | @media (max-width: 480px) { 283 | .j-title-col { 284 | width: 120px !important; 285 | min-width: 80px !important; 286 | font-size: 10px; 287 | } 288 | 289 | .j-body-col { 290 | width: auto !important; 291 | } 292 | 293 | .j-action-col { 294 | width: 20px !important; 295 | min-width: 20px !important; 296 | } 297 | 298 | .j-sep-col { 299 | display: none; 300 | } 301 | 302 | .j-remove-array-item, .j-add-array-item { 303 | width: 16px; 304 | height: 16px; 305 | font-size: 10px; 306 | line-height: 14px; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /v1/jsonToForm/jsonToForm.css: -------------------------------------------------------------------------------- 1 | .j-container { 2 | width: 100%; 3 | max-width: 100%; 4 | border-collapse: collapse; 5 | border-width: 0px; 6 | border-radius: 5px; 7 | box-sizing: border-box; 8 | overflow: hidden; 9 | } 10 | 11 | .j-container td { 12 | padding: 0px; 13 | word-wrap: break-word; 14 | overflow: hidden; 15 | box-sizing: border-box; 16 | } 17 | 18 | .j-action-col { 19 | width: 30px; 20 | min-width: 30px; 21 | max-width: 30px; 22 | text-align: center; 23 | vertical-align: top; 24 | padding: 2px !important; 25 | overflow: visible; 26 | } 27 | 28 | .j-container tr { 29 | outline: 0px solid gainsboro; 30 | } 31 | 32 | .j-title-col { 33 | width: 200px; 34 | max-width: 200px; 35 | min-width: 120px; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | vertical-align: top; 40 | } 41 | 42 | .j-sep-col { 43 | width: 0px; 44 | background-color:#f6f6f6; 45 | } 46 | 47 | .j-spacer-row { 48 | min-height:5px; 49 | background-color:#e8f2ff; 50 | margin:10px 0px 10px 0px; 51 | } 52 | 53 | .j-input-text,.j-input-select,.j-input-textarea,.j-input-date,.j-input-number,.j-input-html,.j-input-email,.j-input-tel { 54 | width: 100%; 55 | max-width: 100%; 56 | border: 2px solid #e6e6e6; 57 | padding: 5px; 58 | box-sizing: border-box; 59 | } 60 | 61 | .j-input-text{ 62 | background-color:#fefefe; 63 | border-radius:5px; 64 | } 65 | 66 | .j-input-text:disabled{ 67 | border-color:#f0f5ff; 68 | background-color:#f5f8ff; 69 | } 70 | 71 | .j-input-radio-label,.j-input-radio { 72 | vertical-align:middle; 73 | } 74 | 75 | .j-input-radio-label{ 76 | margin-left:5px; 77 | } 78 | 79 | .j-input-text::placeholder{ 80 | color: silver; 81 | } 82 | 83 | .j-input-html-div{ 84 | padding: 8px; 85 | margin-top: 3px; 86 | margin-bottom: 3px; 87 | margin-left: 2px; 88 | margin-right: 2px; 89 | } 90 | 91 | .j-input-html{ 92 | width: 0px; 93 | height: 0px; 94 | padding: 0px; 95 | border-width: 0px; 96 | } 97 | 98 | .j-input-textarea{ 99 | margin-top: 2px; 100 | margin-bottom: 2px; 101 | } 102 | 103 | .j-input-text:focus,.j-input-select:focus,.j-input-textarea:focus,.j-input-date:focus,.j-input-number:focus,.j-input-html-div:focus { 104 | outline: 1px solid gainsboro; 105 | } 106 | 107 | 108 | 109 | 110 | .j-oject-title-row,.j-array-title-row, 111 | .j-oject-title-row td,.j-array-title-row td { 112 | font-weight: bold; 113 | font-size: 14px; 114 | background-color: gainsboro; 115 | } 116 | 117 | .j-oject-title-row .j-title-col,.j-array-title-row .j-title-col { 118 | padding: 3px !important; 119 | padding-left: 8px !important; 120 | padding-right: 8px !important; 121 | } 122 | 123 | 124 | .j-oject-value-row td { 125 | font-size: 12px; 126 | } 127 | 128 | .j-oject-value-row .j-title-col { 129 | padding: 6px !important; 130 | font-size: 12px; 131 | } 132 | 133 | 134 | 135 | .j-add-array-item{ 136 | cursor: pointer; 137 | margin-left: 4px; 138 | margin-right: 4px; 139 | font-weight: bolder; 140 | font-size: 12px; 141 | display: inline-block; 142 | width: 18px; 143 | height: 18px; 144 | line-height: 16px; 145 | text-align: center; 146 | border-radius: 50%; 147 | border: 1px solid green; 148 | background: white; 149 | color: green; 150 | vertical-align: middle; 151 | } 152 | 153 | .j-remove-array-item{ 154 | font-family: 'Arial'; 155 | color: red; 156 | cursor: pointer; 157 | font-size: 12px; 158 | font-weight: bold; 159 | display: inline-block; 160 | width: 18px; 161 | height: 18px; 162 | line-height: 16px; 163 | text-align: center; 164 | border-radius: 50%; 165 | border: 1px solid red; 166 | background: white; 167 | margin: 1px; 168 | vertical-align: middle; 169 | } 170 | 171 | .j-ec { 172 | width: 5px; 173 | display: inline-block; 174 | cursor: pointer; 175 | } 176 | 177 | .j-collapsed{ 178 | display: none; 179 | } 180 | 181 | .j-body-col { 182 | font-size: 12px; 183 | width: auto; 184 | overflow: visible; 185 | vertical-align: top; 186 | } 187 | 188 | .j-inline-help { 189 | margin-top: 7px !important; 190 | margin-bottom: 5px !important; 191 | padding-left: 10px; 192 | padding-right: 10px; 193 | font-size: 12px; 194 | color: gray; 195 | } 196 | 197 | .j-validation-help { 198 | margin-top: 7px !important; 199 | margin-bottom: 5px !important; 200 | padding-left: 10px; 201 | padding-right: 10px; 202 | font-size: 12px; 203 | color: orange; 204 | } 205 | 206 | .j-input[data-is-valid="false"] { 207 | outline: 1px solid red !important; 208 | outline-offset: -2px; 209 | } 210 | 211 | .j-input[data-is-valid="true"].j-validation-help { 212 | display: none; 213 | } 214 | 215 | .j-required-star { 216 | color: red; 217 | font-weight: bold; 218 | vertical-align: middle; 219 | } 220 | 221 | /* کنترل کلی overflow */ 222 | body { 223 | overflow-x: hidden; 224 | } 225 | 226 | #jsonEditor { 227 | max-width: 100%; 228 | overflow-x: hidden; 229 | box-sizing: border-box; 230 | } 231 | 232 | .form-panel { 233 | overflow-x: hidden; 234 | box-sizing: border-box; 235 | } 236 | 237 | /* جلوگیری از overflow در تمام سایزها */ 238 | .j-container table { 239 | table-layout: fixed; 240 | width: 100%; 241 | max-width: 100%; 242 | } 243 | 244 | .j-oject-value-row { 245 | width: 100%; 246 | } 247 | 248 | .j-oject-value-row td { 249 | overflow: hidden; 250 | word-wrap: break-word; 251 | } 252 | 253 | /* Responsive Design for Mobile */ 254 | @media (max-width: 768px) { 255 | .j-title-col { 256 | width: 150px !important; 257 | min-width: 100px; 258 | font-size: 11px; 259 | } 260 | 261 | .j-body-col { 262 | width: auto !important; 263 | } 264 | 265 | .j-action-col { 266 | width: 25px !important; 267 | min-width: 25px !important; 268 | } 269 | 270 | .j-container { 271 | font-size: 11px; 272 | } 273 | 274 | .j-input-text, .j-input-select, .j-input-textarea, 275 | .j-input-date, .j-input-number, .j-input-html, 276 | .j-input-email, .j-input-tel { 277 | padding: 3px; 278 | font-size: 12px; 279 | } 280 | } 281 | 282 | @media (max-width: 480px) { 283 | .j-title-col { 284 | width: 120px !important; 285 | min-width: 80px !important; 286 | font-size: 10px; 287 | } 288 | 289 | .j-body-col { 290 | width: auto !important; 291 | } 292 | 293 | .j-action-col { 294 | width: 20px !important; 295 | min-width: 20px !important; 296 | } 297 | 298 | .j-sep-col { 299 | display: none; 300 | } 301 | 302 | .j-remove-array-item, .j-add-array-item { 303 | width: 16px; 304 | height: 16px; 305 | font-size: 10px; 306 | line-height: 14px; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /v1/jsonToForm/jsonToForm_backup.css: -------------------------------------------------------------------------------- 1 | .j-container { 2 | width: 100%; 3 | max-width: 100%; 4 | border-collapse: collapse; 5 | border-width: 0px; 6 | border-radius: 5px; 7 | box-sizing: border-box; 8 | overflow: hidden; 9 | } 10 | 11 | .j-container td { 12 | padding: 0px; 13 | word-wrap: break-word; 14 | overflow: hidden; 15 | box-sizing: border-box; 16 | } 17 | 18 | .j-action-col { 19 | width: 30px; 20 | min-width: 30px; 21 | max-width: 30px; 22 | text-align: center; 23 | vertical-align: top; 24 | padding: 2px !important; 25 | overflow: visible; 26 | } 27 | 28 | .j-container tr { 29 | outline: 0px solid gainsboro; 30 | } 31 | 32 | .j-title-col { 33 | width: 200px; 34 | max-width: 200px; 35 | min-width: 120px; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | vertical-align: top; 40 | } 41 | 42 | .j-sep-col { 43 | width: 0px; 44 | background-color:#f6f6f6; 45 | } 46 | 47 | .j-spacer-row { 48 | min-height:5px; 49 | background-color:#e8f2ff; 50 | margin:10px 0px 10px 0px; 51 | } 52 | 53 | .j-input-text,.j-input-select,.j-input-textarea,.j-input-date,.j-input-number,.j-input-html,.j-input-email,.j-input-tel { 54 | width: 100%; 55 | max-width: 100%; 56 | border: 2px solid #e6e6e6; 57 | padding: 5px; 58 | box-sizing: border-box; 59 | } 60 | 61 | .j-input-text{ 62 | background-color:#fefefe; 63 | border-radius:5px; 64 | } 65 | 66 | .j-input-text:disabled{ 67 | border-color:#f0f5ff; 68 | background-color:#f5f8ff; 69 | } 70 | 71 | .j-input-radio-label,.j-input-radio { 72 | vertical-align:middle; 73 | } 74 | 75 | .j-input-radio-label{ 76 | margin-left:5px; 77 | } 78 | 79 | .j-input-text::placeholder{ 80 | color: silver; 81 | } 82 | 83 | .j-input-html-div{ 84 | padding: 8px; 85 | margin-top: 3px; 86 | margin-bottom: 3px; 87 | margin-left: 2px; 88 | margin-right: 2px; 89 | } 90 | 91 | .j-input-html{ 92 | width: 0px; 93 | height: 0px; 94 | padding: 0px; 95 | border-width: 0px; 96 | } 97 | 98 | .j-input-textarea{ 99 | margin-top: 2px; 100 | margin-bottom: 2px; 101 | } 102 | 103 | .j-input-text:focus,.j-input-select:focus,.j-input-textarea:focus,.j-input-date:focus,.j-input-number:focus,.j-input-html-div:focus { 104 | outline: 1px solid gainsboro; 105 | } 106 | 107 | 108 | 109 | 110 | .j-oject-title-row,.j-array-title-row, 111 | .j-oject-title-row td,.j-array-title-row td { 112 | font-weight: bold; 113 | font-size: 14px; 114 | background-color: gainsboro; 115 | } 116 | 117 | .j-oject-title-row .j-title-col,.j-array-title-row .j-title-col { 118 | padding: 3px !important; 119 | padding-left: 8px !important; 120 | padding-right: 8px !important; 121 | } 122 | 123 | 124 | .j-oject-value-row td { 125 | font-size: 12px; 126 | } 127 | 128 | .j-oject-value-row .j-title-col { 129 | padding: 6px !important; 130 | font-size: 12px; 131 | } 132 | 133 | 134 | 135 | .j-add-array-item{ 136 | cursor: pointer; 137 | margin-left: 4px; 138 | margin-right: 4px; 139 | font-weight: bolder; 140 | font-size: 12px; 141 | display: inline-block; 142 | width: 18px; 143 | height: 18px; 144 | line-height: 16px; 145 | text-align: center; 146 | border-radius: 50%; 147 | border: 1px solid green; 148 | background: white; 149 | color: green; 150 | vertical-align: middle; 151 | } 152 | 153 | .j-remove-array-item{ 154 | font-family: 'Arial'; 155 | color: red; 156 | cursor: pointer; 157 | font-size: 12px; 158 | font-weight: bold; 159 | display: inline-block; 160 | width: 18px; 161 | height: 18px; 162 | line-height: 16px; 163 | text-align: center; 164 | border-radius: 50%; 165 | border: 1px solid red; 166 | background: white; 167 | margin: 1px; 168 | vertical-align: middle; 169 | } 170 | 171 | .j-ec { 172 | width: 5px; 173 | display: inline-block; 174 | cursor: pointer; 175 | } 176 | 177 | .j-collapsed{ 178 | display: none; 179 | } 180 | 181 | .j-body-col { 182 | font-size: 12px; 183 | width: auto; 184 | overflow: visible; 185 | vertical-align: top; 186 | } 187 | 188 | .j-inline-help { 189 | margin-top: 7px !important; 190 | margin-bottom: 5px !important; 191 | padding-left: 10px; 192 | padding-right: 10px; 193 | font-size: 12px; 194 | color: gray; 195 | } 196 | 197 | .j-validation-help { 198 | margin-top: 7px !important; 199 | margin-bottom: 5px !important; 200 | padding-left: 10px; 201 | padding-right: 10px; 202 | font-size: 12px; 203 | color: orange; 204 | } 205 | 206 | .j-input[data-is-valid="false"] { 207 | outline: 1px solid red !important; 208 | outline-offset: -2px; 209 | } 210 | 211 | .j-input[data-is-valid="true"].j-validation-help { 212 | display: none; 213 | } 214 | 215 | .j-required-star { 216 | color: red; 217 | font-weight: bold; 218 | vertical-align: middle; 219 | } 220 | 221 | /* کنترل کلی overflow */ 222 | body { 223 | overflow-x: hidden; 224 | } 225 | 226 | #jsonEditor { 227 | max-width: 100%; 228 | overflow-x: hidden; 229 | box-sizing: border-box; 230 | } 231 | 232 | .form-panel { 233 | overflow-x: hidden; 234 | box-sizing: border-box; 235 | } 236 | 237 | /* جلوگیری از overflow در تمام سایزها */ 238 | .j-container table { 239 | table-layout: fixed; 240 | width: 100%; 241 | max-width: 100%; 242 | } 243 | 244 | .j-oject-value-row { 245 | width: 100%; 246 | } 247 | 248 | .j-oject-value-row td { 249 | overflow: hidden; 250 | word-wrap: break-word; 251 | } 252 | 253 | /* Responsive Design for Mobile */ 254 | @media (max-width: 768px) { 255 | .j-title-col { 256 | width: 150px !important; 257 | min-width: 100px; 258 | font-size: 11px; 259 | } 260 | 261 | .j-body-col { 262 | width: auto !important; 263 | } 264 | 265 | .j-action-col { 266 | width: 25px !important; 267 | min-width: 25px !important; 268 | } 269 | 270 | .j-container { 271 | font-size: 11px; 272 | } 273 | 274 | .j-input-text, .j-input-select, .j-input-textarea, 275 | .j-input-date, .j-input-number, .j-input-html, 276 | .j-input-email, .j-input-tel { 277 | padding: 3px; 278 | font-size: 12px; 279 | } 280 | } 281 | 282 | @media (max-width: 480px) { 283 | .j-title-col { 284 | width: 120px !important; 285 | min-width: 80px !important; 286 | font-size: 10px; 287 | } 288 | 289 | .j-body-col { 290 | width: auto !important; 291 | } 292 | 293 | .j-action-col { 294 | width: 20px !important; 295 | min-width: 20px !important; 296 | } 297 | 298 | .j-sep-col { 299 | display: none; 300 | } 301 | 302 | .j-remove-array-item, .j-add-array-item { 303 | width: 16px; 304 | height: 16px; 305 | font-size: 10px; 306 | line-height: 14px; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to JsonToForm project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.0.0] - 2024-12-19 9 | 10 | ### 🚀 Added 11 | - **Modern ES6+ Architecture**: Complete rewrite using ES6 classes and modules 12 | - **TypeScript Support**: Full TypeScript definitions included 13 | - **Advanced Validation System**: Real-time validation with custom rules 14 | - **Modular Design**: Separated into JsonToForm, Renderer, Validator, EventHandler, and Utils modules 15 | - **Modern CSS Framework**: Two themes (Modern and Clean) with CSS custom properties 16 | - **Enhanced RTL Support**: Better right-to-left language support for Persian/Arabic 17 | - **New Input Types**: Added email, tel, url, date, time, datetime-local support 18 | - **Array Management**: Dynamic add/remove items in arrays 19 | - **Object Nesting**: Better nested object rendering and management 20 | - **Event System**: Enhanced event handling with proper cleanup 21 | - **Persian Demo**: Complete Persian (Farsi) demonstration with RTL layout 22 | 23 | ### 🎨 Improved 24 | - **CSS Architecture**: Modern CSS with custom properties, Flexbox, and Grid 25 | - **Responsive Design**: Mobile-first approach with better responsive behavior 26 | - **Color Palette**: Clean, modern color scheme with proper contrast ratios 27 | - **Typography**: Improved font system and text hierarchy 28 | - **Layout System**: Better spacing and alignment using CSS Grid/Flexbox 29 | - **Form Controls**: Enhanced styling for all input types 30 | - **Button Design**: Modern button styles with hover and focus states 31 | 32 | ### 🔧 Changed 33 | - **Breaking Changes**: New API structure (methods now accept method name as first parameter) 34 | - **File Structure**: Organized into `src/` directory with proper module separation 35 | - **CSS Classes**: Updated class naming convention for better clarity 36 | - **Method Names**: Consistent API method naming 37 | - **Options Object**: Restructured options for better organization 38 | 39 | ### 🐛 Fixed 40 | - **Container Overflow**: Fixed form controls extending beyond their containers 41 | - **Input Width Issues**: Proper `max-width: 100%` and `box-sizing: border-box` 42 | - **RTL Layout**: Better right-to-left text direction handling 43 | - **Validation Timing**: Fixed validation timing and error display 44 | - **Memory Leaks**: Proper event cleanup and object disposal 45 | - **CSS Specificity**: Better CSS specificity management 46 | 47 | ### 📁 File Structure 48 | ``` 49 | jsonToForm/ 50 | ├── src/ 51 | │ ├── core/ 52 | │ │ ├── JsonToForm.js # Main plugin class 53 | │ │ ├── JsonFormRenderer.js # Form rendering engine 54 | │ │ ├── JsonFormValidator.js # Validation system 55 | │ │ ├── JsonFormEventHandler.js # Event management 56 | │ │ └── JsonFormUtils.js # Utility functions 57 | │ ├── styles/ 58 | │ │ ├── jsonToForm.modern.css # Modern theme 59 | │ │ └── jsonToForm.clean.css # Clean minimal theme 60 | │ └── types/ 61 | │ └── jsonToForm.d.ts # TypeScript definitions 62 | ├── jsonToForm/ 63 | │ ├── jsonToForm.v2.js # Compiled v2.0 (production) 64 | │ ├── jsonToForm.js # Legacy v1.x 65 | │ └── jsonToForm.css # Legacy styles 66 | ├── demo-farsi.html # Persian RTL demo 67 | ├── demo-v2.html # English demo 68 | └── README-v2.md # v2.0 documentation 69 | ``` 70 | 71 | ### 🌟 New Features Detail 72 | 73 | #### Enhanced Validation System 74 | - Real-time validation as users type 75 | - Custom validation rules support 76 | - Persian error messages 77 | - Visual validation feedback 78 | - Validation summary display 79 | 80 | #### Modern CSS Themes 81 | - **Clean Theme**: Minimal, professional design 82 | - **Modern Theme**: Rich, feature-complete styling 83 | - CSS custom properties for easy theming 84 | - Dark mode support infrastructure 85 | - Mobile-optimized responsive design 86 | 87 | #### Advanced Input Support 88 | - HTML5 input types (email, tel, url, date, time) 89 | - Rich text editor for HTML content 90 | - Color picker integration 91 | - File upload preparation 92 | - Custom input type extensibility 93 | 94 | #### Improved Internationalization 95 | - Better RTL (Right-to-Left) language support 96 | - Persian/Farsi localization 97 | - Custom message templates 98 | - Direction-aware layout system 99 | 100 | ### 🔄 Migration Guide 101 | 102 | #### From v1.x to v2.0 103 | 104 | **Old Way (v1.x):** 105 | ```javascript 106 | $('#form').jsonToForm({ 107 | schema: schema, 108 | value: initialData 109 | }); 110 | 111 | var data = $('#form').jsonToForm().getValue(); 112 | var isValid = $('#form').jsonToForm().isValid(); 113 | ``` 114 | 115 | **New Way (v2.0):** 116 | ```javascript 117 | $('#form').jsonToForm({ 118 | schema: schema 119 | }); 120 | $('#form').jsonToForm('setValue', initialData); 121 | 122 | var data = $('#form').jsonToForm('getValue'); 123 | var isValid = $('#form').jsonToForm('isValid'); 124 | ``` 125 | 126 | **CSS Migration:** 127 | ```html 128 | 129 | 130 | 131 | 132 | 133 | ``` 134 | 135 | ### 📊 Performance Improvements 136 | - Reduced bundle size through modular architecture 137 | - Better memory management with proper cleanup 138 | - Optimized DOM manipulation 139 | - Efficient event delegation 140 | - Lazy loading of non-essential features 141 | 142 | ### 🧪 Browser Support 143 | - Chrome 80+ 144 | - Firefox 75+ 145 | - Safari 13+ 146 | - Edge 80+ 147 | - Mobile browsers (iOS Safari, Chrome Mobile) 148 | 149 | ### 📋 Known Issues 150 | - Internet Explorer is no longer supported 151 | - Some advanced CSS features require modern browsers 152 | - File upload functionality not yet implemented 153 | 154 | ### 🎯 Future Plans (v2.1) 155 | - [ ] File upload input type 156 | - [ ] Advanced array validation 157 | - [ ] Custom theme builder 158 | - [ ] Vue.js and React adapters 159 | - [ ] Performance monitoring 160 | - [ ] Accessibility improvements 161 | 162 | --- 163 | 164 | ## [1.0.0] - Previous Release 165 | 166 | ### Features 167 | - Basic JSON Schema to HTML form conversion 168 | - Simple validation system 169 | - RTL support 170 | - jQuery plugin architecture 171 | - Property grid mode 172 | - Basic input types support 173 | 174 | ### Supported Input Types 175 | - text, checkbox, textarea, html, color, date, number, radio, select 176 | 177 | ### Options 178 | - schema: JSON schema definition 179 | - value: Initial form values 180 | - expandingLevel: Tree expansion level 181 | - renderFirstLevel: Root element rendering 182 | - autoTrimValues: Automatic value trimming 183 | - indenting: Tree indentation spaces 184 | - treeExpandCollapseButton: Show/hide expand buttons 185 | - selectNullCaption: Select null option caption 186 | - radioNullCaption: Radio null option caption 187 | 188 | ### Events 189 | - afterValueChanged: Triggered after value changes 190 | - afterWidgetCreated: Triggered after widget creation 191 | 192 | ### Methods 193 | - isValid(): Check form validation 194 | - getSchema(): Get current schema 195 | - getValue(): Get form values 196 | - setValue(value): Set form values 197 | 198 | --- 199 | 200 | ## Contributing 201 | 202 | Please read our contributing guidelines before submitting pull requests. All contributions should include appropriate tests and documentation updates. 203 | 204 | ## License 205 | 206 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /v1/jsonToForm/jsonToForm_new.css: -------------------------------------------------------------------------------- 1 | /* ===== JsonToForm Modern CSS با Flexbox Layout ===== */ 2 | 3 | /* کنترل کلی overflow */ 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | overflow-x: hidden; 10 | } 11 | 12 | #jsonEditor { 13 | max-width: 100%; 14 | overflow-x: hidden; 15 | } 16 | 17 | .form-panel { 18 | overflow-x: hidden; 19 | } 20 | 21 | /* ===== Container اصلی - حالا div بجای table ===== */ 22 | .j-container { 23 | width: 100%; 24 | max-width: 100%; 25 | margin-bottom: 8px; 26 | border-radius: 5px; 27 | overflow: hidden; 28 | } 29 | 30 | /* ===== Row Layout با Flexbox ===== */ 31 | .j-field-row { 32 | display: flex; 33 | align-items: flex-start; 34 | min-height: 32px; 35 | padding: 4px 8px; 36 | border-bottom: 1px solid #f0f0f0; 37 | gap: 8px; 38 | } 39 | 40 | .j-field-row:last-child { 41 | border-bottom: none; 42 | } 43 | 44 | /* ===== Header Rows ===== */ 45 | .j-header-row { 46 | background-color: gainsboro; 47 | font-weight: bold; 48 | font-size: 14px; 49 | padding: 8px; 50 | border-bottom: 2px solid #ddd; 51 | } 52 | 53 | /* ===== Columns با Flexbox ===== */ 54 | .j-label-col { 55 | flex: 0 0 180px; 56 | min-width: 120px; 57 | max-width: 200px; 58 | padding: 6px 8px; 59 | font-size: 12px; 60 | font-weight: 500; 61 | color: #333; 62 | text-align: right; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | white-space: nowrap; 66 | align-self: flex-start; 67 | } 68 | 69 | .j-input-col { 70 | flex: 1; 71 | min-width: 0; /* مهم برای flexbox */ 72 | padding: 4px; 73 | } 74 | 75 | .j-action-col { 76 | flex: 0 0 32px; 77 | min-width: 32px; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | padding: 2px; 82 | } 83 | 84 | /* ===== Input Styles ===== */ 85 | .j-input-text, 86 | .j-input-select, 87 | .j-input-textarea, 88 | .j-input-date, 89 | .j-input-number, 90 | .j-input-html, 91 | .j-input-email, 92 | .j-input-tel { 93 | width: 100%; 94 | border: 2px solid #e6e6e6; 95 | padding: 5px; 96 | border-radius: 5px; 97 | background-color: #fefefe; 98 | font-size: 12px; 99 | box-sizing: border-box; 100 | } 101 | 102 | .j-input-text:focus, 103 | .j-input-select:focus, 104 | .j-input-textarea:focus, 105 | .j-input-date:focus, 106 | .j-input-number:focus, 107 | .j-input-html-div:focus { 108 | outline: 1px solid #007bff; 109 | border-color: #007bff; 110 | } 111 | 112 | .j-input-text:disabled { 113 | border-color: #f0f5ff; 114 | background-color: #f5f8ff; 115 | } 116 | 117 | .j-input-text::placeholder { 118 | color: silver; 119 | } 120 | 121 | .j-input-textarea { 122 | margin: 2px 0; 123 | resize: vertical; 124 | min-height: 60px; 125 | } 126 | 127 | /* ===== Radio & Checkbox ===== */ 128 | .j-input-radio, 129 | .j-input-checkbox { 130 | width: auto; 131 | height: auto; 132 | margin-left: 6px; 133 | vertical-align: middle; 134 | } 135 | 136 | .j-input-radio-label, 137 | .j-input-radio { 138 | vertical-align: middle; 139 | } 140 | 141 | .j-input-radio-label { 142 | margin-left: 5px; 143 | } 144 | 145 | /* ===== HTML Editor ===== */ 146 | .j-input-html-div { 147 | padding: 8px; 148 | margin: 3px 2px; 149 | border: 2px solid #e6e6e6; 150 | border-radius: 5px; 151 | min-height: 60px; 152 | } 153 | 154 | .j-input-html { 155 | width: 0; 156 | height: 0; 157 | padding: 0; 158 | border-width: 0; 159 | } 160 | 161 | /* ===== Buttons - مدرن و زیبا ===== */ 162 | .j-add-array-item, 163 | .j-remove-array-item { 164 | display: inline-flex; 165 | align-items: center; 166 | justify-content: center; 167 | width: 24px; 168 | height: 24px; 169 | border-radius: 50%; 170 | cursor: pointer; 171 | font-weight: bold; 172 | font-size: 14px; 173 | line-height: 1; 174 | transition: all 0.2s ease; 175 | margin: 1px; 176 | border: 2px solid; 177 | background: white; 178 | flex-shrink: 0; 179 | } 180 | 181 | .j-add-array-item { color: #28a745; border-color: #28a745; } 182 | .j-add-array-item:hover { background: #28a745; color: white; transform: scale(1.1); } 183 | .j-remove-array-item { color: #dc3545; border-color: #dc3545; } 184 | .j-remove-array-item:hover { background: #dc3545; color: white; transform: scale(1.1); } 185 | .j-remove-array-item:before { content: "×"; } 186 | .j-add-array-item:before { content: "+"; } 187 | 188 | /* ===== Expand/Collapse Button ===== */ 189 | .j-ec { 190 | display: inline-flex; 191 | align-items: center; 192 | justify-content: center; 193 | width: 18px; 194 | height: 18px; 195 | background: #6c757d; 196 | color: white; 197 | border-radius: 3px; 198 | cursor: pointer; 199 | font-size: 12px; 200 | font-weight: bold; 201 | margin-left: 8px; 202 | transition: all 0.2s ease; 203 | } 204 | 205 | .j-ec:hover { background: #495057; transform: scale(1.05); } 206 | 207 | /* ===== Nested Containers - Indentation ===== */ 208 | .j-nested-1 .j-field-row { padding-right: 20px; } 209 | .j-nested-2 .j-field-row { padding-right: 40px; } 210 | .j-nested-3 .j-field-row { padding-right: 60px; } 211 | 212 | /* ===== Array Items ===== */ 213 | .j-array-container { border: 1px solid #e9ecef; border-radius: 5px; margin-bottom: 8px; background: white; } 214 | .j-array-header { background: #f8f9fa; padding: 8px 12px; border-bottom: 1px solid #e9ecef; display: flex; align-items: center; gap: 8px; } 215 | .j-array-body { padding: 8px; } 216 | .j-array-item { border: 1px solid #e9ecef; border-radius: 4px; margin-bottom: 6px; padding: 8px; background: #fefefe; position: relative; } 217 | 218 | /* ===== Object Containers ===== */ 219 | .j-object-container { border: 1px solid #e9ecef; border-radius: 5px; margin-bottom: 8px; background: white; } 220 | .j-object-header { background: #f8f9fa; padding: 8px 12px; border-bottom: 1px solid #e9ecef; display: flex; align-items: center; gap: 8px; font-weight: bold; font-size: 14px; } 221 | .j-object-body { padding: 8px; } 222 | 223 | /* ===== Collapse State ===== */ 224 | .j-collapsed { display: none !important; } 225 | 226 | /* ===== Spacer ===== */ 227 | .j-spacer-row { min-height: 5px; background-color: #e8f2ff; margin: 10px 0; border-radius: 3px; } 228 | 229 | /* ===== Helper Classes ===== */ 230 | .j-inline-help { margin-top: 4px; font-size: 11px; color: #6c757d; font-style: italic; } 231 | .j-validation-help { margin-top: 4px; font-size: 11px; color: #fd7e14; } 232 | .j-required-star { color: #dc3545; font-weight: bold; margin-right: 3px; } 233 | 234 | /* ===== Validation States ===== */ 235 | .j-input[data-is-valid="false"] { border-color: #dc3545 !important; box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25); } 236 | .j-input[data-is-valid="true"] + .j-validation-help { display: none; } 237 | 238 | /* ===== Responsive Design ===== */ 239 | @media (max-width: 768px) { 240 | .j-label-col { flex: 0 0 140px; min-width: 100px; font-size: 11px; } 241 | .j-action-col { flex: 0 0 28px; min-width: 28px; } 242 | .j-add-array-item, .j-remove-array-item { width: 20px; height: 20px; font-size: 12px; } 243 | .j-field-row { padding: 3px 6px; min-height: 28px; } 244 | } 245 | 246 | @media (max-width: 480px) { 247 | .j-field-row { flex-direction: column; align-items: stretch; gap: 4px; } 248 | .j-label-col { flex: none; width: 100%; max-width: none; text-align: right; border-bottom: 1px solid #eee; padding-bottom: 4px; margin-bottom: 4px; } 249 | .j-input-col { flex: none; width: 100%; } 250 | .j-action-col { flex: none; width: 100%; justify-content: flex-end; padding-top: 4px; } 251 | } 252 | 253 | /* ===== Legacy Table Support (for backwards compatibility) ===== */ 254 | table.j-container { display: table; width: 100%; border-collapse: collapse; } 255 | table.j-container td { padding: 4px 8px; vertical-align: top; } 256 | table.j-container .j-title-col { width: 180px; max-width: 200px; min-width: 120px; } 257 | table.j-container .j-body-col { width: auto; } 258 | table.j-container .j-action-col { width: 32px; text-align: center; } 259 | -------------------------------------------------------------------------------- /src/core/JsonToForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonToForm - Modern jQuery plugin for converting JSON Schema to HTML forms 3 | * 4 | * @class JsonToForm 5 | * @version 2.0.0 6 | */ 7 | class JsonToForm { 8 | 9 | /** 10 | * Constructor - Initialize the JsonToForm instance 11 | * @param {jQuery} element - The jQuery element to render the form 12 | * @param {Object} options - Configuration options 13 | */ 14 | constructor(element, options = {}) { 15 | this.element = element; 16 | // Initialize utils first so config merging can use it if needed 17 | this.utils = new JsonFormUtils(this); 18 | this.config = this._initializeConfig(options); 19 | this.level = 0; 20 | this.arrayTemplates = {}; 21 | 22 | // Initialize submodules 23 | this.renderer = new JsonFormRenderer(this); 24 | this.validator = new JsonFormValidator(this); 25 | this.eventHandler = new JsonFormEventHandler(this); 26 | 27 | this._initialize(); 28 | } 29 | 30 | /** 31 | * Initialize configuration with defaults 32 | * @private 33 | */ 34 | _initializeConfig(options) { 35 | const defaults = { 36 | expandingLevel: -1, // -1: expand all levels 37 | value: {}, 38 | schema: {}, 39 | autoTrimValues: true, 40 | indenting: 5, 41 | radioNullCaption: 'null', 42 | selectNullCaption: '', 43 | treeExpandCollapseButton: true, 44 | theme: 'default', // New: theme support 45 | responsive: true, // New: responsive design 46 | validation: { 47 | realTime: true, // New: real-time validation 48 | showHints: true, // New: show validation hints 49 | customRules: {} // New: custom validation rules 50 | }, 51 | callbacks: { 52 | afterValueChanged: null, 53 | afterWidgetCreated: null, 54 | beforeValidation: null, 55 | afterValidation: null 56 | } 57 | }; 58 | 59 | return this.utils.deepMerge(defaults, options); 60 | } 61 | 62 | /** 63 | * Initialize the widget 64 | * @private 65 | */ 66 | _initialize() { 67 | this.level = 0; 68 | this.arrayTemplates = {}; 69 | 70 | const widgetContent = this.renderer.renderSchemaNode(this.config.schema, ""); 71 | this.element.html(widgetContent); 72 | 73 | this._initValuePaths(); 74 | this.setValue(this.config.value); 75 | this.eventHandler.initialize(); 76 | this.validator.validateAll(); 77 | 78 | // Execute callback if provided 79 | if (this.config.callbacks.afterWidgetCreated) { 80 | this.config.callbacks.afterWidgetCreated(this.config.value, this.config.schema); 81 | } 82 | } 83 | 84 | /** 85 | * Initialize data paths for all form elements 86 | * @private 87 | */ 88 | _initValuePaths() { 89 | this.element.find("[data-value-name]").each((index, element) => { 90 | const $element = $(element); 91 | const dataPath = this.utils.generatePath($element); 92 | $element.attr("data-path", dataPath); 93 | 94 | if (dataPath) { 95 | const elementId = this.utils.getIdBasedDataPath(dataPath, this.element.attr("id")); 96 | $element.attr("id", elementId); 97 | $element.parents("table:first").find("label:first").attr("for", elementId); 98 | } 99 | }); 100 | } 101 | 102 | /** 103 | * Public API Methods 104 | */ 105 | 106 | /** 107 | * Check if the form is valid 108 | * @returns {boolean} True if form is valid 109 | */ 110 | isValid() { 111 | return this.validator.isFormValid(); 112 | } 113 | 114 | /** 115 | * Get the current schema 116 | * @returns {Object} The JSON schema 117 | */ 118 | getSchema() { 119 | return this.config.schema; 120 | } 121 | 122 | /** 123 | * Get the current form values 124 | * @returns {Object} The form values as JSON 125 | */ 126 | getValue() { 127 | return this.config.value; 128 | } 129 | 130 | /** 131 | * Set form values 132 | * @param {Object} value - The new values to set 133 | */ 134 | setValue(value) { 135 | this.config.value = value; 136 | this._addArrayItemsToDOM(); 137 | this._populateFormValues(); 138 | } 139 | 140 | /** 141 | * Update the schema and re-render 142 | * @param {Object} schema - The new schema 143 | */ 144 | updateSchema(schema) { 145 | this.config.schema = schema; 146 | this._initialize(); 147 | } 148 | 149 | /** 150 | * Destroy the widget and clean up event listeners 151 | */ 152 | destroy() { 153 | this.eventHandler.destroy(); 154 | this.element.empty(); 155 | } 156 | 157 | /** 158 | * Private helper methods 159 | */ 160 | 161 | /** 162 | * Add array items to DOM based on current values 163 | * @private 164 | */ 165 | _addArrayItemsToDOM() { 166 | const arrayNodes = this.element.find('[data-array-loaded="false"]'); 167 | if (arrayNodes.length === 0) { 168 | this._initValuePaths(); 169 | return; 170 | } 171 | 172 | arrayNodes.each((index, element) => { 173 | const $addBtn = $(element); 174 | const dataPath = this._getArrayDataPath($addBtn); 175 | const arrayValue = this.utils.getNestedValue(this.config.value, dataPath); 176 | 177 | if (arrayValue && Array.isArray(arrayValue)) { 178 | arrayValue.forEach((item, idx) => { 179 | this.renderer.addArrayItem($addBtn, false, idx); 180 | }); 181 | } 182 | 183 | $addBtn.attr("data-array-loaded", "true"); 184 | }); 185 | 186 | // Recursively handle nested arrays 187 | this._addArrayItemsToDOM(); 188 | } 189 | 190 | /** 191 | * Get array data path for add button 192 | * @private 193 | */ 194 | _getArrayDataPath($addBtn) { 195 | let dataPath = $addBtn.parents("tr:first").next("tr").find("td:first").attr("data-path"); 196 | 197 | if (!dataPath) { 198 | const $container = $addBtn.parents("tr:first").next("tr").find("td:first"); 199 | dataPath = this.utils.generatePath($container); 200 | $container.attr("data-path", dataPath); 201 | } 202 | 203 | return dataPath; 204 | } 205 | 206 | /** 207 | * Populate form values from current data 208 | * @private 209 | */ 210 | _populateFormValues() { 211 | this.element.find("input[data-path], select[data-path], textarea[data-path]").each((index, element) => { 212 | const $element = $(element); 213 | const dataPath = $element.attr("data-path"); 214 | const value = this.utils.getNestedValue(this.config.value, dataPath); 215 | 216 | this._setElementValue($element, value); 217 | }); 218 | } 219 | 220 | /** 221 | * Set individual element value 222 | * @private 223 | */ 224 | _setElementValue($element, value) { 225 | const tagName = $element.prop("tagName").toLowerCase(); 226 | const inputType = $element.prop("type") ? $element.prop("type").toLowerCase() : ""; 227 | 228 | if (tagName === "input" && inputType === "checkbox") { 229 | $element.prop("checked", value === true); 230 | } else if (tagName === "input" && inputType === "radio") { 231 | this.element.find(`[data-path="${$element.attr("data-path")}"][value="${value}"]`).prop("checked", true); 232 | } else { 233 | const processedValue = this.config.autoTrimValues && value ? value.toString().trim() : value; 234 | $element.val(processedValue || ''); 235 | 236 | // Handle HTML editor 237 | if ($element.hasClass("j-input-html")) { 238 | $element.parents(":first").find(".j-input-html-div:first").html($element.val()); 239 | } 240 | } 241 | } 242 | } 243 | 244 | // Export for module systems or global usage 245 | if (typeof module !== 'undefined' && module.exports) { 246 | module.exports = JsonToForm; 247 | } else if (typeof window !== 'undefined') { 248 | window.JsonToForm = JsonToForm; 249 | } -------------------------------------------------------------------------------- /demo-simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 230 | 231 | 238 | 239 | 240 |
228 |
229 |
  232 | json value : 233 |

234 | 
235 |                 Is Valid :
236 |                 
true
237 |
241 | 242 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /v1/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 232 | 233 | 240 | 241 | 242 |
230 |
231 |
  234 | json value : 235 |

236 | 
237 |                 Is Valid :
238 |                 
true
239 |
243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /src/styles/jsonToForm.clean.css: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonToForm v2.0 - Clean & Modern CSS 3 | * Minimal, clean design with proper container management 4 | */ 5 | 6 | :root { 7 | /* Modern Clean Palette */ 8 | --jtf-primary: #4f46e5; 9 | --jtf-primary-light: rgba(79, 70, 229, 0.1); 10 | --jtf-success: #16a34a; 11 | --jtf-danger: #dc2626; 12 | --jtf-warning: #f59e0b; 13 | 14 | /* Neutral Grays */ 15 | --jtf-gray-50: #f8fafc; 16 | --jtf-gray-100: #f1f5f9; 17 | --jtf-gray-200: #e2e8f0; 18 | --jtf-gray-300: #cbd5e1; 19 | --jtf-gray-400: #94a3b8; 20 | --jtf-gray-500: #64748b; 21 | --jtf-gray-600: #475569; 22 | --jtf-gray-700: #334155; 23 | --jtf-gray-800: #1e293b; 24 | --jtf-gray-900: #0f172a; 25 | 26 | /* Spacing Scale */ 27 | --jtf-space-1: 0.25rem; 28 | --jtf-space-2: 0.5rem; 29 | --jtf-space-3: 0.75rem; 30 | --jtf-space-4: 1rem; 31 | --jtf-space-5: 1.25rem; 32 | --jtf-space-6: 1.5rem; 33 | --jtf-space-8: 2rem; 34 | 35 | /* Typography */ 36 | --jtf-text-xs: 0.75rem; 37 | --jtf-text-sm: 0.875rem; 38 | --jtf-text-base: 1rem; 39 | --jtf-text-lg: 1.125rem; 40 | 41 | /* Radius */ 42 | --jtf-radius-sm: 0.375rem; 43 | --jtf-radius: 0.5rem; 44 | --jtf-radius-lg: 0.75rem; 45 | 46 | /* Shadows */ 47 | --jtf-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); 48 | --jtf-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 49 | --jtf-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 50 | } 51 | 52 | /* Base Container */ 53 | .j-container { 54 | font-family: ui-sans-serif, system-ui, sans-serif; 55 | font-size: var(--jtf-text-base); 56 | line-height: 1.6; 57 | color: var(--jtf-gray-800); 58 | background: white; 59 | border-radius: var(--jtf-radius); 60 | border: 1px solid var(--jtf-gray-200); 61 | padding: var(--jtf-space-6); 62 | margin-bottom: var(--jtf-space-4); 63 | max-width: 100%; 64 | overflow: hidden; 65 | } 66 | 67 | /* Object & Array Headers */ 68 | .j-oject-title-row, 69 | .j-array-title-row { 70 | background: var(--jtf-gray-50) !important; 71 | } 72 | 73 | .j-oject-title-row td, 74 | .j-array-title-row td { 75 | background: var(--jtf-gray-50) !important; 76 | color: var(--jtf-gray-700) !important; 77 | font-weight: 500 !important; 78 | font-size: var(--jtf-text-sm) !important; 79 | padding: var(--jtf-space-3) var(--jtf-space-4) !important; 80 | border-bottom: 1px solid var(--jtf-gray-200) !important; 81 | } 82 | 83 | /* Field Rows */ 84 | .j-oject-value-row td, 85 | .j-array-value-row td { 86 | padding: var(--jtf-space-3) var(--jtf-space-4) !important; 87 | vertical-align: top !important; 88 | } 89 | 90 | .j-title-col { 91 | width: 200px !important; 92 | min-width: 200px !important; 93 | font-weight: 500 !important; 94 | color: var(--jtf-gray-700) !important; 95 | font-size: var(--jtf-text-sm) !important; 96 | padding-right: var(--jtf-space-4) !important; 97 | } 98 | 99 | .j-body-col { 100 | width: auto !important; 101 | min-width: 0 !important; 102 | word-wrap: break-word !important; 103 | overflow-wrap: break-word !important; 104 | } 105 | 106 | .j-sep-col { 107 | width: var(--jtf-space-2) !important; 108 | } 109 | 110 | /* Form Inputs - Base Styles */ 111 | .j-input, 112 | .j-input-text, 113 | .j-input-textarea, 114 | .j-input-select, 115 | .j-input-number, 116 | .j-input-email, 117 | .j-input-tel, 118 | .j-input-url, 119 | .j-input-date, 120 | .j-input-time, 121 | .j-input-datetime-local { 122 | width: 100% !important; 123 | max-width: 100% !important; 124 | min-width: 0 !important; 125 | padding: var(--jtf-space-3) !important; 126 | font-size: var(--jtf-text-sm) !important; 127 | line-height: 1.5 !important; 128 | color: var(--jtf-gray-800) !important; 129 | background: white !important; 130 | border: 1px solid var(--jtf-gray-300) !important; 131 | border-radius: var(--jtf-radius-sm) !important; 132 | transition: all 0.15s ease !important; 133 | box-sizing: border-box !important; 134 | font-family: inherit !important; 135 | } 136 | 137 | .j-input:focus, 138 | .j-input-text:focus, 139 | .j-input-textarea:focus, 140 | .j-input-select:focus, 141 | .j-input-number:focus, 142 | .j-input-email:focus, 143 | .j-input-tel:focus, 144 | .j-input-url:focus, 145 | .j-input-date:focus, 146 | .j-input-time:focus, 147 | .j-input-datetime-local:focus { 148 | outline: none !important; 149 | border-color: var(--jtf-primary) !important; 150 | box-shadow: 0 0 0 3px var(--jtf-primary-light) !important; 151 | } 152 | 153 | /* Textarea Specific */ 154 | .j-input-textarea { 155 | min-height: 80px !important; 156 | resize: vertical !important; 157 | } 158 | 159 | /* Checkbox & Radio */ 160 | .j-input-checkbox, 161 | .j-input-radio { 162 | width: auto !important; 163 | margin-right: var(--jtf-space-2) !important; 164 | cursor: pointer !important; 165 | } 166 | 167 | /* Select Dropdown */ 168 | .j-input-select { 169 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e") !important; 170 | background-position: right var(--jtf-space-3) center !important; 171 | background-repeat: no-repeat !important; 172 | background-size: 16px 16px !important; 173 | padding-right: 2.5rem !important; 174 | cursor: pointer !important; 175 | } 176 | 177 | /* Buttons */ 178 | .j-add-array-item, 179 | .j-remove-array-item, 180 | .j-ec { 181 | display: inline-flex !important; 182 | align-items: center !important; 183 | justify-content: center !important; 184 | padding: var(--jtf-space-1) var(--jtf-space-3) !important; 185 | font-size: var(--jtf-text-xs) !important; 186 | font-weight: 500 !important; 187 | border: none !important; 188 | border-radius: var(--jtf-radius-sm) !important; 189 | cursor: pointer !important; 190 | transition: all 0.15s ease !important; 191 | text-decoration: none !important; 192 | line-height: 1.5 !important; 193 | } 194 | 195 | .j-add-array-item { 196 | background: var(--jtf-success) !important; 197 | color: white !important; 198 | } 199 | 200 | .j-add-array-item:hover { 201 | background: #15803d !important; 202 | box-shadow: var(--jtf-shadow-sm) !important; 203 | } 204 | 205 | .j-remove-array-item { 206 | background: var(--jtf-danger) !important; 207 | color: white !important; 208 | } 209 | 210 | .j-remove-array-item:hover { 211 | background: #b91c1c !important; 212 | box-shadow: var(--jtf-shadow-sm) !important; 213 | } 214 | 215 | .j-ec { 216 | background: var(--jtf-gray-100) !important; 217 | color: var(--jtf-gray-600) !important; 218 | border: 1px solid var(--jtf-gray-300) !important; 219 | } 220 | 221 | .j-ec:hover { 222 | background: var(--jtf-gray-200) !important; 223 | color: var(--jtf-gray-700) !important; 224 | } 225 | 226 | /* Help Text */ 227 | .j-inline-help { 228 | color: var(--jtf-gray-500) !important; 229 | font-size: var(--jtf-text-xs) !important; 230 | margin-top: var(--jtf-space-1) !important; 231 | line-height: 1.4 !important; 232 | } 233 | 234 | .j-validation-help { 235 | color: var(--jtf-warning) !important; 236 | font-size: var(--jtf-text-xs) !important; 237 | margin-top: var(--jtf-space-1) !important; 238 | line-height: 1.4 !important; 239 | } 240 | 241 | .j-validation-message { 242 | color: var(--jtf-danger) !important; 243 | font-size: var(--jtf-text-xs) !important; 244 | margin-top: var(--jtf-space-1) !important; 245 | line-height: 1.4 !important; 246 | } 247 | 248 | .j-required-star { 249 | color: var(--jtf-danger) !important; 250 | font-weight: 600 !important; 251 | } 252 | 253 | /* Validation States */ 254 | .j-input[data-is-valid="false"], 255 | .j-input-text[data-is-valid="false"], 256 | .j-input-textarea[data-is-valid="false"], 257 | .j-input-select[data-is-valid="false"] { 258 | border-color: var(--jtf-danger) !important; 259 | box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1) !important; 260 | } 261 | 262 | .j-input[data-is-valid="true"], 263 | .j-input-text[data-is-valid="true"], 264 | .j-input-textarea[data-is-valid="true"], 265 | .j-input-select[data-is-valid="true"] { 266 | border-color: var(--jtf-success) !important; 267 | } 268 | 269 | /* Table Layout Fixes */ 270 | table.j-table { 271 | width: 100% !important; 272 | table-layout: fixed !important; 273 | border-collapse: collapse !important; 274 | } 275 | 276 | /* Responsive */ 277 | @media (max-width: 768px) { 278 | .j-container { 279 | padding: var(--jtf-space-4) !important; 280 | font-size: var(--jtf-text-sm) !important; 281 | } 282 | 283 | .j-title-col { 284 | width: 120px !important; 285 | min-width: 120px !important; 286 | } 287 | 288 | .j-oject-title-row td, 289 | .j-array-title-row td { 290 | padding: var(--jtf-space-2) var(--jtf-space-3) !important; 291 | } 292 | 293 | .j-oject-value-row td, 294 | .j-array-value-row td { 295 | padding: var(--jtf-space-2) var(--jtf-space-3) !important; 296 | } 297 | } 298 | 299 | /* Utility Classes */ 300 | .j-text-muted { 301 | color: var(--jtf-gray-500) !important; 302 | } 303 | 304 | .j-border { 305 | border: 1px solid var(--jtf-gray-200) !important; 306 | } 307 | 308 | .j-rounded { 309 | border-radius: var(--jtf-radius) !important; 310 | } 311 | 312 | .j-shadow { 313 | box-shadow: var(--jtf-shadow) !important; 314 | } -------------------------------------------------------------------------------- /jsonToForm/jsonToForm.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonToForm v2.0.0 TypeScript Definitions 3 | * 4 | * Provides type safety and IntelliSense support for JsonToForm plugin 5 | */ 6 | 7 | declare namespace JsonToForm { 8 | 9 | // ===== INTERFACES ===== 10 | 11 | interface JsonSchema { 12 | type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'spacer' | 'email' | 'tel' | 'url' | 'date' | 'color'; 13 | title?: string; 14 | description?: string; 15 | 16 | // String/Number constraints 17 | minLength?: number; 18 | maxLength?: number; 19 | minimum?: number; 20 | maximum?: number; 21 | pattern?: string; 22 | 23 | // Array constraints 24 | items?: JsonSchema; 25 | 26 | // Object constraints 27 | properties?: { [key: string]: JsonSchema }; 28 | required?: string[]; 29 | 30 | // Enum values 31 | enum?: any[]; 32 | 33 | // Reference to definitions 34 | $ref?: string; 35 | 36 | // UI configuration 37 | ui?: UIConfiguration; 38 | 39 | // Schema definitions 40 | definitions?: { [key: string]: JsonSchema }; 41 | } 42 | 43 | interface UIConfiguration { 44 | editor?: 'text' | 'textarea' | 'number' | 'email' | 'tel' | 'url' | 'date' | 'color' | 'checkbox' | 'radio' | 'select' | 'html'; 45 | class?: string; 46 | disabled?: boolean; 47 | placeholder?: string; 48 | placeholderHint?: string; 49 | hoverHint?: string; 50 | inlineHint?: string; 51 | validationHint?: string; 52 | validationRule?: string; 53 | rows?: number; // For textarea 54 | [key: string]: any; // Allow additional custom properties 55 | } 56 | 57 | interface ValidationRule { 58 | pattern?: RegExp; 59 | validate?: (value: any, element?: JQuery) => boolean; 60 | message: string; 61 | } 62 | 63 | interface ValidationResult { 64 | isValid: boolean; 65 | errors: string[]; 66 | element: JQuery; 67 | } 68 | 69 | interface ValidationConfiguration { 70 | realTime?: boolean; 71 | showHints?: boolean; 72 | customRules?: { [ruleName: string]: ValidationRule }; 73 | } 74 | 75 | interface CallbackConfiguration { 76 | afterValueChanged?: (value: any, schema: JsonSchema) => void; 77 | afterWidgetCreated?: (value: any, schema: JsonSchema) => void; 78 | beforeValidation?: (element: JQuery, result: ValidationResult) => void; 79 | afterValidation?: (element: JQuery, result: ValidationResult) => void; 80 | } 81 | 82 | interface Configuration { 83 | // Core settings 84 | schema?: JsonSchema; 85 | value?: any; 86 | 87 | // Display settings 88 | expandingLevel?: number; 89 | renderFirstLevel?: boolean; 90 | indenting?: number; 91 | treeExpandCollapseButton?: boolean; 92 | 93 | // Form behavior 94 | autoTrimValues?: boolean; 95 | radioNullCaption?: string; 96 | selectNullCaption?: string; 97 | 98 | // Theme and responsiveness 99 | theme?: 'default' | 'dark' | string; 100 | responsive?: boolean; 101 | 102 | // Validation 103 | validation?: ValidationConfiguration; 104 | 105 | // Callbacks 106 | callbacks?: CallbackConfiguration; 107 | } 108 | 109 | interface ArrayTemplate { 110 | htmlTemplate: string; 111 | dataTemplate: any; 112 | } 113 | 114 | interface ValidationError { 115 | field: string; 116 | message: string; 117 | element: JQuery; 118 | } 119 | 120 | // ===== CLASSES ===== 121 | 122 | class JsonFormUtils { 123 | constructor(jsonToForm: JsonToFormInstance); 124 | 125 | deepMerge(target: object, source: object): object; 126 | isObject(value: any): boolean; 127 | generatePath(element: JQuery): string; 128 | getIdBasedDataPath(dataPath: string, containerId: string): string; 129 | getNestedValue(obj: object, path: string): any; 130 | setNestedValue(obj: object, path: string, value: any): void; 131 | ensureDataPath(obj: object, dataPath: string): void; 132 | jsonEscape(str: string): string; 133 | replaceAll(source: string, find: string, replace: string): string; 134 | escapeRegExp(string: string): string; 135 | fixNullUndefined(value: any, defaultValue: any): any; 136 | getUISetting(schemaNode: JsonSchema, settingName: string, defaultValue: any): any; 137 | getArrayType(schemaNode: JsonSchema): string; 138 | generateSpacer(level: number): string; 139 | generateExpandCollapseButton(type: string): string; 140 | generateTitle(schemaNode: JsonSchema, schemaName: string): string; 141 | escapeHtml(text: string): string; 142 | debounce(func: Function, wait: number): Function; 143 | isEmpty(value: any): boolean; 144 | } 145 | 146 | class JsonFormValidator { 147 | validationRules: { [ruleName: string]: ValidationRule }; 148 | 149 | constructor(jsonToForm: JsonToFormInstance); 150 | 151 | validateAll(): void; 152 | validateInput(element: JQuery): ValidationResult; 153 | isFormValid(): boolean; 154 | getAllErrors(): ValidationError[]; 155 | clearValidationMessages(): void; 156 | addCustomRule(name: string, rule: ValidationRule): void; 157 | } 158 | 159 | class JsonFormEventHandler { 160 | constructor(jsonToForm: JsonToFormInstance); 161 | 162 | initialize(): void; 163 | destroy(): void; 164 | triggerChange(element: JQuery): void; 165 | on(eventName: string, handler: Function): void; 166 | off(eventName: string, handler?: Function): void; 167 | trigger(eventName: string, data?: any[]): void; 168 | } 169 | 170 | class JsonFormRenderer { 171 | constructor(jsonToForm: JsonToFormInstance); 172 | 173 | renderSchemaNode(schemaNode: JsonSchema, schemaName: string, requiredItems?: string[]): string; 174 | addArrayItem(addButton: JQuery, needsReinitialization: boolean, itemIndex: number | null): void; 175 | } 176 | 177 | class JsonToFormInstance { 178 | element: JQuery; 179 | config: Configuration; 180 | level: number; 181 | arrayTemplates: { [templateId: string]: ArrayTemplate }; 182 | 183 | renderer: JsonFormRenderer; 184 | validator: JsonFormValidator; 185 | eventHandler: JsonFormEventHandler; 186 | utils: JsonFormUtils; 187 | 188 | constructor(element: JQuery, options?: Configuration); 189 | 190 | // Public API 191 | isValid(): boolean; 192 | getSchema(): JsonSchema; 193 | getValue(): any; 194 | setValue(value: any): void; 195 | updateSchema(schema: JsonSchema): void; 196 | destroy(): void; 197 | } 198 | } 199 | 200 | // ===== JQUERY PLUGIN INTERFACE ===== 201 | 202 | interface JQuery { 203 | /** 204 | * Initialize JsonToForm plugin on selected elements 205 | * @param options Configuration options 206 | * @returns JsonToForm instance or jQuery object for chaining 207 | */ 208 | jsonToForm(options?: JsonToForm.Configuration): JsonToForm.JsonToFormInstance | JQuery; 209 | } 210 | 211 | interface JQueryStatic { 212 | fn: { 213 | jsonToForm: { 214 | /** 215 | * Plugin version 216 | */ 217 | version: string; 218 | 219 | /** 220 | * Default configuration options 221 | */ 222 | defaults: JsonToForm.Configuration; 223 | 224 | /** 225 | * Add global validation rule 226 | * @param name Rule name 227 | * @param rule Rule definition 228 | */ 229 | addValidationRule(name: string, rule: JsonToForm.ValidationRule): void; 230 | 231 | /** 232 | * Set global theme 233 | * @param themeName Theme name 234 | */ 235 | setTheme(themeName: string): void; 236 | } 237 | } 238 | } 239 | 240 | // ===== GLOBAL CLASSES (for direct usage) ===== 241 | 242 | declare global { 243 | interface Window { 244 | JsonToForm: typeof JsonToForm.JsonToFormInstance; 245 | JsonFormRenderer: typeof JsonToForm.JsonFormRenderer; 246 | JsonFormValidator: typeof JsonToForm.JsonFormValidator; 247 | JsonFormEventHandler: typeof JsonToForm.JsonFormEventHandler; 248 | JsonFormUtils: typeof JsonToForm.JsonFormUtils; 249 | 250 | JsonToFormClasses: { 251 | JsonToForm: typeof JsonToForm.JsonToFormInstance; 252 | JsonFormRenderer: typeof JsonToForm.JsonFormRenderer; 253 | JsonFormValidator: typeof JsonToForm.JsonFormValidator; 254 | JsonFormEventHandler: typeof JsonToForm.JsonFormEventHandler; 255 | JsonFormUtils: typeof JsonToForm.JsonFormUtils; 256 | }; 257 | } 258 | } 259 | 260 | // ===== MODULE EXPORTS (for ES6/CommonJS) ===== 261 | 262 | export = JsonToForm; 263 | export as namespace JsonToForm; -------------------------------------------------------------------------------- /jsonToForm/jsonToForm_new.css: -------------------------------------------------------------------------------- 1 | /* ===== JsonToForm Modern CSS با Flexbox Layout ===== */ 2 | 3 | /* کنترل کلی overflow */ 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | overflow-x: hidden; 10 | } 11 | 12 | #jsonEditor { 13 | max-width: 100%; 14 | overflow-x: hidden; 15 | } 16 | 17 | .form-panel { 18 | overflow-x: hidden; 19 | } 20 | 21 | /* ===== Container اصلی - حالا div بجای table ===== */ 22 | .j-container { 23 | width: 100%; 24 | max-width: 100%; 25 | margin-bottom: 8px; 26 | border-radius: 5px; 27 | overflow: hidden; 28 | } 29 | 30 | /* ===== Row Layout با Flexbox ===== */ 31 | .j-field-row { 32 | display: flex; 33 | align-items: flex-start; 34 | min-height: 32px; 35 | padding: 4px 8px; 36 | border-bottom: 1px solid #f0f0f0; 37 | gap: 8px; 38 | } 39 | 40 | .j-field-row:last-child { 41 | border-bottom: none; 42 | } 43 | 44 | /* ===== Header Rows ===== */ 45 | .j-header-row { 46 | background-color: gainsboro; 47 | font-weight: bold; 48 | font-size: 14px; 49 | padding: 8px; 50 | border-bottom: 2px solid #ddd; 51 | } 52 | 53 | /* ===== Columns با Flexbox ===== */ 54 | .j-label-col { 55 | flex: 0 0 180px; 56 | min-width: 120px; 57 | max-width: 200px; 58 | padding: 6px 8px; 59 | font-size: 12px; 60 | font-weight: 500; 61 | color: #333; 62 | text-align: right; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | white-space: nowrap; 66 | align-self: flex-start; 67 | } 68 | 69 | .j-input-col { 70 | flex: 1; 71 | min-width: 0; /* مهم برای flexbox */ 72 | padding: 4px; 73 | } 74 | 75 | .j-action-col { 76 | flex: 0 0 32px; 77 | min-width: 32px; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | padding: 2px; 82 | } 83 | 84 | /* ===== Input Styles ===== */ 85 | .j-input-text, 86 | .j-input-select, 87 | .j-input-textarea, 88 | .j-input-date, 89 | .j-input-number, 90 | .j-input-html, 91 | .j-input-email, 92 | .j-input-tel { 93 | width: 100%; 94 | border: 2px solid #e6e6e6; 95 | padding: 5px; 96 | border-radius: 5px; 97 | background-color: #fefefe; 98 | font-size: 12px; 99 | box-sizing: border-box; 100 | } 101 | 102 | .j-input-text:focus, 103 | .j-input-select:focus, 104 | .j-input-textarea:focus, 105 | .j-input-date:focus, 106 | .j-input-number:focus, 107 | .j-input-html-div:focus { 108 | outline: 1px solid #007bff; 109 | border-color: #007bff; 110 | } 111 | 112 | .j-input-text:disabled { 113 | border-color: #f0f5ff; 114 | background-color: #f5f8ff; 115 | } 116 | 117 | .j-input-text::placeholder { 118 | color: silver; 119 | } 120 | 121 | .j-input-textarea { 122 | margin: 2px 0; 123 | resize: vertical; 124 | min-height: 60px; 125 | } 126 | 127 | /* ===== Radio & Checkbox ===== */ 128 | .j-input-radio, 129 | .j-input-checkbox { 130 | width: auto; 131 | height: auto; 132 | margin-left: 6px; 133 | vertical-align: middle; 134 | } 135 | 136 | .j-input-radio-label, 137 | .j-input-radio { 138 | vertical-align: middle; 139 | } 140 | 141 | .j-input-radio-label { 142 | margin-left: 5px; 143 | } 144 | 145 | /* ===== HTML Editor ===== */ 146 | .j-input-html-div { 147 | padding: 8px; 148 | margin: 3px 2px; 149 | border: 2px solid #e6e6e6; 150 | border-radius: 5px; 151 | min-height: 60px; 152 | } 153 | 154 | .j-input-html { 155 | width: 0; 156 | height: 0; 157 | padding: 0; 158 | border-width: 0; 159 | } 160 | 161 | /* ===== Buttons - مدرن و زیبا ===== */ 162 | .j-add-array-item, 163 | .j-remove-array-item { 164 | display: inline-flex; 165 | align-items: center; 166 | justify-content: center; 167 | width: 24px; 168 | height: 24px; 169 | border-radius: 50%; 170 | cursor: pointer; 171 | font-weight: bold; 172 | font-size: 14px; 173 | line-height: 1; 174 | transition: all 0.2s ease; 175 | margin: 1px; 176 | border: 2px solid; 177 | background: white; 178 | flex-shrink: 0; 179 | } 180 | 181 | .j-add-array-item { 182 | color: #28a745; 183 | border-color: #28a745; 184 | } 185 | 186 | .j-add-array-item:hover { 187 | background: #28a745; 188 | color: white; 189 | transform: scale(1.1); 190 | } 191 | 192 | .j-remove-array-item { 193 | color: #dc3545; 194 | border-color: #dc3545; 195 | } 196 | 197 | .j-remove-array-item:hover { 198 | background: #dc3545; 199 | color: white; 200 | transform: scale(1.1); 201 | } 202 | 203 | .j-remove-array-item:before { 204 | content: "×"; 205 | } 206 | 207 | .j-add-array-item:before { 208 | content: "+"; 209 | } 210 | 211 | /* ===== Expand/Collapse Button ===== */ 212 | .j-ec { 213 | display: inline-flex; 214 | align-items: center; 215 | justify-content: center; 216 | width: 18px; 217 | height: 18px; 218 | background: #6c757d; 219 | color: white; 220 | border-radius: 3px; 221 | cursor: pointer; 222 | font-size: 12px; 223 | font-weight: bold; 224 | margin-left: 8px; 225 | transition: all 0.2s ease; 226 | } 227 | 228 | .j-ec:hover { 229 | background: #495057; 230 | transform: scale(1.05); 231 | } 232 | 233 | /* ===== Nested Containers - Indentation ===== */ 234 | .j-nested-1 .j-field-row { 235 | padding-right: 20px; 236 | } 237 | 238 | .j-nested-2 .j-field-row { 239 | padding-right: 40px; 240 | } 241 | 242 | .j-nested-3 .j-field-row { 243 | padding-right: 60px; 244 | } 245 | 246 | /* ===== Array Items ===== */ 247 | .j-array-container { 248 | border: 1px solid #e9ecef; 249 | border-radius: 5px; 250 | margin-bottom: 8px; 251 | background: white; 252 | } 253 | 254 | .j-array-header { 255 | background: #f8f9fa; 256 | padding: 8px 12px; 257 | border-bottom: 1px solid #e9ecef; 258 | display: flex; 259 | align-items: center; 260 | gap: 8px; 261 | } 262 | 263 | .j-array-body { 264 | padding: 8px; 265 | } 266 | 267 | .j-array-item { 268 | border: 1px solid #e9ecef; 269 | border-radius: 4px; 270 | margin-bottom: 6px; 271 | padding: 8px; 272 | background: #fefefe; 273 | position: relative; 274 | } 275 | 276 | /* ===== Object Containers ===== */ 277 | .j-object-container { 278 | border: 1px solid #e9ecef; 279 | border-radius: 5px; 280 | margin-bottom: 8px; 281 | background: white; 282 | } 283 | 284 | .j-object-header { 285 | background: #f8f9fa; 286 | padding: 8px 12px; 287 | border-bottom: 1px solid #e9ecef; 288 | display: flex; 289 | align-items: center; 290 | gap: 8px; 291 | font-weight: bold; 292 | font-size: 14px; 293 | } 294 | 295 | .j-object-body { 296 | padding: 8px; 297 | } 298 | 299 | /* ===== Collapse State ===== */ 300 | .j-collapsed { 301 | display: none !important; 302 | } 303 | 304 | /* ===== Spacer ===== */ 305 | .j-spacer-row { 306 | min-height: 5px; 307 | background-color: #e8f2ff; 308 | margin: 10px 0; 309 | border-radius: 3px; 310 | } 311 | 312 | /* ===== Helper Classes ===== */ 313 | .j-inline-help { 314 | margin-top: 4px; 315 | font-size: 11px; 316 | color: #6c757d; 317 | font-style: italic; 318 | } 319 | 320 | .j-validation-help { 321 | margin-top: 4px; 322 | font-size: 11px; 323 | color: #fd7e14; 324 | } 325 | 326 | .j-required-star { 327 | color: #dc3545; 328 | font-weight: bold; 329 | margin-right: 3px; 330 | } 331 | 332 | /* ===== Validation States ===== */ 333 | .j-input[data-is-valid="false"] { 334 | border-color: #dc3545 !important; 335 | box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25); 336 | } 337 | 338 | .j-input[data-is-valid="true"] + .j-validation-help { 339 | display: none; 340 | } 341 | 342 | /* ===== Responsive Design ===== */ 343 | @media (max-width: 768px) { 344 | .j-label-col { 345 | flex: 0 0 140px; 346 | min-width: 100px; 347 | font-size: 11px; 348 | } 349 | 350 | .j-action-col { 351 | flex: 0 0 28px; 352 | min-width: 28px; 353 | } 354 | 355 | .j-add-array-item, 356 | .j-remove-array-item { 357 | width: 20px; 358 | height: 20px; 359 | font-size: 12px; 360 | } 361 | 362 | .j-field-row { 363 | padding: 3px 6px; 364 | min-height: 28px; 365 | } 366 | } 367 | 368 | @media (max-width: 480px) { 369 | .j-field-row { 370 | flex-direction: column; 371 | align-items: stretch; 372 | gap: 4px; 373 | } 374 | 375 | .j-label-col { 376 | flex: none; 377 | width: 100%; 378 | max-width: none; 379 | text-align: right; 380 | border-bottom: 1px solid #eee; 381 | padding-bottom: 4px; 382 | margin-bottom: 4px; 383 | } 384 | 385 | .j-input-col { 386 | flex: none; 387 | width: 100%; 388 | } 389 | 390 | .j-action-col { 391 | flex: none; 392 | width: 100%; 393 | justify-content: flex-end; 394 | padding-top: 4px; 395 | } 396 | } 397 | 398 | /* ===== Legacy Table Support (for backwards compatibility) ===== */ 399 | table.j-container { 400 | display: table; 401 | width: 100%; 402 | border-collapse: collapse; 403 | } 404 | 405 | table.j-container td { 406 | padding: 4px 8px; 407 | vertical-align: top; 408 | } 409 | 410 | table.j-container .j-title-col { 411 | width: 180px; 412 | max-width: 200px; 413 | min-width: 120px; 414 | } 415 | 416 | table.j-container .j-body-col { 417 | width: auto; 418 | } 419 | 420 | table.j-container .j-action-col { 421 | width: 32px; 422 | text-align: center; 423 | } -------------------------------------------------------------------------------- /src/styles/jsonToForm.branded.css: -------------------------------------------------------------------------------- 1 | /* JsonToForm - Clean Modern Style (Minimal Borders) */ 2 | 3 | /* Reset unwanted original styles */ 4 | .j-container td { 5 | padding: 0 !important; 6 | } 7 | 8 | /* Hierarchical Container System - No Parent Borders */ 9 | .j-container { 10 | font-family: "Segoe UI", Tahoma, Arial, sans-serif !important; 11 | font-size: 12px !important; 12 | border: none !important; 13 | background: transparent !important; 14 | margin-bottom: 6px !important; 15 | border-radius: 0 !important; 16 | box-shadow: none !important; 17 | } 18 | 19 | /* Only leaf containers (no nested containers) get visual styling */ 20 | .j-container:not(:has(.j-container)) { 21 | border: 1px solid #e9ecef !important; 22 | background: white !important; 23 | border-radius: 4px !important; 24 | box-shadow: 0 1px 3px rgba(0,0,0,0.08) !important; 25 | margin-bottom: 8px !important; 26 | } 27 | 28 | /* Object/Array Headers - Subtle Background */ 29 | .j-oject-title-row td, 30 | .j-array-title-row td { 31 | background: #f8f9fa !important; 32 | border-bottom: 1px solid #e9ecef !important; 33 | padding: 8px 12px !important; 34 | font-weight: 600 !important; 35 | font-size: 13px !important; 36 | color: #495057 !important; 37 | } 38 | 39 | /* Form Row Layout - No Internal Borders */ 40 | .j-oject-value-row td { 41 | border: none !important; 42 | padding: 6px !important; 43 | } 44 | 45 | /* Remove row separators - use spacing instead */ 46 | .j-oject-value-row:nth-child(even) { 47 | background: rgba(248,249,250,0.3) !important; 48 | } 49 | 50 | .j-title-col { 51 | background: transparent !important; 52 | border: none !important; 53 | padding: 8px 12px 8px 8px !important; 54 | font-weight: 500 !important; 55 | color: #6c757d !important; 56 | vertical-align: middle !important; 57 | text-align: right !important; 58 | width: 140px !important; 59 | min-width: 140px !important; 60 | white-space: nowrap !important; 61 | } 62 | 63 | /* Remove separator column completely */ 64 | .j-sep-col { 65 | display: none !important; 66 | } 67 | 68 | .j-body-col { 69 | padding: 8px 12px !important; 70 | vertical-align: middle !important; 71 | } 72 | 73 | /* Input Controls - Clean & Modern */ 74 | input.j-input-text, 75 | input.j-input-number, 76 | input.j-input-email, 77 | input.j-input-tel, 78 | input.j-input-date, 79 | input.j-input-time, 80 | select.j-input-select, 81 | textarea.j-input-textarea { 82 | border: 1px solid #ced4da !important; 83 | border-radius: 4px !important; 84 | padding: 6px 8px !important; 85 | font-size: 13px !important; 86 | height: 32px !important; 87 | line-height: 1.4 !important; 88 | background: white !important; 89 | transition: all 0.15s ease !important; 90 | } 91 | 92 | textarea.j-input-textarea { 93 | height: auto !important; 94 | min-height: 60px !important; 95 | padding: 8px !important; 96 | line-height: 1.5 !important; 97 | } 98 | 99 | /* Focus Enhancement - Subtle */ 100 | input.j-input-text:focus, 101 | input.j-input-number:focus, 102 | input.j-input-email:focus, 103 | input.j-input-tel:focus, 104 | input.j-input-date:focus, 105 | input.j-input-time:focus, 106 | select.j-input-select:focus, 107 | textarea.j-input-textarea:focus { 108 | border-color: #80bdff !important; 109 | outline: none !important; 110 | box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25) !important; 111 | } 112 | 113 | /* Modern Icon Buttons - Consistent Design */ 114 | button.j-add-array-item, 115 | button.j-remove-array-item, 116 | .j-ec { 117 | display: inline-flex !important; 118 | align-items: center !important; 119 | justify-content: center !important; 120 | width: 24px !important; 121 | height: 24px !important; 122 | padding: 0 !important; 123 | margin: 0 3px !important; 124 | border: none !important; 125 | border-radius: 50% !important; 126 | cursor: pointer !important; 127 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; 128 | font-size: 12px !important; 129 | font-weight: 500 !important; 130 | line-height: 1 !important; 131 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; 132 | vertical-align: middle !important; 133 | box-sizing: border-box !important; 134 | position: relative !important; 135 | } 136 | 137 | /* Add Button - Success Green with Plus Icon */ 138 | button.j-add-array-item { 139 | background: #10b981 !important; 140 | color: white !important; 141 | } 142 | 143 | /* Hide original text content */ 144 | button.j-add-array-item { 145 | font-size: 0 !important; 146 | text-indent: -9999px !important; 147 | overflow: hidden !important; 148 | } 149 | 150 | button.j-add-array-item::before { 151 | content: "+" !important; 152 | font-size: 14px !important; 153 | font-weight: 600 !important; 154 | text-indent: 0 !important; 155 | position: absolute !important; 156 | left: 50% !important; 157 | top: 50% !important; 158 | transform: translate(-50%, -50%) !important; 159 | } 160 | 161 | button.j-add-array-item:hover { 162 | background: #059669 !important; 163 | transform: scale(1.1) !important; 164 | box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4) !important; 165 | } 166 | 167 | button.j-add-array-item:active { 168 | transform: scale(0.9) !important; 169 | } 170 | 171 | /* Remove Button - Danger Red with X Icon */ 172 | button.j-remove-array-item { 173 | background: #ef4444 !important; 174 | color: white !important; 175 | } 176 | 177 | /* Hide original text content */ 178 | button.j-remove-array-item { 179 | font-size: 0 !important; 180 | text-indent: -9999px !important; 181 | overflow: hidden !important; 182 | } 183 | 184 | button.j-remove-array-item::before { 185 | content: "×" !important; 186 | font-size: 16px !important; 187 | font-weight: 300 !important; 188 | line-height: 1 !important; 189 | text-indent: 0 !important; 190 | position: absolute !important; 191 | left: 50% !important; 192 | top: 50% !important; 193 | transform: translate(-50%, -50%) !important; 194 | } 195 | 196 | button.j-remove-array-item:hover { 197 | background: #dc2626 !important; 198 | transform: scale(1.1) !important; 199 | box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important; 200 | } 201 | 202 | button.j-remove-array-item:active { 203 | transform: scale(0.9) !important; 204 | } 205 | 206 | /* Expand/Collapse Button - Neutral Gray */ 207 | .j-ec { 208 | background: #6b7280 !important; 209 | color: white !important; 210 | font-family: ui-monospace, SFMono-Regular, monospace !important; 211 | font-weight: 700 !important; 212 | font-size: 10px !important; 213 | } 214 | 215 | .j-ec:hover { 216 | background: #4b5563 !important; 217 | transform: scale(1.1) !important; 218 | box-shadow: 0 3px 8px rgba(107, 114, 128, 0.3) !important; 219 | } 220 | 221 | .j-ec:active { 222 | transform: scale(0.9) !important; 223 | } 224 | 225 | /* Spacing Optimization - Clean Layout */ 226 | .j-container { 227 | margin-bottom: 8px !important; 228 | } 229 | 230 | .j-container:first-child { 231 | margin-top: 0 !important; 232 | } 233 | 234 | /* Remove all internal borders for cleaner look */ 235 | .j-container table { 236 | border-spacing: 0 !important; 237 | border-collapse: collapse !important; 238 | } 239 | 240 | .j-container tr { 241 | border: none !important; 242 | } 243 | 244 | .j-container td { 245 | border: none !important; 246 | } 247 | 248 | /* Use subtle background alternation instead of borders */ 249 | .j-oject-value-row:hover { 250 | background: rgba(0,123,255,0.05) !important; 251 | } 252 | 253 | /* Help Text Styling */ 254 | .j-inline-help { 255 | font-size: 11px !important; 256 | color: #6c757d !important; 257 | font-style: italic !important; 258 | margin-top: 4px !important; 259 | line-height: 1.3 !important; 260 | } 261 | 262 | .j-validation-help { 263 | font-size: 11px !important; 264 | color: #fd7e14 !important; 265 | font-weight: 500 !important; 266 | margin-top: 4px !important; 267 | } 268 | 269 | /* Required Field Indicator */ 270 | .j-required-star { 271 | color: #dc3545 !important; 272 | font-weight: bold !important; 273 | margin-right: 3px !important; 274 | } 275 | 276 | /* Array Container Styling */ 277 | .j-array-container { 278 | border: none !important; 279 | } 280 | 281 | /* Subtle Hierarchical Indentation System */ 282 | 283 | /* Level 1 nested containers */ 284 | .j-container .j-container { 285 | margin-right: 12px !important; 286 | border-right: 2px solid #f1f5f9 !important; 287 | padding-right: 8px !important; 288 | } 289 | 290 | /* Level 2 nested containers */ 291 | .j-container .j-container .j-container { 292 | margin-right: 24px !important; 293 | border-right: 2px solid #e2e8f0 !important; 294 | } 295 | 296 | /* Level 3+ nested containers */ 297 | .j-container .j-container .j-container .j-container { 298 | margin-right: 36px !important; 299 | border-right: 1px solid #cbd5e1 !important; 300 | } 301 | 302 | /* Nested headers - progressively smaller */ 303 | .j-container .j-container .j-oject-title-row td, 304 | .j-container .j-container .j-array-title-row td { 305 | font-size: 11px !important; 306 | font-weight: 500 !important; 307 | color: #5f6368 !important; 308 | padding: 4px 0 2px 0 !important; 309 | } 310 | 311 | .j-container .j-container .j-container .j-oject-title-row td, 312 | .j-container .j-container .j-container .j-array-title-row td { 313 | font-size: 10px !important; 314 | color: #80868b !important; 315 | } 316 | 317 | /* Array items - clean styling */ 318 | .j-array-item { 319 | background: rgba(248,249,250,0.5) !important; 320 | margin-bottom: 3px !important; 321 | padding: 6px !important; 322 | border-radius: 3px !important; 323 | border: 1px solid #f1f3f4 !important; 324 | } -------------------------------------------------------------------------------- /src/utils/JsonFormUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonFormUtils - Utility functions for JsonToForm 3 | * 4 | * @class JsonFormUtils 5 | */ 6 | class JsonFormUtils { 7 | 8 | constructor(jsonToForm) { 9 | this.jsonToForm = jsonToForm; 10 | } 11 | 12 | /** 13 | * Deep merge two objects 14 | * @param {Object} target - Target object 15 | * @param {Object} source - Source object 16 | * @returns {Object} Merged object 17 | */ 18 | deepMerge(target, source) { 19 | const result = { ...target }; 20 | 21 | for (const key in source) { 22 | if (source.hasOwnProperty(key)) { 23 | if (this.isObject(source[key]) && this.isObject(result[key])) { 24 | result[key] = this.deepMerge(result[key], source[key]); 25 | } else { 26 | result[key] = source[key]; 27 | } 28 | } 29 | } 30 | 31 | return result; 32 | } 33 | 34 | /** 35 | * Check if value is an object 36 | * @param {*} value - Value to check 37 | * @returns {boolean} True if object 38 | */ 39 | isObject(value) { 40 | return value !== null && typeof value === 'object' && !Array.isArray(value); 41 | } 42 | 43 | /** 44 | * Generate data path for an element 45 | * @param {jQuery} element - jQuery element 46 | * @returns {string} Generated path 47 | */ 48 | generatePath(element) { 49 | const pathParts = []; 50 | 51 | element.parents("[data-value-name]").each(function() { 52 | pathParts.push(`['${$(this).attr("data-value-name")}']`); 53 | }); 54 | 55 | const basePath = pathParts.reverse().join("."); 56 | const elementName = element.attr("data-value-name"); 57 | 58 | let result = basePath.replace(/\.\[/g, "[").replace(/\['']/g, ""); 59 | if (elementName) { 60 | result += `['${elementName}']`; 61 | } 62 | 63 | return result; 64 | } 65 | 66 | /** 67 | * Get ID-based data path for form elements 68 | * @param {string} dataPath - Data path 69 | * @param {string} containerId - Container ID 70 | * @returns {string} Element ID 71 | */ 72 | getIdBasedDataPath(dataPath, containerId) { 73 | let id = dataPath 74 | .replace(/\]\[/g, '_') 75 | .replace(/[\[\]"']/g, ''); 76 | 77 | return `${containerId}_${id}`; 78 | } 79 | 80 | /** 81 | * Get nested value from object using path 82 | * @param {Object} obj - Object to traverse 83 | * @param {string} path - Path string 84 | * @returns {*} Value at path 85 | */ 86 | getNestedValue(obj, path) { 87 | try { 88 | // Convert path format to property access 89 | const sanitizedPath = path.replace(/\['/g, '.').replace(/']/g, '').replace(/^\./, ''); 90 | const pathParts = sanitizedPath.split('.'); 91 | 92 | let current = obj; 93 | for (const part of pathParts) { 94 | if (current === null || current === undefined) { 95 | return null; 96 | } 97 | current = current[part]; 98 | } 99 | 100 | return current; 101 | } catch (e) { 102 | console.warn('Error getting nested value:', e); 103 | return null; 104 | } 105 | } 106 | 107 | /** 108 | * Set nested value in object using path 109 | * @param {Object} obj - Object to modify 110 | * @param {string} path - Path string 111 | * @param {*} value - Value to set 112 | */ 113 | setNestedValue(obj, path, value) { 114 | try { 115 | // Convert path format to property access 116 | const sanitizedPath = path.replace(/\['/g, '.').replace(/']/g, '').replace(/^\./, ''); 117 | const pathParts = sanitizedPath.split('.'); 118 | 119 | let current = obj; 120 | for (let i = 0; i < pathParts.length - 1; i++) { 121 | const part = pathParts[i]; 122 | if (!(part in current) || typeof current[part] !== 'object') { 123 | current[part] = {}; 124 | } 125 | current = current[part]; 126 | } 127 | 128 | const lastPart = pathParts[pathParts.length - 1]; 129 | current[lastPart] = value; 130 | } catch (e) { 131 | console.error('Error setting nested value:', e); 132 | } 133 | } 134 | 135 | /** 136 | * Ensure data path exists in object 137 | * @param {Object} obj - Object to modify 138 | * @param {string} dataPath - Data path to ensure 139 | */ 140 | ensureDataPath(obj, dataPath) { 141 | const pathParts = dataPath.replace(/\]\[/g, '].['). split('.'); 142 | let pathCursor = ""; 143 | 144 | pathParts.forEach(part => { 145 | pathCursor += part; 146 | if (this.getNestedValue(obj, pathCursor) === undefined) { 147 | this.setNestedValue(obj, pathCursor, {}); 148 | } 149 | }); 150 | } 151 | 152 | /** 153 | * Escape JSON string for safe usage 154 | * @param {string} str - String to escape 155 | * @returns {string} Escaped string 156 | */ 157 | jsonEscape(str) { 158 | if (!str) return ''; 159 | return str.toString() 160 | .replace(/\\/g, "\\\\") 161 | .replace(/\n/g, "\\n") 162 | .replace(/\r/g, "\\r") 163 | .replace(/\t/g, "\\t") 164 | .replace(/"/g, '\\"'); 165 | } 166 | 167 | /** 168 | * Replace all occurrences of a string 169 | * @param {string} source - Source string 170 | * @param {string} find - String to find 171 | * @param {string} replace - Replacement string 172 | * @returns {string} Modified string 173 | */ 174 | replaceAll(source, find, replace) { 175 | if (!source) return ''; 176 | return source.replace(new RegExp(this.escapeRegExp(find), 'g'), replace); 177 | } 178 | 179 | /** 180 | * Escape string for use in regular expression 181 | * @param {string} string - String to escape 182 | * @returns {string} Escaped string 183 | */ 184 | escapeRegExp(string) { 185 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 186 | } 187 | 188 | /** 189 | * Fix null/undefined values with default 190 | * @param {*} value - Value to check 191 | * @param {*} defaultValue - Default value if null/undefined 192 | * @returns {*} Fixed value 193 | */ 194 | fixNullUndefined(value, defaultValue) { 195 | return value !== null && value !== undefined ? value : defaultValue; 196 | } 197 | 198 | /** 199 | * Get UI setting from schema node 200 | * @param {Object} schemaNode - Schema node 201 | * @param {string} settingName - Setting name 202 | * @param {*} defaultValue - Default value 203 | * @returns {*} Setting value 204 | */ 205 | getUISetting(schemaNode, settingName, defaultValue) { 206 | if (schemaNode && schemaNode.ui && schemaNode.ui[settingName] !== undefined) { 207 | return schemaNode.ui[settingName]; 208 | } 209 | return defaultValue; 210 | } 211 | 212 | /** 213 | * Get array type from schema node 214 | * @param {Object} schemaNode - Schema node 215 | * @returns {string} Array type 216 | */ 217 | getArrayType(schemaNode) { 218 | if (schemaNode.items && schemaNode.items.type) { 219 | return schemaNode.items.type; 220 | } 221 | 222 | if (schemaNode.items && schemaNode.items.$ref) { 223 | return schemaNode.items.$ref; 224 | } 225 | 226 | return "string"; 227 | } 228 | 229 | /** 230 | * Generate spacer HTML for indentation 231 | * @param {number} level - Indentation level 232 | * @returns {string} Spacer HTML 233 | */ 234 | generateSpacer(level) { 235 | const adjustedLevel = Math.max(0, level - 1); 236 | const spaceCount = adjustedLevel * this.jsonToForm.config.indenting; 237 | const spaces = ' '.repeat(spaceCount); 238 | 239 | return `${spaces}`; 240 | } 241 | 242 | /** 243 | * Generate expand/collapse button 244 | * @param {string} type - Button type ('e' for expand, 'c' for collapse, '' for none) 245 | * @returns {string} Button HTML 246 | */ 247 | generateExpandCollapseButton(type) { 248 | if (!this.jsonToForm.config.treeExpandCollapseButton) { 249 | return ''; 250 | } 251 | 252 | switch (type) { 253 | case 'e': 254 | return '+  '; 255 | case 'c': 256 | return '-  '; 257 | default: 258 | return ''; 259 | } 260 | } 261 | 262 | /** 263 | * Generate title HTML 264 | * @param {Object} schemaNode - Schema node 265 | * @param {string} schemaName - Schema name 266 | * @returns {string} Title HTML 267 | */ 268 | generateTitle(schemaNode, schemaName) { 269 | const title = this.fixNullUndefined(schemaNode.title, schemaName); 270 | return ``; 271 | } 272 | 273 | /** 274 | * Escape HTML characters 275 | * @param {string} text - Text to escape 276 | * @returns {string} Escaped text 277 | */ 278 | escapeHtml(text) { 279 | if (!text) return ''; 280 | const div = document.createElement('div'); 281 | div.textContent = text; 282 | return div.innerHTML; 283 | } 284 | 285 | /** 286 | * Debounce function execution 287 | * @param {Function} func - Function to debounce 288 | * @param {number} wait - Wait time in milliseconds 289 | * @returns {Function} Debounced function 290 | */ 291 | debounce(func, wait) { 292 | let timeout; 293 | return function executedFunction(...args) { 294 | const later = () => { 295 | clearTimeout(timeout); 296 | func(...args); 297 | }; 298 | clearTimeout(timeout); 299 | timeout = setTimeout(later, wait); 300 | }; 301 | } 302 | 303 | /** 304 | * Check if value is empty (null, undefined, empty string, empty array) 305 | * @param {*} value - Value to check 306 | * @returns {boolean} True if empty 307 | */ 308 | isEmpty(value) { 309 | if (value === null || value === undefined) return true; 310 | if (typeof value === 'string' && value.trim() === '') return true; 311 | if (Array.isArray(value) && value.length === 0) return true; 312 | return false; 313 | } 314 | } 315 | 316 | // Export for module systems or global usage 317 | if (typeof module !== 'undefined' && module.exports) { 318 | module.exports = JsonFormUtils; 319 | } else if (typeof window !== 'undefined') { 320 | window.JsonFormUtils = JsonFormUtils; 321 | } -------------------------------------------------------------------------------- /v1/demo-farsi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JsonToForm v1 - دموی فارسی 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 37 | 38 | 39 |
40 |
41 |

JsonToForm فارسی

42 |

نمونه کاربرد پلاگین jQuery برای تبدیل JSON Schema به فرم HTML

43 |
44 | 45 |
46 |
47 |
48 |
49 |
در حال بارگذاری فرم...
50 |
51 | 52 |
53 | 54 |
55 |
56 | ✅ فرم معتبر است 57 |
58 | 59 |
60 |

📊 مقادیر JSON

61 |
{}
62 |
63 | 64 |
65 |

🔍 وضعیت اعتبارسنجی

66 |
بدون خطا
67 |
68 |
69 |
70 | 71 |
72 | 73 | 74 | 75 | 76 |
77 |
78 | 79 | 123 | 124 | -------------------------------------------------------------------------------- /src/validators/JsonFormValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonFormValidator - Validation engine for JsonToForm 3 | * 4 | * @class JsonFormValidator 5 | */ 6 | class JsonFormValidator { 7 | 8 | constructor(jsonToForm) { 9 | this.jsonToForm = jsonToForm; 10 | this.validationRules = this._initializeValidationRules(); 11 | } 12 | 13 | /** 14 | * Initialize built-in validation rules 15 | * @private 16 | */ 17 | _initializeValidationRules() { 18 | return { 19 | // Email validation 20 | email: { 21 | pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, 22 | message: 'Please enter a valid email address' 23 | }, 24 | 25 | // Phone number validation 26 | tel: { 27 | pattern: /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/, 28 | message: 'Please enter a valid phone number' 29 | }, 30 | 31 | // URL validation 32 | url: { 33 | pattern: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i, 34 | message: 'Please enter a valid URL' 35 | }, 36 | 37 | // Credit card validation (basic Luhn algorithm) 38 | creditCard: { 39 | validate: (value) => this._validateCreditCard(value), 40 | message: 'Please enter a valid credit card number' 41 | }, 42 | 43 | // Strong password validation 44 | strongPassword: { 45 | pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, 46 | message: 'Password must contain at least 8 characters including uppercase, lowercase, number and special character' 47 | } 48 | }; 49 | } 50 | 51 | /** 52 | * Validate all form inputs 53 | */ 54 | validateAll() { 55 | const inputs = this.jsonToForm.element.find(".j-input"); 56 | inputs.each((index, element) => { 57 | this.validateInput($(element)); 58 | }); 59 | } 60 | 61 | /** 62 | * Validate a single input element 63 | * @param {jQuery} $element - Input element to validate 64 | * @returns {Object} Validation result 65 | */ 66 | validateInput($element) { 67 | const result = { 68 | isValid: true, 69 | errors: [], 70 | element: $element 71 | }; 72 | 73 | // Execute before validation callback 74 | if (this.jsonToForm.config.callbacks.beforeValidation) { 75 | this.jsonToForm.config.callbacks.beforeValidation($element, result); 76 | } 77 | 78 | // Get validation parameters 79 | const isRequired = $element.attr("data-required") === "true"; 80 | const minValue = this._parseNumeric($element.attr("data-min")); 81 | const maxValue = this._parseNumeric($element.attr("data-max")); 82 | const pattern = $element.attr("data-pattern"); 83 | const customRule = $element.attr("data-validation-rule"); 84 | 85 | // Get current value 86 | const value = this._getElementValue($element); 87 | 88 | // Required validation 89 | if (isRequired && this.jsonToForm.utils.isEmpty(value)) { 90 | result.isValid = false; 91 | result.errors.push('This field is required'); 92 | } else if (!this.jsonToForm.utils.isEmpty(value)) { 93 | // Only validate other rules if field has value 94 | 95 | // Length/value range validation 96 | this._validateRange($element, value, minValue, maxValue, result); 97 | 98 | // Pattern validation 99 | this._validatePattern($element, value, pattern, result); 100 | 101 | // Input type specific validation 102 | this._validateInputType($element, value, result); 103 | 104 | // Custom rule validation 105 | this._validateCustomRule($element, value, customRule, result); 106 | } 107 | 108 | // Update element validation state 109 | this._updateElementValidationState($element, result); 110 | 111 | // Execute after validation callback 112 | if (this.jsonToForm.config.callbacks.afterValidation) { 113 | this.jsonToForm.config.callbacks.afterValidation($element, result); 114 | } 115 | 116 | return result; 117 | } 118 | 119 | /** 120 | * Get element value based on type 121 | * @private 122 | */ 123 | _getElementValue($element) { 124 | const tagName = $element.prop("tagName").toLowerCase(); 125 | const inputType = $element.prop("type") ? $element.prop("type").toLowerCase() : ""; 126 | 127 | if (tagName === "input" && inputType === "checkbox") { 128 | return $element.prop("checked"); 129 | } else if (tagName === "input" && inputType === "radio") { 130 | const name = $element.attr("name"); 131 | const checkedRadio = this.jsonToForm.element.find(`input[name="${name}"]:checked`); 132 | return checkedRadio.length > 0 ? checkedRadio.val() : null; 133 | } else if ($element.hasClass("j-input-html")) { 134 | return $element.siblings(".j-input-html-div").text(); 135 | } else { 136 | return $element.val(); 137 | } 138 | } 139 | 140 | /** 141 | * Validate range (length for strings, value for numbers) 142 | * @private 143 | */ 144 | _validateRange($element, value, minValue, maxValue, result) { 145 | if ($element.hasClass("j-input-text") || $element.hasClass("j-input-textarea")) { 146 | // String length validation 147 | const length = value ? value.toString().length : 0; 148 | 149 | if (minValue !== null && length < minValue) { 150 | result.isValid = false; 151 | result.errors.push(`Minimum length is ${minValue} characters`); 152 | } 153 | 154 | if (maxValue !== null && length > maxValue) { 155 | result.isValid = false; 156 | result.errors.push(`Maximum length is ${maxValue} characters`); 157 | } 158 | } else if ($element.hasClass("j-input-number")) { 159 | // Numeric value validation 160 | const numValue = parseFloat(value); 161 | 162 | if (!isNaN(numValue)) { 163 | if (minValue !== null && numValue < minValue) { 164 | result.isValid = false; 165 | result.errors.push(`Minimum value is ${minValue}`); 166 | } 167 | 168 | if (maxValue !== null && numValue > maxValue) { 169 | result.isValid = false; 170 | result.errors.push(`Maximum value is ${maxValue}`); 171 | } 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * Validate pattern 178 | * @private 179 | */ 180 | _validatePattern($element, value, pattern, result) { 181 | if (pattern && value) { 182 | try { 183 | const regex = new RegExp(pattern); 184 | if (!regex.test(value)) { 185 | result.isValid = false; 186 | result.errors.push('Value does not match required pattern'); 187 | } 188 | } catch (e) { 189 | console.warn('Invalid regex pattern:', pattern); 190 | } 191 | } 192 | } 193 | 194 | /** 195 | * Validate based on input type 196 | * @private 197 | */ 198 | _validateInputType($element, value, result) { 199 | if ($element.hasClass("j-input-email")) { 200 | this._applyValidationRule(value, 'email', result); 201 | } else if ($element.hasClass("j-input-tel")) { 202 | this._applyValidationRule(value, 'tel', result); 203 | } else if ($element.hasClass("j-input-url")) { 204 | this._applyValidationRule(value, 'url', result); 205 | } else if ($element.hasClass("j-input-number")) { 206 | if (value && isNaN(parseFloat(value))) { 207 | result.isValid = false; 208 | result.errors.push('Please enter a valid number'); 209 | } 210 | } else if ($element.hasClass("j-input-radio")) { 211 | // Radio button group validation 212 | const name = $element.attr("name"); 213 | const isChecked = this.jsonToForm.element.find(`input[name="${name}"]:checked`).length > 0; 214 | 215 | if ($element.attr("data-required") === "true" && !isChecked) { 216 | result.isValid = false; 217 | result.errors.push('Please select an option'); 218 | 219 | // Set validation state on the container 220 | $element.closest('div[data-required]').attr("data-is-valid", "false"); 221 | } else { 222 | $element.closest('div[data-required]').attr("data-is-valid", "true"); 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * Validate custom rule 229 | * @private 230 | */ 231 | _validateCustomRule($element, value, customRule, result) { 232 | if (customRule) { 233 | // Check built-in rules 234 | if (this.validationRules[customRule]) { 235 | this._applyValidationRule(value, customRule, result); 236 | } 237 | 238 | // Check user-defined custom rules 239 | const userRules = this.jsonToForm.config.validation.customRules; 240 | if (userRules[customRule]) { 241 | const rule = userRules[customRule]; 242 | let isValid = true; 243 | 244 | if (typeof rule.validate === 'function') { 245 | isValid = rule.validate(value, $element); 246 | } else if (rule.pattern) { 247 | isValid = rule.pattern.test(value); 248 | } 249 | 250 | if (!isValid) { 251 | result.isValid = false; 252 | result.errors.push(rule.message || 'Custom validation failed'); 253 | } 254 | } 255 | } 256 | } 257 | 258 | /** 259 | * Apply a validation rule 260 | * @private 261 | */ 262 | _applyValidationRule(value, ruleName, result) { 263 | const rule = this.validationRules[ruleName]; 264 | if (!rule) return; 265 | 266 | let isValid = true; 267 | 268 | if (typeof rule.validate === 'function') { 269 | isValid = rule.validate(value); 270 | } else if (rule.pattern) { 271 | isValid = rule.pattern.test(value); 272 | } 273 | 274 | if (!isValid) { 275 | result.isValid = false; 276 | result.errors.push(rule.message); 277 | } 278 | } 279 | 280 | /** 281 | * Update element validation state 282 | * @private 283 | */ 284 | _updateElementValidationState($element, result) { 285 | $element.attr("data-is-valid", result.isValid ? "true" : "false"); 286 | 287 | // Remove existing validation messages 288 | $element.siblings('.j-validation-message').remove(); 289 | 290 | // Add validation messages if configured 291 | if (!result.isValid && this.jsonToForm.config.validation.showHints && result.errors.length > 0) { 292 | const errorMessage = result.errors[0]; // Show first error 293 | const messageHtml = `
${this.jsonToForm.utils.escapeHtml(errorMessage)}
`; 294 | $element.after(messageHtml); 295 | } 296 | } 297 | 298 | /** 299 | * Check if entire form is valid 300 | * @returns {boolean} True if form is valid 301 | */ 302 | isFormValid() { 303 | return this.jsonToForm.element.find('[data-is-valid="false"]').length === 0; 304 | } 305 | 306 | /** 307 | * Get all validation errors 308 | * @returns {Array} Array of validation errors 309 | */ 310 | getAllErrors() { 311 | const errors = []; 312 | 313 | this.jsonToForm.element.find('[data-is-valid="false"]').each((index, element) => { 314 | const $element = $(element); 315 | const fieldName = $element.attr('data-value-name') || 'Unknown field'; 316 | const message = $element.siblings('.j-validation-message').text() || 'Validation error'; 317 | 318 | errors.push({ 319 | field: fieldName, 320 | message: message, 321 | element: $element 322 | }); 323 | }); 324 | 325 | return errors; 326 | } 327 | 328 | /** 329 | * Clear all validation messages 330 | */ 331 | clearValidationMessages() { 332 | this.jsonToForm.element.find('.j-validation-message').remove(); 333 | this.jsonToForm.element.find('[data-is-valid]').attr('data-is-valid', 'true'); 334 | } 335 | 336 | /** 337 | * Add custom validation rule 338 | * @param {string} name - Rule name 339 | * @param {Object} rule - Rule definition 340 | */ 341 | addCustomRule(name, rule) { 342 | this.validationRules[name] = rule; 343 | } 344 | 345 | /** 346 | * Helper method to parse numeric values 347 | * @private 348 | */ 349 | _parseNumeric(value) { 350 | return value ? parseFloat(value) : null; 351 | } 352 | 353 | /** 354 | * Validate credit card number using Luhn algorithm 355 | * @private 356 | */ 357 | _validateCreditCard(cardNumber) { 358 | if (!cardNumber) return false; 359 | 360 | const number = cardNumber.replace(/\D/g, ''); 361 | if (number.length < 13 || number.length > 19) return false; 362 | 363 | let sum = 0; 364 | let shouldDouble = false; 365 | 366 | for (let i = number.length - 1; i >= 0; i--) { 367 | let digit = parseInt(number.charAt(i)); 368 | 369 | if (shouldDouble) { 370 | digit *= 2; 371 | if (digit > 9) { 372 | digit -= 9; 373 | } 374 | } 375 | 376 | sum += digit; 377 | shouldDouble = !shouldDouble; 378 | } 379 | 380 | return sum % 10 === 0; 381 | } 382 | } 383 | 384 | // Export for module systems or global usage 385 | if (typeof module !== 'undefined' && module.exports) { 386 | module.exports = JsonFormValidator; 387 | } else if (typeof window !== 'undefined') { 388 | window.JsonFormValidator = JsonFormValidator; 389 | } -------------------------------------------------------------------------------- /src/core/JsonFormEventHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonFormEventHandler - Event management for JsonToForm 3 | * 4 | * @class JsonFormEventHandler 5 | */ 6 | class JsonFormEventHandler { 7 | 8 | constructor(jsonToForm) { 9 | this.jsonToForm = jsonToForm; 10 | this.eventNamespace = '.jsonToForm'; 11 | this.debouncedValidation = this.jsonToForm.utils.debounce( 12 | (element) => this.jsonToForm.validator.validateInput(element), 13 | 300 14 | ); 15 | } 16 | 17 | /** 18 | * Initialize all event handlers 19 | */ 20 | initialize() { 21 | this._bindExpandCollapseEvents(); 22 | this._bindArrayManipulationEvents(); 23 | this._bindInputChangeEvents(); 24 | this._bindFormValidationEvents(); 25 | this._bindKeyboardEvents(); 26 | this._bindFocusEvents(); 27 | } 28 | 29 | /** 30 | * Destroy all event handlers 31 | */ 32 | destroy() { 33 | this.jsonToForm.element.off(this.eventNamespace); 34 | } 35 | 36 | /** 37 | * Bind expand/collapse events for tree nodes 38 | * @private 39 | */ 40 | _bindExpandCollapseEvents() { 41 | this.jsonToForm.element 42 | .off(`click${this.eventNamespace}`, '.j-ec') 43 | .on(`click${this.eventNamespace}`, '.j-ec', (event) => { 44 | event.preventDefault(); 45 | event.stopPropagation(); 46 | this._handleToggleSubTree(event.target); 47 | }); 48 | } 49 | 50 | /** 51 | * Bind array item add/remove events 52 | * @private 53 | */ 54 | _bindArrayManipulationEvents() { 55 | // Add array item 56 | this.jsonToForm.element 57 | .off(`click${this.eventNamespace}`, '.j-add-array-item') 58 | .on(`click${this.eventNamespace}`, '.j-add-array-item', (event) => { 59 | event.preventDefault(); 60 | this._handleAddArrayItem($(event.target)); 61 | }); 62 | 63 | // Remove array item 64 | this.jsonToForm.element 65 | .off(`click${this.eventNamespace}`, '.j-remove-array-item') 66 | .on(`click${this.eventNamespace}`, '.j-remove-array-item', (event) => { 67 | event.preventDefault(); 68 | this._handleRemoveArrayItem($(event.target)); 69 | }); 70 | } 71 | 72 | /** 73 | * Bind input change events 74 | * @private 75 | */ 76 | _bindInputChangeEvents() { 77 | // Text inputs (keyup for real-time) 78 | this.jsonToForm.element 79 | .off(`keyup${this.eventNamespace}`, '.j-input-text, .j-input-textarea, .j-input-date, .j-input-number, .j-input-email, .j-input-tel, .j-input-url') 80 | .on(`keyup${this.eventNamespace}`, '.j-input-text, .j-input-textarea, .j-input-date, .j-input-number, .j-input-email, .j-input-tel, .j-input-url', (event) => { 81 | this._handleInputChange($(event.target)); 82 | }); 83 | 84 | // Change events for select, checkbox, radio, etc. 85 | this.jsonToForm.element 86 | .off(`change${this.eventNamespace}`, '.j-input-checkbox, .j-input-radio, .j-input-select, .j-input-color, .j-input-date, .j-input-number, .j-input-html') 87 | .on(`change${this.eventNamespace}`, '.j-input-checkbox, .j-input-radio, .j-input-select, .j-input-color, .j-input-date, .j-input-number, .j-input-html', (event) => { 88 | this._handleInputChange($(event.target)); 89 | }); 90 | 91 | // HTML editor events 92 | this.jsonToForm.element 93 | .off(`keyup${this.eventNamespace}`, '.j-input-html-div') 94 | .on(`keyup${this.eventNamespace}`, '.j-input-html-div', (event) => { 95 | this._handleHtmlEditorChange($(event.target)); 96 | }) 97 | .off(`paste${this.eventNamespace}`, '.j-input-html-div') 98 | .on(`paste${this.eventNamespace}`, '.j-input-html-div', (event) => { 99 | // Delay to allow paste content to be processed 100 | setTimeout(() => this._handleHtmlEditorChange($(event.target)), 10); 101 | }); 102 | } 103 | 104 | /** 105 | * Bind form validation events 106 | * @private 107 | */ 108 | _bindFormValidationEvents() { 109 | // Real-time validation on blur 110 | this.jsonToForm.element 111 | .off(`blur${this.eventNamespace}`, '.j-input') 112 | .on(`blur${this.eventNamespace}`, '.j-input', (event) => { 113 | if (this.jsonToForm.config.validation.realTime) { 114 | this.jsonToForm.validator.validateInput($(event.target)); 115 | } 116 | }); 117 | } 118 | 119 | /** 120 | * Bind keyboard events for better UX 121 | * @private 122 | */ 123 | _bindKeyboardEvents() { 124 | // Enter key handling 125 | this.jsonToForm.element 126 | .off(`keypress${this.eventNamespace}`, '.j-input') 127 | .on(`keypress${this.eventNamespace}`, '.j-input', (event) => { 128 | if (event.which === 13) { // Enter key 129 | this._handleEnterKey($(event.target), event); 130 | } 131 | }); 132 | 133 | // Tab navigation enhancement 134 | this.jsonToForm.element 135 | .off(`keydown${this.eventNamespace}`, '.j-input') 136 | .on(`keydown${this.eventNamespace}`, '.j-input', (event) => { 137 | if (event.which === 9) { // Tab key 138 | this._handleTabKey($(event.target), event); 139 | } 140 | }); 141 | } 142 | 143 | /** 144 | * Bind focus events 145 | * @private 146 | */ 147 | _bindFocusEvents() { 148 | // HTML editor focus redirect 149 | this.jsonToForm.element 150 | .off(`focus${this.eventNamespace}`, '.j-input-html') 151 | .on(`focus${this.eventNamespace}`, '.j-input-html', (event) => { 152 | $(event.target).closest('td').find('.j-input-html-div').focus(); 153 | }); 154 | 155 | // Focus highlighting 156 | this.jsonToForm.element 157 | .off(`focus${this.eventNamespace}`, '.j-input') 158 | .on(`focus${this.eventNamespace}`, '.j-input', (event) => { 159 | $(event.target).addClass('j-input-focused'); 160 | }) 161 | .off(`blur${this.eventNamespace}`, '.j-input') 162 | .on(`blur${this.eventNamespace}`, '.j-input', (event) => { 163 | $(event.target).removeClass('j-input-focused'); 164 | }); 165 | } 166 | 167 | /** 168 | * Handle expand/collapse tree nodes 169 | * @private 170 | */ 171 | _handleToggleSubTree(button) { 172 | const $button = $(button); 173 | const isExpanded = $button.text().trim() === '-'; 174 | 175 | if (isExpanded) { 176 | this._collapseNode($button); 177 | } else { 178 | this._expandNode($button); 179 | } 180 | } 181 | 182 | /** 183 | * Expand tree node 184 | * @private 185 | */ 186 | _expandNode($button) { 187 | $button.text('-').removeClass('j-expand-btn').addClass('j-collapse-btn'); 188 | $button.closest('tr').next('tr').removeClass('j-collapsed'); 189 | 190 | // Trigger custom event 191 | this.jsonToForm.element.trigger('nodeExpanded', [$button]); 192 | } 193 | 194 | /** 195 | * Collapse tree node 196 | * @private 197 | */ 198 | _collapseNode($button) { 199 | $button.text('+').removeClass('j-collapse-btn').addClass('j-expand-btn'); 200 | $button.closest('tr').next('tr').addClass('j-collapsed'); 201 | 202 | // Trigger custom event 203 | this.jsonToForm.element.trigger('nodeCollapsed', [$button]); 204 | } 205 | 206 | /** 207 | * Handle adding array item 208 | * @private 209 | */ 210 | _handleAddArrayItem($button) { 211 | this.jsonToForm.renderer.addArrayItem($button, true, null); 212 | 213 | // Focus on the newly added item 214 | setTimeout(() => { 215 | const $newItem = $button.closest('tr').next('tr').find('.j-input').last(); 216 | if ($newItem.length) { 217 | $newItem.focus(); 218 | } 219 | }, 100); 220 | 221 | // Trigger custom event 222 | this.jsonToForm.element.trigger('arrayItemAdded', [$button]); 223 | } 224 | 225 | /** 226 | * Handle removing array item 227 | * @private 228 | */ 229 | _handleRemoveArrayItem($button) { 230 | const itemIndex = $button.attr('data-index'); 231 | const $nodeToRemove = $button.closest('table'); 232 | const dataPath = $nodeToRemove.closest('td').attr('data-path'); 233 | 234 | if (confirm('Are you sure you want to remove this item?')) { 235 | // Remove from data 236 | const pathParts = dataPath.replace(/\['/g, '.').replace(/']/g, '').replace(/^\./, '').split('.'); 237 | let current = this.jsonToForm.config.value; 238 | 239 | for (let i = 0; i < pathParts.length; i++) { 240 | if (i === pathParts.length - 1) { 241 | if (Array.isArray(current) && current[itemIndex] !== undefined) { 242 | current.splice(itemIndex, 1); 243 | } 244 | } else { 245 | current = current[pathParts[i]]; 246 | if (!current) break; 247 | } 248 | } 249 | 250 | // Remove from DOM 251 | $nodeToRemove.remove(); 252 | 253 | // Re-render to fix indexes 254 | this.jsonToForm.setValue(this.jsonToForm.config.value); 255 | 256 | // Trigger callback and custom event 257 | if (this.jsonToForm.config.callbacks.afterValueChanged) { 258 | this.jsonToForm.config.callbacks.afterValueChanged( 259 | this.jsonToForm.config.value, 260 | this.jsonToForm.config.schema 261 | ); 262 | } 263 | 264 | this.jsonToForm.element.trigger('arrayItemRemoved', [$button, itemIndex]); 265 | } 266 | } 267 | 268 | /** 269 | * Handle input value changes 270 | * @private 271 | */ 272 | _handleInputChange($element) { 273 | this._updateValueFromInput($element); 274 | 275 | // Real-time validation if enabled 276 | if (this.jsonToForm.config.validation.realTime) { 277 | this.debouncedValidation($element); 278 | } 279 | 280 | // Trigger callback 281 | if (this.jsonToForm.config.callbacks.afterValueChanged) { 282 | this.jsonToForm.config.callbacks.afterValueChanged( 283 | this.jsonToForm.config.value, 284 | this.jsonToForm.config.schema 285 | ); 286 | } 287 | 288 | // Trigger custom event 289 | this.jsonToForm.element.trigger('valueChanged', [$element, this.jsonToForm.config.value]); 290 | } 291 | 292 | /** 293 | * Handle HTML editor changes 294 | * @private 295 | */ 296 | _handleHtmlEditorChange($htmlDiv) { 297 | const $input = $htmlDiv.closest('td').find('.j-input-html'); 298 | $input.val($htmlDiv.html()); 299 | this._handleInputChange($input); 300 | } 301 | 302 | /** 303 | * Update internal value from input element 304 | * @private 305 | */ 306 | _updateValueFromInput($element) { 307 | const dataPath = $element.attr('data-path'); 308 | if (!dataPath) return; 309 | 310 | this.jsonToForm.utils.ensureDataPath(this.jsonToForm.config.value, dataPath); 311 | 312 | const tagName = $element.prop('tagName').toLowerCase(); 313 | const inputType = $element.prop('type') ? $element.prop('type').toLowerCase() : ''; 314 | 315 | let value; 316 | 317 | if (tagName === 'input' && inputType === 'checkbox') { 318 | value = $element.prop('checked'); 319 | } else if (tagName === 'input' && inputType === 'radio') { 320 | // Handle radio button groups 321 | const name = $element.attr('name'); 322 | const $checkedRadio = this.jsonToForm.element.find(`input[name="${name}"]:checked`); 323 | value = $checkedRadio.length ? $checkedRadio.val() : null; 324 | 325 | // Update validation state for radio group 326 | const $radioContainer = $element.closest('div[data-required]'); 327 | if ($radioContainer.length) { 328 | $radioContainer.attr('data-is-valid', value !== null ? 'true' : 'false'); 329 | } 330 | } else { 331 | value = $element.val(); 332 | 333 | // Auto-trim if enabled 334 | if (this.jsonToForm.config.autoTrimValues && typeof value === 'string') { 335 | value = value.trim(); 336 | $element.val(value); // Update the input with trimmed value 337 | } 338 | } 339 | 340 | // Set the value in the data object 341 | this.jsonToForm.utils.setNestedValue(this.jsonToForm.config.value, dataPath, value); 342 | } 343 | 344 | /** 345 | * Handle Enter key press 346 | * @private 347 | */ 348 | _handleEnterKey($element, event) { 349 | const tagName = $element.prop('tagName').toLowerCase(); 350 | 351 | if (tagName === 'input') { 352 | // Move to next input field 353 | const $inputs = this.jsonToForm.element.find('.j-input:visible'); 354 | const currentIndex = $inputs.index($element); 355 | const $nextInput = $inputs.eq(currentIndex + 1); 356 | 357 | if ($nextInput.length) { 358 | $nextInput.focus(); 359 | } 360 | } else if (tagName === 'textarea') { 361 | // Allow normal Enter behavior in textarea 362 | return true; 363 | } 364 | 365 | event.preventDefault(); 366 | return false; 367 | } 368 | 369 | /** 370 | * Handle Tab key navigation 371 | * @private 372 | */ 373 | _handleTabKey($element, event) { 374 | // Enhanced tab navigation could be implemented here 375 | // For now, we let the browser handle default tab behavior 376 | return true; 377 | } 378 | 379 | /** 380 | * Programmatically trigger input change 381 | * @param {jQuery} $element - Element to trigger change on 382 | */ 383 | triggerChange($element) { 384 | this._handleInputChange($element); 385 | } 386 | 387 | /** 388 | * Add custom event listener 389 | * @param {string} eventName - Event name 390 | * @param {Function} handler - Event handler 391 | */ 392 | on(eventName, handler) { 393 | this.jsonToForm.element.on(eventName, handler); 394 | } 395 | 396 | /** 397 | * Remove custom event listener 398 | * @param {string} eventName - Event name 399 | * @param {Function} handler - Event handler (optional) 400 | */ 401 | off(eventName, handler) { 402 | this.jsonToForm.element.off(eventName, handler); 403 | } 404 | 405 | /** 406 | * Trigger custom event 407 | * @param {string} eventName - Event name 408 | * @param {Array} data - Event data 409 | */ 410 | trigger(eventName, data) { 411 | this.jsonToForm.element.trigger(eventName, data); 412 | } 413 | } 414 | 415 | // Export for module systems or global usage 416 | if (typeof module !== 'undefined' && module.exports) { 417 | module.exports = JsonFormEventHandler; 418 | } else if (typeof window !== 'undefined') { 419 | window.JsonFormEventHandler = JsonFormEventHandler; 420 | } -------------------------------------------------------------------------------- /jsonToForm/jsonToForm.css: -------------------------------------------------------------------------------- 1 | /* ===== JsonToForm Modern CSS با Flexbox Layout ===== */ 2 | 3 | /* کنترل کلی overflow */ 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | overflow-x: hidden; 10 | } 11 | 12 | #jsonEditor { 13 | max-width: 100%; 14 | overflow-x: hidden; 15 | } 16 | 17 | .form-panel { 18 | overflow-x: hidden; 19 | } 20 | 21 | /* ===== Container اصلی - حالا div بجای table ===== */ 22 | .j-container { 23 | width: 100%; 24 | max-width: 100%; 25 | margin-bottom: 8px; 26 | border-radius: 5px; 27 | overflow: hidden; 28 | } 29 | 30 | /* ===== Row Layout با Flexbox ===== */ 31 | .j-field-row { 32 | display: flex; 33 | align-items: flex-start; 34 | min-height: 32px; 35 | padding: 4px 8px; 36 | gap: 8px; 37 | } 38 | 39 | .j-field-row:last-child { 40 | border-bottom: none; 41 | } 42 | 43 | /* ===== Header Rows ===== */ 44 | .j-header-row { 45 | background-color: gainsboro; 46 | font-weight: bold; 47 | font-size: 14px; 48 | padding: 8px; 49 | } 50 | 51 | /* ===== Columns با Flexbox ===== */ 52 | .j-label-col { 53 | flex: 0 0 180px; 54 | min-width: 120px; 55 | max-width: 200px; 56 | padding: 6px 8px; 57 | font-size: 12px; 58 | font-weight: 500; 59 | color: #333; 60 | text-align: right; 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | align-self: flex-start; 65 | } 66 | 67 | .j-input-col { 68 | flex: 1; 69 | min-width: 0; /* مهم برای flexbox */ 70 | padding: 4px; 71 | } 72 | 73 | .j-action-col { 74 | flex: 0 0 24px; 75 | min-width: 24px; 76 | display: flex; 77 | align-items: center; 78 | justify-content: center; 79 | padding: 0; 80 | } 81 | 82 | /* ===== Input Styles ===== */ 83 | .j-input-text, 84 | .j-input-select, 85 | .j-input-textarea, 86 | .j-input-date, 87 | .j-input-number, 88 | .j-input-html, 89 | .j-input-email, 90 | .j-input-tel { 91 | width: 100%; 92 | border: 1px solid #e0e0e0; 93 | padding: 5px; 94 | border-radius: 5px; 95 | background-color: #fefefe; 96 | font-size: 12px; 97 | box-sizing: border-box; 98 | } 99 | 100 | .j-input-text:focus, 101 | .j-input-select:focus, 102 | .j-input-textarea:focus, 103 | .j-input-date:focus, 104 | .j-input-number:focus, 105 | .j-input-html-div:focus { 106 | outline: 1px solid #007bff; 107 | border-color: #007bff; 108 | } 109 | 110 | .j-input-text:disabled { 111 | border-color: #f0f5ff; 112 | background-color: #f5f8ff; 113 | } 114 | 115 | .j-input-text::placeholder { 116 | color: silver; 117 | } 118 | 119 | .j-input-textarea { 120 | margin: 2px 0; 121 | resize: vertical; 122 | min-height: 60px; 123 | } 124 | 125 | /* ===== Radio & Checkbox ===== */ 126 | .j-input-radio, 127 | .j-input-checkbox { 128 | width: auto; 129 | height: auto; 130 | margin-left: 6px; 131 | vertical-align: middle; 132 | } 133 | 134 | .j-input-radio-label, 135 | .j-input-radio { 136 | vertical-align: middle; 137 | } 138 | 139 | .j-input-radio-label { 140 | margin-left: 5px; 141 | } 142 | 143 | /* ===== HTML Editor ===== */ 144 | .j-input-html-div { 145 | padding: 8px; 146 | margin: 3px 2px; 147 | border: 1px solid #e0e0e0; 148 | border-radius: 5px; 149 | min-height: 60px; 150 | } 151 | 152 | .j-input-html { 153 | width: 0; 154 | height: 0; 155 | padding: 0; 156 | border-width: 0; 157 | } 158 | 159 | /* ===== Buttons - مدرن و زیبا ===== */ 160 | .j-add-array-item, 161 | .j-remove-array-item { 162 | display: inline-flex; 163 | align-items: center; 164 | justify-content: center; 165 | width: 20px; 166 | height: 20px; 167 | border-radius: 50%; 168 | cursor: pointer; 169 | font-weight: bold; 170 | font-size: 12px; 171 | line-height: 1; 172 | transition: all 0.2s ease; 173 | margin: 0 1px; 174 | border: 1px solid; 175 | background: white; 176 | flex-shrink: 0; 177 | } 178 | 179 | .j-add-array-item { 180 | color: #28a745; 181 | border-color: #28a745; 182 | } 183 | 184 | .j-add-array-item:hover { 185 | background: #28a745; 186 | color: white; 187 | transform: scale(1.1); 188 | } 189 | 190 | .j-remove-array-item { 191 | color: #dc3545; 192 | border-color: #dc3545; 193 | } 194 | 195 | .j-remove-array-item:hover { 196 | background: #dc3545; 197 | color: white; 198 | transform: scale(1.1); 199 | } 200 | 201 | .j-remove-array-item:before { 202 | content: "×"; 203 | } 204 | 205 | .j-add-array-item:before { 206 | content: "+"; 207 | } 208 | 209 | /* ===== Expand/Collapse Button ===== */ 210 | .j-ec { 211 | display: inline-flex; 212 | align-items: center; 213 | justify-content: center; 214 | width: 16px; 215 | height: 16px; 216 | background: #6c757d; 217 | color: white; 218 | border-radius: 3px; 219 | cursor: pointer; 220 | font-size: 10px; 221 | font-weight: bold; 222 | margin: 0 3px; 223 | transition: all 0.2s ease; 224 | } 225 | 226 | .j-ec:hover { 227 | background: #495057; 228 | transform: scale(1.05); 229 | } 230 | 231 | /* ===== Collapsed State ===== */ 232 | .j-collapsed { 233 | display: none !important; 234 | } 235 | 236 | /* ===== Nested Containers - Indentation ===== */ 237 | .j-nested-1 .j-field-row { 238 | padding-right: 20px; 239 | } 240 | 241 | .j-nested-2 .j-field-row { 242 | padding-right: 40px; 243 | } 244 | 245 | .j-nested-3 .j-field-row { 246 | padding-right: 60px; 247 | } 248 | 249 | /* ===== Array Items ===== */ 250 | .j-array-container { 251 | border-radius: 5px; 252 | margin-bottom: 8px; 253 | background: white; 254 | } 255 | 256 | /* ===== Mode Selector ===== */ 257 | .j-mode-selector { 258 | background: #f8f9fa; 259 | padding: 10px 15px; 260 | border-radius: 5px; 261 | margin-bottom: 15px; 262 | border: 1px solid #dee2e6; 263 | display: flex; 264 | align-items: center; 265 | gap: 10px; 266 | } 267 | 268 | .j-mode-selector label { 269 | font-weight: bold; 270 | font-size: 14px; 271 | color: #495057; 272 | margin: 0; 273 | } 274 | 275 | .j-mode-select { 276 | padding: 5px 10px; 277 | border: 1px solid #ced4da; 278 | border-radius: 4px; 279 | background: white; 280 | font-size: 13px; 281 | min-width: 150px; 282 | } 283 | 284 | /* ===== Property Grid Mode ===== */ 285 | .j-property-grid-container { 286 | margin-bottom: 20px; 287 | border-radius: 5px; 288 | overflow: hidden; 289 | } 290 | 291 | .j-property-grid-header { 292 | background: #f8f9fa; 293 | padding: 10px 15px; 294 | font-weight: bold; 295 | font-size: 14px; 296 | } 297 | 298 | .j-property-grid-table { 299 | width: 100%; 300 | border-collapse: collapse; 301 | background: white; 302 | } 303 | 304 | .j-property-grid-table thead { 305 | background: #e9ecef; 306 | } 307 | 308 | .j-property-grid-table th, 309 | .j-property-grid-table td { 310 | padding: 8px 12px; 311 | text-align: right; 312 | vertical-align: top; 313 | } 314 | 315 | .j-property-grid-table th { 316 | font-weight: bold; 317 | font-size: 13px; 318 | color: #495057; 319 | } 320 | 321 | .j-property-name { 322 | width: 30%; 323 | font-weight: 500; 324 | color: #333; 325 | } 326 | 327 | .j-property-value { 328 | width: 60%; 329 | } 330 | 331 | .j-property-actions { 332 | width: 10%; 333 | text-align: center; 334 | } 335 | 336 | .j-property-grid-row:hover { 337 | background: #f8f9fa; 338 | } 339 | 340 | .j-property-grid-array-container { 341 | margin: 10px 0; 342 | border-radius: 4px; 343 | } 344 | 345 | .j-property-grid-array-header { 346 | background: #f1f3f4; 347 | padding: 8px 12px; 348 | font-weight: bold; 349 | display: flex; 350 | justify-content: space-between; 351 | align-items: center; 352 | } 353 | 354 | .j-property-grid-array-body { 355 | padding: 10px; 356 | } 357 | 358 | .j-array-item-grid { 359 | display: flex; 360 | align-items: center; 361 | gap: 10px; 362 | padding: 5px; 363 | } 364 | 365 | .j-array-item-grid:last-child { 366 | border-bottom: none; 367 | } 368 | 369 | /* ===== Standard Form Mode ===== */ 370 | .j-standard-form-fieldset { 371 | border-radius: 5px; 372 | padding: 15px; 373 | margin-bottom: 15px; 374 | background: #fafbfc; 375 | } 376 | 377 | .j-standard-form-fieldset legend { 378 | font-weight: bold; 379 | font-size: 16px; 380 | color: #495057; 381 | padding: 0 10px; 382 | margin-bottom: 0; 383 | } 384 | 385 | .j-standard-form-field { 386 | margin-bottom: 15px; 387 | display: flex; 388 | flex-direction: column; 389 | } 390 | 391 | .j-standard-form-label { 392 | font-weight: 500; 393 | font-size: 14px; 394 | color: #495057; 395 | margin-bottom: 5px; 396 | display: block; 397 | } 398 | 399 | .j-standard-form-input { 400 | width: 100%; 401 | } 402 | 403 | .j-standard-form-input input, 404 | .j-standard-form-input select, 405 | .j-standard-form-input textarea { 406 | width: 100%; 407 | max-width: 400px; 408 | padding: 8px 12px; 409 | border: 1px solid #e0e0e0; 410 | border-radius: 4px; 411 | font-size: 14px; 412 | } 413 | 414 | .j-standard-form-input input:focus, 415 | .j-standard-form-input select:focus, 416 | .j-standard-form-input textarea:focus { 417 | outline: none; 418 | border-color: #80bdff; 419 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 420 | } 421 | 422 | .j-standard-form-array { 423 | margin-bottom: 20px; 424 | border-radius: 5px; 425 | padding: 15px; 426 | } 427 | 428 | .j-standard-form-array h4 { 429 | margin: 0 0 10px 0; 430 | font-size: 16px; 431 | color: #495057; 432 | } 433 | 434 | .j-standard-form-array-controls { 435 | margin-bottom: 15px; 436 | } 437 | 438 | .j-standard-form-array-body { 439 | padding-top: 15px; 440 | } 441 | 442 | .j-array-item-standard { 443 | background: #f8f9fa; 444 | border-radius: 4px; 445 | margin-bottom: 10px; 446 | padding: 10px; 447 | } 448 | 449 | .j-array-item-header { 450 | display: flex; 451 | justify-content: space-between; 452 | align-items: center; 453 | margin-bottom: 10px; 454 | font-weight: bold; 455 | font-size: 14px; 456 | color: #495057; 457 | } 458 | 459 | /* ===== Nested Objects and Arrays Indicators ===== */ 460 | .j-nested-object, 461 | .j-nested-array { 462 | padding: 4px 8px; 463 | background: #e9ecef; 464 | border-radius: 3px; 465 | font-size: 12px; 466 | color: #6c757d; 467 | font-style: italic; 468 | } 469 | 470 | /* ===== Enhanced Button Styles ===== */ 471 | .j-standard-form-array-controls button, 472 | .j-property-grid-array-header button { 473 | background: #007bff; 474 | color: white; 475 | border: none; 476 | padding: 6px 12px; 477 | border-radius: 4px; 478 | font-size: 12px; 479 | cursor: pointer; 480 | transition: background-color 0.2s; 481 | } 482 | 483 | .j-standard-form-array-controls button:hover, 484 | .j-property-grid-array-header button:hover { 485 | background: #0056b3; 486 | } 487 | 488 | .j-array-item-header button { 489 | background: #dc3545; 490 | color: white; 491 | border: none; 492 | padding: 4px 8px; 493 | border-radius: 3px; 494 | font-size: 11px; 495 | cursor: pointer; 496 | } 497 | 498 | .j-array-item-header button:hover { 499 | background: #c82333; 500 | } 501 | 502 | .j-array-item-grid button { 503 | background: #dc3545; 504 | color: white; 505 | border: none; 506 | padding: 4px 8px; 507 | border-radius: 3px; 508 | font-size: 11px; 509 | cursor: pointer; 510 | } 511 | 512 | .j-array-item-grid button:hover { 513 | background: #c82333; 514 | } 515 | 516 | /* ===== Property Grid Empty State ===== */ 517 | .j-property-grid-empty { 518 | padding: 20px; 519 | text-align: center; 520 | color: #6c757d; 521 | font-style: italic; 522 | background: #f8f9fa; 523 | border-radius: 4px; 524 | margin: 10px; 525 | } 526 | 527 | /* ===== Nested Property Grid Tables ===== */ 528 | .j-property-grid-table .j-property-grid-container { 529 | margin: 0; 530 | border: none; 531 | border-radius: 0; 532 | } 533 | 534 | .j-property-grid-table .j-property-grid-table { 535 | font-size: 11px; 536 | } 537 | 538 | .j-property-grid-table .j-property-grid-table th, 539 | .j-property-grid-table .j-property-grid-table td { 540 | padding: 4px 8px; 541 | } 542 | 543 | /* ===== Standard Form Improvements ===== */ 544 | .j-standard-form-fieldset { 545 | position: relative; 546 | } 547 | 548 | .j-standard-form-fieldset fieldset { 549 | margin-top: 10px; 550 | } 551 | 552 | .j-array-header { 553 | background: #f8f9fa; 554 | padding: 8px 12px; 555 | display: flex; 556 | align-items: center; 557 | gap: 8px; 558 | } 559 | 560 | .j-array-body { 561 | padding: 8px; 562 | } 563 | 564 | .j-array-item { 565 | border-radius: 4px; 566 | margin-bottom: 6px; 567 | padding: 8px; 568 | background: #fefefe; 569 | position: relative; 570 | } 571 | 572 | /* ===== Object Containers ===== */ 573 | .j-object-container { 574 | border-radius: 5px; 575 | margin-bottom: 8px; 576 | background: white; 577 | } 578 | 579 | .j-object-header { 580 | background: #f8f9fa; 581 | padding: 8px 12px; 582 | display: flex; 583 | align-items: center; 584 | gap: 8px; 585 | font-weight: bold; 586 | font-size: 14px; 587 | } 588 | 589 | .j-object-body { 590 | padding: 8px; 591 | } 592 | 593 | /* ===== Collapse State ===== */ 594 | .j-collapsed { 595 | display: none !important; 596 | } 597 | 598 | /* ===== Spacer ===== */ 599 | .j-spacer-row { 600 | min-height: 5px; 601 | background-color: #e8f2ff; 602 | margin: 10px 0; 603 | border-radius: 3px; 604 | } 605 | 606 | /* ===== Helper Classes ===== */ 607 | .j-inline-help { 608 | margin-top: 4px; 609 | font-size: 11px; 610 | color: #6c757d; 611 | font-style: italic; 612 | } 613 | 614 | .j-validation-help { 615 | margin-top: 4px; 616 | font-size: 11px; 617 | color: #fd7e14; 618 | } 619 | 620 | .j-required-star { 621 | color: #dc3545; 622 | font-weight: bold; 623 | margin-right: 3px; 624 | } 625 | 626 | /* ===== Validation States ===== */ 627 | .j-input[data-is-valid="false"] { 628 | border-color: #dc3545 !important; 629 | box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25); 630 | } 631 | 632 | .j-input[data-is-valid="true"] + .j-validation-help { 633 | display: none; 634 | } 635 | 636 | /* ===== Responsive Design ===== */ 637 | @media (max-width: 768px) { 638 | .j-label-col { 639 | flex: 0 0 140px; 640 | min-width: 100px; 641 | font-size: 11px; 642 | } 643 | 644 | .j-action-col { 645 | flex: 0 0 22px; 646 | min-width: 22px; 647 | } 648 | 649 | .j-add-array-item, 650 | .j-remove-array-item { 651 | width: 18px; 652 | height: 18px; 653 | font-size: 11px; 654 | margin: 0; 655 | } 656 | 657 | .j-field-row { 658 | padding: 3px 6px; 659 | min-height: 28px; 660 | } 661 | } 662 | 663 | @media (max-width: 480px) { 664 | .j-field-row { 665 | flex-direction: column; 666 | align-items: stretch; 667 | gap: 4px; 668 | } 669 | 670 | .j-label-col { 671 | flex: none; 672 | width: 100%; 673 | max-width: none; 674 | text-align: right; 675 | border-bottom: 1px solid #eee; 676 | padding-bottom: 4px; 677 | margin-bottom: 4px; 678 | } 679 | 680 | .j-input-col { 681 | flex: none; 682 | width: 100%; 683 | } 684 | 685 | .j-action-col { 686 | flex: none; 687 | width: 100%; 688 | justify-content: flex-end; 689 | padding-top: 4px; 690 | } 691 | } 692 | 693 | /* ===== Legacy Table Support (for backwards compatibility) ===== */ 694 | table.j-container { 695 | display: table; 696 | width: 100%; 697 | border-collapse: collapse; 698 | } 699 | 700 | table.j-container td { 701 | padding: 4px 8px; 702 | vertical-align: top; 703 | } 704 | 705 | table.j-container .j-title-col { 706 | width: 180px; 707 | max-width: 200px; 708 | min-width: 120px; 709 | } 710 | 711 | table.j-container .j-body-col { 712 | width: auto; 713 | } 714 | 715 | table.j-container .j-action-col { 716 | width: 24px; 717 | text-align: center; 718 | padding: 0 2px; 719 | } -------------------------------------------------------------------------------- /src/styles/jsonToForm.modern.css: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonToForm v2.0 - Modern CSS Styles 3 | * 4 | * Features: 5 | * - CSS Custom Properties for theming 6 | * - Flexbox/Grid layouts for responsiveness 7 | * - Modern design with smooth animations 8 | * - Accessibility improvements 9 | * - Dark/Light theme support 10 | */ 11 | 12 | /* ===== CSS CUSTOM PROPERTIES (CSS VARIABLES) ===== */ 13 | :root { 14 | /* Color Palette */ 15 | --jtf-primary-color: #007bff; 16 | --jtf-secondary-color: #6c757d; 17 | --jtf-success-color: #28a745; 18 | --jtf-danger-color: #dc3545; 19 | --jtf-warning-color: #ffc107; 20 | --jtf-info-color: #17a2b8; 21 | 22 | /* Background Colors */ 23 | --jtf-bg-primary: #ffffff; 24 | --jtf-bg-secondary: #f8f9fa; 25 | --jtf-bg-tertiary: #e9ecef; 26 | --jtf-bg-input: #ffffff; 27 | --jtf-bg-input-disabled: #f5f5f5; 28 | --jtf-bg-input-focus: #ffffff; 29 | 30 | /* Text Colors */ 31 | --jtf-text-primary: #212529; 32 | --jtf-text-secondary: #6c757d; 33 | --jtf-text-muted: #999999; 34 | --jtf-text-inverse: #ffffff; 35 | 36 | /* Border Colors */ 37 | --jtf-border-color: #dee2e6; 38 | --jtf-border-color-focus: var(--jtf-primary-color); 39 | --jtf-border-color-error: var(--jtf-danger-color); 40 | --jtf-border-color-success: var(--jtf-success-color); 41 | 42 | /* Spacing */ 43 | --jtf-spacing-xs: 0.25rem; 44 | --jtf-spacing-sm: 0.5rem; 45 | --jtf-spacing-md: 1rem; 46 | --jtf-spacing-lg: 1.5rem; 47 | --jtf-spacing-xl: 3rem; 48 | 49 | /* Typography */ 50 | --jtf-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 51 | --jtf-font-size-xs: 0.75rem; 52 | --jtf-font-size-sm: 0.875rem; 53 | --jtf-font-size-base: 1rem; 54 | --jtf-font-size-lg: 1.25rem; 55 | --jtf-font-weight-normal: 400; 56 | --jtf-font-weight-medium: 500; 57 | --jtf-font-weight-bold: 600; 58 | 59 | /* Borders & Radius */ 60 | --jtf-border-width: 1px; 61 | --jtf-border-radius: 0.375rem; 62 | --jtf-border-radius-sm: 0.25rem; 63 | --jtf-border-radius-lg: 0.5rem; 64 | 65 | /* Shadows */ 66 | --jtf-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 67 | --jtf-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 68 | --jtf-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 69 | 70 | /* Transitions */ 71 | --jtf-transition: all 0.2s ease-in-out; 72 | --jtf-transition-fast: all 0.1s ease-in-out; 73 | 74 | /* Z-index */ 75 | --jtf-z-dropdown: 1000; 76 | --jtf-z-modal: 1050; 77 | --jtf-z-tooltip: 1070; 78 | } 79 | 80 | /* ===== DARK THEME ===== */ 81 | [data-json-form-theme="dark"] { 82 | --jtf-bg-primary: #1a1a1a; 83 | --jtf-bg-secondary: #2d2d2d; 84 | --jtf-bg-tertiary: #404040; 85 | --jtf-bg-input: #2d2d2d; 86 | --jtf-bg-input-disabled: #404040; 87 | --jtf-bg-input-focus: #2d2d2d; 88 | 89 | --jtf-text-primary: #ffffff; 90 | --jtf-text-secondary: #b3b3b3; 91 | --jtf-text-muted: #888888; 92 | 93 | --jtf-border-color: #404040; 94 | --jtf-primary-color: #4dabf7; 95 | } 96 | 97 | /* ===== RESET & BASE STYLES ===== */ 98 | .j-container { 99 | font-family: var(--jtf-font-family); 100 | font-size: var(--jtf-font-size-base); 101 | color: var(--jtf-text-primary); 102 | background-color: var(--jtf-bg-primary); 103 | line-height: 1.5; 104 | -webkit-font-smoothing: antialiased; 105 | -moz-osx-font-smoothing: grayscale; 106 | } 107 | 108 | .j-container *, 109 | .j-container *::before, 110 | .j-container *::after { 111 | box-sizing: border-box; 112 | } 113 | 114 | /* ===== LAYOUT COMPONENTS ===== */ 115 | 116 | /* Field Row - Using Flexbox for Modern Layout */ 117 | .j-field-row { 118 | display: flex; 119 | flex-direction: column; 120 | gap: var(--jtf-spacing-sm); 121 | margin-bottom: var(--jtf-spacing-md); 122 | } 123 | 124 | @media (min-width: 768px) { 125 | .j-field-row { 126 | flex-direction: row; 127 | align-items: flex-start; 128 | } 129 | 130 | .j-field-label-col { 131 | flex: 0 0 200px; 132 | padding-right: var(--jtf-spacing-md); 133 | } 134 | 135 | .j-field-input-col { 136 | flex: 1; 137 | min-width: 0; /* Prevent flex item overflow */ 138 | } 139 | } 140 | 141 | /* Object Container */ 142 | .j-object-container { 143 | background-color: var(--jtf-bg-primary); 144 | border: var(--jtf-border-width) solid var(--jtf-border-color); 145 | border-radius: var(--jtf-border-radius); 146 | margin-bottom: var(--jtf-spacing-md); 147 | overflow: hidden; 148 | transition: var(--jtf-transition); 149 | } 150 | 151 | .j-object-header { 152 | background-color: var(--jtf-bg-secondary); 153 | padding: var(--jtf-spacing-sm) var(--jtf-spacing-md); 154 | border-bottom: var(--jtf-border-width) solid var(--jtf-border-color); 155 | font-weight: var(--jtf-font-weight-medium); 156 | display: flex; 157 | align-items: center; 158 | gap: var(--jtf-spacing-sm); 159 | } 160 | 161 | .j-object-body { 162 | padding: var(--jtf-spacing-md); 163 | } 164 | 165 | .j-object-body.j-collapsed { 166 | display: none; 167 | } 168 | 169 | /* Array Container */ 170 | .j-array-container { 171 | background-color: var(--jtf-bg-primary); 172 | border: var(--jtf-border-width) solid var(--jtf-border-color); 173 | border-radius: var(--jtf-border-radius); 174 | margin-bottom: var(--jtf-spacing-md); 175 | overflow: hidden; 176 | } 177 | 178 | .j-array-header { 179 | background-color: var(--jtf-bg-tertiary); 180 | padding: var(--jtf-spacing-sm) var(--jtf-spacing-md); 181 | border-bottom: var(--jtf-border-width) solid var(--jtf-border-color); 182 | font-weight: var(--jtf-font-weight-medium); 183 | display: flex; 184 | align-items: center; 185 | justify-content: space-between; 186 | gap: var(--jtf-spacing-sm); 187 | } 188 | 189 | .j-array-body { 190 | padding: var(--jtf-spacing-md); 191 | } 192 | 193 | .j-array-body.j-collapsed { 194 | display: none; 195 | } 196 | 197 | .j-array-item { 198 | background-color: var(--jtf-bg-secondary); 199 | border: var(--jtf-border-width) solid var(--jtf-border-color); 200 | border-radius: var(--jtf-border-radius-sm); 201 | padding: var(--jtf-spacing-md); 202 | margin-bottom: var(--jtf-spacing-sm); 203 | position: relative; 204 | transition: var(--jtf-transition); 205 | } 206 | 207 | .j-array-item:hover { 208 | box-shadow: var(--jtf-shadow-sm); 209 | } 210 | 211 | .j-array-item:last-child { 212 | margin-bottom: 0; 213 | } 214 | 215 | /* ===== FORM CONTROLS ===== */ 216 | 217 | /* Label Styles */ 218 | .j-field-label { 219 | display: block; 220 | font-weight: var(--jtf-font-weight-medium); 221 | color: var(--jtf-text-primary); 222 | margin-bottom: var(--jtf-spacing-xs); 223 | font-size: var(--jtf-font-size-sm); 224 | } 225 | 226 | /* Input Base Styles */ 227 | .j-input { 228 | display: block; 229 | width: 100%; 230 | max-width: 100%; 231 | min-width: 0; 232 | padding: var(--jtf-spacing-sm) var(--jtf-spacing-sm); 233 | font-size: var(--jtf-font-size-base); 234 | font-weight: var(--jtf-font-weight-normal); 235 | line-height: 1.5; 236 | color: var(--jtf-text-primary); 237 | background-color: var(--jtf-bg-input); 238 | background-clip: padding-box; 239 | border: var(--jtf-border-width) solid var(--jtf-border-color); 240 | border-radius: var(--jtf-border-radius); 241 | transition: var(--jtf-transition); 242 | outline: none; 243 | box-sizing: border-box; 244 | } 245 | 246 | .j-input:focus { 247 | border-color: var(--jtf-border-color-focus); 248 | box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); 249 | background-color: var(--jtf-bg-input-focus); 250 | } 251 | 252 | .j-input:disabled { 253 | background-color: var(--jtf-bg-input-disabled); 254 | opacity: 0.65; 255 | cursor: not-allowed; 256 | } 257 | 258 | .j-input::placeholder { 259 | color: var(--jtf-text-muted); 260 | opacity: 1; 261 | } 262 | 263 | /* Input Variants inherit base styles from .j-input */ 264 | 265 | .j-input-textarea { 266 | min-height: 80px; 267 | resize: vertical; 268 | } 269 | 270 | .j-input-select { 271 | cursor: pointer; 272 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); 273 | background-position: right var(--jtf-spacing-sm) center; 274 | background-repeat: no-repeat; 275 | background-size: 1.5em 1.5em; 276 | padding-right: 2.5rem; 277 | } 278 | 279 | .j-input-color { 280 | width: 60px; 281 | height: 38px; 282 | padding: 2px; 283 | border: var(--jtf-border-width) solid var(--jtf-border-color); 284 | border-radius: var(--jtf-border-radius); 285 | background-color: var(--jtf-bg-input); 286 | cursor: pointer; 287 | } 288 | 289 | /* Checkbox Styles */ 290 | .j-input-checkbox { 291 | width: auto; 292 | margin-right: var(--jtf-spacing-xs); 293 | cursor: pointer; 294 | } 295 | 296 | /* Radio Group Styles */ 297 | .j-radio-group { 298 | display: flex; 299 | flex-direction: column; 300 | gap: var(--jtf-spacing-xs); 301 | } 302 | 303 | @media (min-width: 576px) { 304 | .j-radio-group { 305 | flex-direction: row; 306 | flex-wrap: wrap; 307 | gap: var(--jtf-spacing-md); 308 | } 309 | } 310 | 311 | .j-radio-option { 312 | display: flex; 313 | align-items: center; 314 | cursor: pointer; 315 | font-size: var(--jtf-font-size-sm); 316 | } 317 | 318 | .j-radio-option input[type="radio"] { 319 | width: auto; 320 | margin-right: var(--jtf-spacing-xs); 321 | cursor: pointer; 322 | } 323 | 324 | .j-radio-label { 325 | cursor: pointer; 326 | user-select: none; 327 | } 328 | 329 | /* HTML Editor */ 330 | .j-input-html { 331 | display: none; 332 | } 333 | 334 | .j-input-html-div { 335 | min-height: 100px; 336 | padding: var(--jtf-spacing-sm); 337 | border: var(--jtf-border-width) solid var(--jtf-border-color); 338 | border-radius: var(--jtf-border-radius); 339 | background-color: var(--jtf-bg-input); 340 | transition: var(--jtf-transition); 341 | outline: none; 342 | } 343 | 344 | .j-input-html-div:focus { 345 | border-color: var(--jtf-border-color-focus); 346 | box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); 347 | } 348 | 349 | .j-input-html-div:empty::before { 350 | content: "Enter your content here..."; 351 | color: var(--jtf-text-muted); 352 | } 353 | 354 | /* ===== VALIDATION STATES ===== */ 355 | .j-input[data-is-valid="false"] { 356 | border-color: var(--jtf-border-color-error); 357 | box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25); 358 | } 359 | 360 | .j-input[data-is-valid="true"] { 361 | border-color: var(--jtf-border-color-success); 362 | } 363 | 364 | .j-input-focused { 365 | border-color: var(--jtf-border-color-focus); 366 | box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); 367 | } 368 | 369 | /* ===== HELP TEXT & MESSAGES ===== */ 370 | .j-inline-help { 371 | font-size: var(--jtf-font-size-xs); 372 | color: var(--jtf-text-secondary); 373 | margin-top: var(--jtf-spacing-xs); 374 | line-height: 1.4; 375 | } 376 | 377 | .j-validation-help { 378 | font-size: var(--jtf-font-size-xs); 379 | color: var(--jtf-warning-color); 380 | margin-top: var(--jtf-spacing-xs); 381 | line-height: 1.4; 382 | } 383 | 384 | .j-validation-message { 385 | font-size: var(--jtf-font-size-xs); 386 | color: var(--jtf-danger-color); 387 | margin-top: var(--jtf-spacing-xs); 388 | display: flex; 389 | align-items: center; 390 | gap: var(--jtf-spacing-xs); 391 | } 392 | 393 | .j-validation-message::before { 394 | content: "⚠"; 395 | font-size: 0.875em; 396 | } 397 | 398 | .j-required-star { 399 | color: var(--jtf-danger-color); 400 | font-weight: var(--jtf-font-weight-bold); 401 | margin-left: var(--jtf-spacing-xs); 402 | } 403 | 404 | /* ===== BUTTONS & CONTROLS ===== */ 405 | .j-expand-collapse-btn, 406 | .j-add-array-item, 407 | .j-remove-array-item { 408 | display: inline-flex; 409 | align-items: center; 410 | justify-content: center; 411 | padding: var(--jtf-spacing-xs) var(--jtf-spacing-sm); 412 | font-size: var(--jtf-font-size-sm); 413 | font-weight: var(--jtf-font-weight-medium); 414 | line-height: 1; 415 | color: var(--jtf-text-inverse); 416 | background-color: var(--jtf-primary-color); 417 | border: none; 418 | border-radius: var(--jtf-border-radius-sm); 419 | cursor: pointer; 420 | transition: var(--jtf-transition); 421 | text-decoration: none; 422 | user-select: none; 423 | } 424 | 425 | .j-expand-collapse-btn:hover, 426 | .j-add-array-item:hover, 427 | .j-remove-array-item:hover { 428 | opacity: 0.85; 429 | transform: translateY(-1px); 430 | box-shadow: var(--jtf-shadow-sm); 431 | } 432 | 433 | .j-expand-collapse-btn:active, 434 | .j-add-array-item:active, 435 | .j-remove-array-item:active { 436 | transform: translateY(0); 437 | } 438 | 439 | .j-expand-collapse-btn { 440 | width: 24px; 441 | height: 24px; 442 | padding: 0; 443 | font-size: 0.75rem; 444 | background-color: var(--jtf-secondary-color); 445 | } 446 | 447 | .j-add-array-item { 448 | background-color: var(--jtf-success-color); 449 | gap: var(--jtf-spacing-xs); 450 | } 451 | 452 | .j-remove-array-item { 453 | background-color: var(--jtf-danger-color); 454 | gap: var(--jtf-spacing-xs); 455 | } 456 | 457 | /* ===== SPACER & UTILITY ===== */ 458 | .j-spacer { 459 | display: inline-block; 460 | width: var(--jtf-spacing-md); 461 | } 462 | 463 | .j-spacer-row { 464 | background-color: var(--jtf-bg-secondary); 465 | padding: var(--jtf-spacing-sm) var(--jtf-spacing-md); 466 | margin: var(--jtf-spacing-md) 0; 467 | border-radius: var(--jtf-border-radius); 468 | font-size: var(--jtf-font-size-sm); 469 | font-weight: var(--jtf-font-weight-medium); 470 | color: var(--jtf-text-secondary); 471 | border-left: 4px solid var(--jtf-primary-color); 472 | } 473 | 474 | .j-collapsed { 475 | display: none !important; 476 | } 477 | 478 | /* ===== RESPONSIVE DESIGN ===== */ 479 | @media (max-width: 767px) { 480 | .j-container { 481 | font-size: var(--jtf-font-size-sm); 482 | } 483 | 484 | .j-field-row { 485 | gap: var(--jtf-spacing-xs); 486 | margin-bottom: var(--jtf-spacing-sm); 487 | } 488 | 489 | .j-object-header, 490 | .j-array-header { 491 | padding: var(--jtf-spacing-xs) var(--jtf-spacing-sm); 492 | font-size: var(--jtf-font-size-sm); 493 | } 494 | 495 | .j-object-body, 496 | .j-array-body { 497 | padding: var(--jtf-spacing-sm); 498 | } 499 | 500 | .j-radio-group { 501 | gap: var(--jtf-spacing-sm); 502 | } 503 | 504 | .j-input { 505 | font-size: 16px; /* Prevent zoom on iOS */ 506 | } 507 | } 508 | 509 | /* ===== ACCESSIBILITY ===== */ 510 | @media (prefers-reduced-motion: reduce) { 511 | * { 512 | animation-duration: 0.01ms !important; 513 | animation-iteration-count: 1 !important; 514 | transition-duration: 0.01ms !important; 515 | scroll-behavior: auto !important; 516 | } 517 | } 518 | 519 | /* Focus management for keyboard navigation */ 520 | .j-input:focus-visible { 521 | outline: 2px solid var(--jtf-primary-color); 522 | outline-offset: 2px; 523 | } 524 | 525 | /* Screen reader only content */ 526 | .j-sr-only { 527 | position: absolute; 528 | width: 1px; 529 | height: 1px; 530 | padding: 0; 531 | margin: -1px; 532 | overflow: hidden; 533 | clip: rect(0, 0, 0, 0); 534 | white-space: nowrap; 535 | border: 0; 536 | } 537 | 538 | /* ===== ANIMATIONS ===== */ 539 | @keyframes fadeIn { 540 | from { 541 | opacity: 0; 542 | transform: translateY(-10px); 543 | } 544 | to { 545 | opacity: 1; 546 | transform: translateY(0); 547 | } 548 | } 549 | 550 | .j-array-item { 551 | animation: fadeIn 0.3s ease-out; 552 | } 553 | 554 | /* ===== PRINT STYLES ===== */ 555 | @media print { 556 | .j-container { 557 | background: white !important; 558 | color: black !important; 559 | } 560 | 561 | .j-expand-collapse-btn, 562 | .j-add-array-item, 563 | .j-remove-array-item { 564 | display: none !important; 565 | } 566 | 567 | .j-collapsed { 568 | display: block !important; 569 | } 570 | 571 | .j-input { 572 | border: 1px solid #ccc !important; 573 | box-shadow: none !important; 574 | } 575 | } 576 | 577 | /* ===== LEGACY BROWSER SUPPORT ===== */ 578 | /* Support for browsers that don't support CSS custom properties */ 579 | .j-container.no-css-vars { 580 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; 581 | color: #212529; 582 | background-color: #ffffff; 583 | } 584 | 585 | .j-container.no-css-vars .j-input { 586 | border: 1px solid #dee2e6; 587 | border-radius: 0.375rem; 588 | padding: 0.5rem; 589 | } 590 | 591 | .j-container.no-css-vars .j-input:focus { 592 | border-color: #007bff; 593 | box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); 594 | } --------------------------------------------------------------------------------