@cyprnet/node-red-contrib-uibuilder-formgen 0.4.13 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,506 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="[[themeMode]]">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <!-- SPDX-License-Identifier: MIT -->
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>[[title]]</title>
8
+ <meta name="color-scheme" content="light dark">
9
+
10
+ <!-- Bootstrap 5 CSS -->
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
12
+
13
+ <style>
14
+ /* PortalSmith theming (Bootstrap 5 + native HTML controls)
15
+ themeMode: [[themeMode]] ('auto' | 'light' | 'dark')
16
+ */
17
+ :root {
18
+ --ps-bg: #f5f5f5;
19
+ --ps-surface: #ffffff;
20
+ --ps-text: #212529;
21
+ --ps-muted: #6c757d;
22
+ --ps-border: #dee2e6;
23
+ --ps-input-bg: #ffffff;
24
+ --ps-input-text: #212529;
25
+ --ps-input-border: #ced4da;
26
+ --ps-code-bg: rgba(0,0,0,0.04);
27
+ --ps-link: #0d6efd;
28
+ --ps-focus: rgba(13,110,253,0.25);
29
+ }
30
+
31
+ html[data-theme="dark"] {
32
+ --ps-bg: #0f1115;
33
+ --ps-surface: #171a21;
34
+ --ps-text: #e6eaf2;
35
+ --ps-muted: #aab3c2;
36
+ --ps-border: #2a2f3a;
37
+ --ps-input-bg: #11141a;
38
+ --ps-input-text: #e6eaf2;
39
+ --ps-input-border: #3a4150;
40
+ --ps-code-bg: rgba(255,255,255,0.08);
41
+ --ps-link: #8ab4ff;
42
+ --ps-focus: rgba(138,180,255,0.25);
43
+ }
44
+
45
+ /* Auto mode: follow OS preference when theme isn't forced */
46
+ @media (prefers-color-scheme: dark) {
47
+ html:not([data-theme]),
48
+ html[data-theme="auto"] {
49
+ --ps-bg: #0f1115;
50
+ --ps-surface: #171a21;
51
+ --ps-text: #e6eaf2;
52
+ --ps-muted: #aab3c2;
53
+ --ps-border: #2a2f3a;
54
+ --ps-input-bg: #11141a;
55
+ --ps-input-text: #e6eaf2;
56
+ --ps-input-border: #3a4150;
57
+ --ps-code-bg: rgba(255,255,255,0.08);
58
+ --ps-link: #8ab4ff;
59
+ --ps-focus: rgba(138,180,255,0.25);
60
+ }
61
+ }
62
+
63
+ body {
64
+ background-color: var(--ps-bg);
65
+ color: var(--ps-text);
66
+ padding: 20px 0;
67
+ }
68
+ a { color: var(--ps-link); }
69
+ .text-muted { color: var(--ps-muted) !important; }
70
+ .border { border-color: var(--ps-border) !important; }
71
+
72
+ .card,
73
+ .modal-content,
74
+ .dropdown-menu,
75
+ .list-group-item {
76
+ background-color: var(--ps-surface);
77
+ color: var(--ps-text);
78
+ border-color: var(--ps-border);
79
+ }
80
+
81
+ .table,
82
+ .table th,
83
+ .table td,
84
+ .table thead th {
85
+ color: var(--ps-text);
86
+ border-color: var(--ps-border) !important;
87
+ }
88
+
89
+ .form-control,
90
+ .form-select {
91
+ background-color: var(--ps-input-bg);
92
+ color: var(--ps-input-text);
93
+ border-color: var(--ps-input-border);
94
+ }
95
+ .form-control:focus,
96
+ .form-select:focus {
97
+ border-color: var(--ps-link);
98
+ box-shadow: 0 0 0 .2rem var(--ps-focus);
99
+ }
100
+
101
+ code {
102
+ color: var(--ps-text);
103
+ background: var(--ps-code-bg);
104
+ padding: 0.1rem 0.25rem;
105
+ border-radius: 0.2rem;
106
+ }
107
+
108
+ /* Header layout */
109
+ .ps-header {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ gap: 1rem;
114
+ margin-bottom: 1rem;
115
+ }
116
+ .ps-logo {
117
+ max-height: 48px;
118
+ max-width: 220px;
119
+ width: auto;
120
+ height: auto;
121
+ object-fit: contain;
122
+ }
123
+ .ps-watermark {
124
+ position: fixed;
125
+ right: 14px;
126
+ bottom: 14px;
127
+ z-index: 9999;
128
+ background: rgba(0,0,0,0.08);
129
+ border: 1px solid rgba(0,0,0,0.10);
130
+ color: var(--ps-text);
131
+ padding: 6px 10px;
132
+ border-radius: 8px;
133
+ font-size: 12px;
134
+ backdrop-filter: blur(2px);
135
+ }
136
+ html[data-theme="dark"] .ps-watermark {
137
+ background: rgba(255,255,255,0.10);
138
+ border-color: rgba(255,255,255,0.14);
139
+ }
140
+ .form-container {
141
+ max-width: 800px;
142
+ margin: 0 auto;
143
+ }
144
+ .field-error {
145
+ color: #dc3545;
146
+ font-size: 0.875rem;
147
+ margin-top: 0.25rem;
148
+ }
149
+ .section-header {
150
+ border-bottom: 2px solid #0d6efd;
151
+ padding-bottom: 0.5rem;
152
+ margin-bottom: 1.5rem;
153
+ margin-top: 2rem;
154
+ }
155
+ .section-header:first-child {
156
+ margin-top: 0;
157
+ }
158
+ .action-buttons {
159
+ margin-top: 2rem;
160
+ padding-top: 1rem;
161
+ border-top: 1px solid var(--ps-border);
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ align-items: center;
165
+ gap: 0.5rem;
166
+ }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <div id="app">
171
+ <div class="container form-container">
172
+ <div class="ps-header">
173
+ <h1 class="mb-0">[[title]]</h1>
174
+ [[#logoUrl]]
175
+ <img class="ps-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
176
+ [[/logoUrl]]
177
+ [[^logoUrl]]
178
+ [[^licensed]]
179
+ <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
180
+ <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
181
+ <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
182
+ <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
183
+ </svg>
184
+ [[/licensed]]
185
+ [[/logoUrl]]
186
+ </div>
187
+ <p v-if="description" class="text-muted mb-4">[[description]]</p>
188
+
189
+ <!-- Status Alert Area -->
190
+ <div
191
+ v-show="showAlert"
192
+ class="alert alert-dismissible fade show mb-3"
193
+ :class="'alert-' + (alertVariant || 'info')"
194
+ role="alert"
195
+ >
196
+ <span v-html="alertMessage"></span>
197
+ <button type="button" class="btn-close" @click="showAlert = false" aria-label="Close"></button>
198
+ </div>
199
+
200
+ <!-- Result View -->
201
+ <div v-if="showResult" class="card mb-3">
202
+ <div class="card-body">
203
+ <div class="d-flex justify-content-between align-items-center mb-2">
204
+ <h3 class="mb-0">Result</h3>
205
+ <div class="btn-group" role="group" aria-label="Result actions">
206
+ <button type="button" class="btn btn-outline-secondary" @click="showResult = false">
207
+ Back to form
208
+ </button>
209
+ <button type="button" class="btn btn-outline-primary" @click="downloadResultJson" :disabled="!lastResult">
210
+ Download JSON
211
+ </button>
212
+ </div>
213
+ </div>
214
+ <div v-if="lastResultStatus" class="text-muted mb-2">
215
+ HTTP Status: {{lastResultStatus}}
216
+ </div>
217
+
218
+ <div class="btn-group mb-2" role="group" aria-label="Result view toggle">
219
+ <button type="button" class="btn btn-sm" :class="resultView === 'table' ? 'btn-primary' : 'btn-outline-primary'" @click="resultView = 'table'">Table</button>
220
+ <button type="button" class="btn btn-sm" :class="resultView === 'json' ? 'btn-primary' : 'btn-outline-primary'" @click="resultView = 'json'">JSON</button>
221
+ </div>
222
+
223
+ <div v-if="resultView === 'table'">
224
+ <div v-if="formattedResult && formattedResult.length" class="table-responsive">
225
+ <table class="table table-bordered table-sm">
226
+ <thead>
227
+ <tr>
228
+ <th v-for="h in resultHeaders" :key="h">{{h}}</th>
229
+ </tr>
230
+ </thead>
231
+ <tbody>
232
+ <tr v-for="(row, idx) in formattedResult" :key="'r-' + idx">
233
+ <td v-for="h in resultHeaders" :key="'c-' + idx + '-' + h">{{row[h]}}</td>
234
+ </tr>
235
+ </tbody>
236
+ </table>
237
+ </div>
238
+ <div v-else class="text-muted">No table-friendly data found.</div>
239
+ </div>
240
+
241
+ <pre v-else class="mt-2"><code>{{ lastResultPretty }}</code></pre>
242
+ </div>
243
+ </div>
244
+
245
+ <div v-else class="card">
246
+ <div class="card-body">
247
+ <form @submit.prevent="handleSubmit">
248
+
249
+ <!-- Sections -->
250
+ <div v-for="section in schema.sections" :key="section.id" class="mb-4">
251
+ <div class="section-header">
252
+ <h2 class="h4 mb-1">{{section.title}}</h2>
253
+ <p v-if="section.description" class="text-muted mb-0">{{section.description}}</p>
254
+ </div>
255
+
256
+ <!-- Fields -->
257
+ <div v-for="field in visibleFieldsBySection[section.id]" :key="field.id" class="mb-3">
258
+ <label :for="field.id" class="form-label">
259
+ {{field.label}}
260
+ <span v-if="field.required" class="text-danger">*</span>
261
+ </label>
262
+
263
+ <!-- Text / Email / Tel / URL / Password -->
264
+ <input
265
+ v-if="field.type === 'text'"
266
+ :id="field.id"
267
+ v-model="formData[field.id]"
268
+ :type="field.inputType || 'text'"
269
+ :placeholder="field.placeholder"
270
+ class="form-control"
271
+ @blur="validateField(field)"
272
+ >
273
+
274
+ <!-- Textarea -->
275
+ <textarea
276
+ v-else-if="field.type === 'textarea'"
277
+ :id="field.id"
278
+ v-model="formData[field.id]"
279
+ :placeholder="field.placeholder"
280
+ class="form-control"
281
+ :rows="field.rows || 3"
282
+ @blur="validateField(field)"
283
+ ></textarea>
284
+
285
+ <!-- Number -->
286
+ <input
287
+ v-else-if="field.type === 'number'"
288
+ :id="field.id"
289
+ v-model.number="formData[field.id]"
290
+ type="number"
291
+ :placeholder="field.placeholder"
292
+ :min="field.min"
293
+ :max="field.max"
294
+ :step="field.step"
295
+ class="form-control"
296
+ @blur="validateField(field)"
297
+ >
298
+
299
+ <!-- Select -->
300
+ <select
301
+ v-else-if="field.type === 'select'"
302
+ :id="field.id"
303
+ v-model="formData[field.id]"
304
+ class="form-select"
305
+ @change="validateField(field)"
306
+ >
307
+ <option value="">-- select --</option>
308
+ <option
309
+ v-for="(opt, optIdx) in (field.options || [])"
310
+ :key="field.id + '-opt-' + optIdx"
311
+ :value="(typeof opt === 'string') ? opt : opt.value"
312
+ >{{ (typeof opt === 'string') ? opt : (opt.text || opt.label || opt.name || opt.value) }}</option>
313
+ </select>
314
+
315
+ <!-- Checkbox -->
316
+ <div v-else-if="field.type === 'checkbox'" class="form-check">
317
+ <input
318
+ type="checkbox"
319
+ class="form-check-input"
320
+ :id="field.id"
321
+ v-model="formData[field.id]"
322
+ @change="validateField(field)"
323
+ >
324
+ <label class="form-check-label" :for="field.id">{{field.checkboxLabel || field.label}}</label>
325
+ </div>
326
+
327
+ <!-- Radio -->
328
+ <div v-else-if="field.type === 'radio'">
329
+ <div
330
+ v-for="(opt, optIdx) in (field.options || [])"
331
+ :key="field.id + '-radio-' + optIdx"
332
+ class="form-check"
333
+ >
334
+ <input
335
+ type="radio"
336
+ class="form-check-input"
337
+ :id="field.id + '-radio-' + optIdx"
338
+ :name="field.id"
339
+ :value="(typeof opt === 'string') ? opt : opt.value"
340
+ v-model="formData[field.id]"
341
+ @change="validateField(field)"
342
+ >
343
+ <label class="form-check-label" :for="field.id + '-radio-' + optIdx">{{ (typeof opt === 'string') ? opt : (opt.text || opt.label || opt.name || opt.value) }}</label>
344
+ </div>
345
+ </div>
346
+
347
+ <!-- Date -->
348
+ <input
349
+ v-else-if="field.type === 'date'"
350
+ :id="field.id"
351
+ v-model="formData[field.id]"
352
+ type="date"
353
+ class="form-control"
354
+ @blur="validateField(field)"
355
+ >
356
+
357
+ <!-- Key-Value Pairs -->
358
+ <div v-else-if="field.type === 'keyvalue'">
359
+ <!-- Individual Pairs Mode -->
360
+ <div v-if="!field.keyvalueMode || field.keyvalueMode === 'pairs'" class="keyvalue-pairs">
361
+ <div
362
+ v-for="(pair, pairIdx) in formData[field.id]"
363
+ :key="field.id + '-pair-' + pairIdx"
364
+ class="mb-2 border rounded p-2"
365
+ :style="{ backgroundColor: 'var(--ps-surface)' }"
366
+ >
367
+ <div class="row">
368
+ <div class="col-md-5 mb-2 mb-md-0">
369
+ <input
370
+ type="text"
371
+ class="form-control"
372
+ v-model="pair.key"
373
+ placeholder="Key"
374
+ @blur="validateField(field)"
375
+ >
376
+ </div>
377
+ <div class="col-md-5 mb-2 mb-md-0">
378
+ <input
379
+ type="text"
380
+ class="form-control"
381
+ v-model="pair.value"
382
+ placeholder="Value"
383
+ @blur="validateField(field)"
384
+ >
385
+ </div>
386
+ <div class="col-md-2 text-md-right">
387
+ <button type="button" class="btn btn-outline-danger btn-sm" @click="removePair(field.id, pairIdx)">
388
+ Remove
389
+ </button>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ <button type="button" class="btn btn-outline-primary btn-sm" @click="addPair(field.id)">
394
+ Add pair
395
+ </button>
396
+ </div>
397
+
398
+ <!-- Delimiter Mode -->
399
+ <div v-else class="keyvalue-delimiter">
400
+ <textarea
401
+ :id="field.id"
402
+ v-model="formData[field.id]"
403
+ class="form-control"
404
+ :rows="field.rows || 4"
405
+ :placeholder="field.placeholder || ('one per line, e.g. vlan_id=101')"
406
+ @blur="validateField(field)"
407
+ ></textarea>
408
+ <small class="text-muted">Delimiter: <code>{{field.keyvalueDelimiter || '='}}</code> (output is an array of strings)</small>
409
+ </div>
410
+ </div>
411
+
412
+ <!-- Help text -->
413
+ <small v-if="field.help" class="form-text text-muted">{{field.help}}</small>
414
+
415
+ <!-- Error message -->
416
+ <div v-if="fieldErrors[field.id]" class="field-error">
417
+ {{fieldErrors[field.id]}}
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ <!-- Actions -->
423
+ <div class="action-buttons">
424
+ <button type="button" class="btn btn-secondary" @click="handleSaveDraft" :disabled="saving">
425
+ Save Draft
426
+ </button>
427
+
428
+ <label class="btn btn-secondary mb-0" for="ps-draft-file-input-trigger">
429
+ Load Draft
430
+ </label>
431
+ <input
432
+ id="ps-draft-file-input-trigger"
433
+ type="file"
434
+ accept="application/json,.json"
435
+ style="position:fixed;left:-9999px;top:-9999px"
436
+ @change="handleDraftFileSelected"
437
+ />
438
+
439
+ <button type="button" class="btn btn-outline-secondary" @click="handleClearForm">
440
+ Clear Form
441
+ </button>
442
+
443
+ <button type="submit" class="btn btn-primary" :disabled="submitting">
444
+ Submit
445
+ </button>
446
+
447
+ <!-- CopyBlock Actions -->
448
+ <template v-for="action in copyBlockActions" :key="action.id">
449
+ <button
450
+ type="button"
451
+ class="btn btn-info"
452
+ @click="handleCopyBlock(action)"
453
+ >
454
+ Copy Block
455
+ </button>
456
+ </template>
457
+
458
+ <!-- Export Buttons -->
459
+ <div class="dropdown" v-if="exportFormats.length > 0">
460
+ <button
461
+ type="button"
462
+ class="btn btn-outline-secondary dropdown-toggle"
463
+ data-bs-toggle="dropdown"
464
+ aria-haspopup="true"
465
+ aria-expanded="false"
466
+ >
467
+ Export
468
+ </button>
469
+ <ul class="dropdown-menu">
470
+ <li v-for="format in exportFormats" :key="format">
471
+ <button
472
+ type="button"
473
+ class="dropdown-item"
474
+ @click="handleExport(format)"
475
+ >
476
+ {{format.toUpperCase()}}
477
+ </button>
478
+ </li>
479
+ </ul>
480
+ </div>
481
+ </div>
482
+
483
+ <!-- License watermark (FREE mode) -->
484
+ <div v-show="license && license.watermark" class="ps-watermark">
485
+ Powered by <strong>PortalSmith FormGen</strong>
486
+ </div>
487
+ </form>
488
+ </div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+
493
+ <!-- Bootstrap 5 JS (bundle includes Popper) -->
494
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
495
+
496
+ <!-- Vue 3 -->
497
+ <script src="https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.global.prod.js"></script>
498
+
499
+ <!-- uibuilder client script (uibuilder v7 IIFE) -->
500
+ <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
501
+
502
+ <!-- Portal Logic -->
503
+ <script defer src="./index.js"></script>
504
+ </body>
505
+ </html>
506
+