├── .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 |
41 | حالت فعلی
42 | جدول ویژگیها
43 | فرم استاندارد
44 |
45 | تغییر حالت
46 |
47 |
48 |
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 |
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)
8 | 
9 | [](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 |
32 |
33 |
34 |
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 |
228 |
229 |
230 |
231 |
232 | json value :
233 |
234 |
235 | Is Valid :
236 | true
237 |
238 |
239 |
240 |
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 |
230 |
231 |
232 |
233 |
234 | json value :
235 |
236 |
237 | Is Valid :
238 | true
239 |
240 |
241 |
242 |
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 `${this.escapeHtml(title)} `;
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 |
44 |
45 |
46 |
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 | }
--------------------------------------------------------------------------------