@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.
- package/README.md +11 -1
- package/docs/user-guide.html +8 -2
- package/index.js +1 -0
- package/nodes/uibuilder-formgen-v3.html +237 -0
- package/nodes/uibuilder-formgen-v3.js +482 -0
- package/nodes/uibuilder-formgen.js +1 -1
- package/package.json +2 -1
- package/templates/index.v3.html.mustache +506 -0
- package/templates/index.v3.js.mustache +463 -0
|
@@ -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
|
+
|