@mosaicoo/form-angular 0.2.0 → 0.4.0
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 +18 -0
- package/fesm2022/mosaicoo-form-angular-designer.mjs +1305 -0
- package/fesm2022/mosaicoo-form-angular-designer.mjs.map +1 -0
- package/fesm2022/mosaicoo-form-angular.mjs +464 -112
- package/fesm2022/mosaicoo-form-angular.mjs.map +1 -1
- package/package.json +6 -2
- package/types/mosaicoo-form-angular-designer.d.ts +155 -0
- package/types/mosaicoo-form-angular.d.ts +137 -66
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input as input$1, output, signal, ViewEncapsulation, ChangeDetectionStrategy, Component, computed, effect } from '@angular/core';
|
|
3
|
+
import { JsonPipe } from '@angular/common';
|
|
4
|
+
import { collectInputs, generateKey, validateSchema, createFormEngine, getNodeAt, insertNode, parentOf, removeNode, duplicateNode, moveNode, updateNode } from '@mosaicoo/form-core';
|
|
5
|
+
import { importForm, createLegacyImporter } from '@mosaicoo/form-core/interop';
|
|
6
|
+
import { FormNodeComponent, FormRendererComponent } from '@mosaicoo/form-angular';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Canvas node: draws the selection frame and structure chrome, delegating
|
|
10
|
+
* leaf rendering to the RUNTIME `<mform-node>` so what you see is what the
|
|
11
|
+
* renderer will produce. Canvas controls are non-interactive — testing
|
|
12
|
+
* happens in Preview mode.
|
|
13
|
+
*/
|
|
14
|
+
class DesignerNodeComponent {
|
|
15
|
+
node = input$1.required(...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
|
|
16
|
+
path = input$1.required(...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
17
|
+
engine = input$1.required(...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
|
|
18
|
+
selected = input$1(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
19
|
+
selectNode = output();
|
|
20
|
+
activeTab = signal(0, ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
21
|
+
container() {
|
|
22
|
+
return this.node();
|
|
23
|
+
}
|
|
24
|
+
chip() {
|
|
25
|
+
const node = this.node();
|
|
26
|
+
const label = node.kind === 'container' || node.kind === 'static'
|
|
27
|
+
? node.label ?? node.key
|
|
28
|
+
: node.label ?? node.key;
|
|
29
|
+
return `${label} · ${node.type}`;
|
|
30
|
+
}
|
|
31
|
+
isSelected() {
|
|
32
|
+
const selected = this.selected();
|
|
33
|
+
return selected !== null && selected.join('.') === this.path().join('.');
|
|
34
|
+
}
|
|
35
|
+
childPath(index) {
|
|
36
|
+
return [...this.path(), index];
|
|
37
|
+
}
|
|
38
|
+
columnsTemplate() {
|
|
39
|
+
const columns = this.container().columns ?? [];
|
|
40
|
+
const total = columns.reduce((sum, c) => sum + c.width, 0) || 12;
|
|
41
|
+
return columns.map((c) => `${((c.width / total) * 100).toFixed(3)}fr`).join(' ');
|
|
42
|
+
}
|
|
43
|
+
onSelect(event) {
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
this.selectNode.emit(this.path());
|
|
46
|
+
}
|
|
47
|
+
setTab(index, event) {
|
|
48
|
+
event.stopPropagation();
|
|
49
|
+
this.activeTab.set(index);
|
|
50
|
+
}
|
|
51
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DesignerNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
52
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: DesignerNodeComponent, isStandalone: true, selector: "mform-designer-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectNode: "selectNode" }, ngImport: i0, template: `
|
|
53
|
+
@let n = node();
|
|
54
|
+
<div
|
|
55
|
+
class="dz-node"
|
|
56
|
+
[class.is-selected]="isSelected()"
|
|
57
|
+
[class.is-container]="n.kind === 'container'"
|
|
58
|
+
(click)="onSelect($event)"
|
|
59
|
+
role="button"
|
|
60
|
+
tabindex="0"
|
|
61
|
+
(keydown.enter)="onSelect($event)"
|
|
62
|
+
[attr.aria-label]="chip() + (isSelected() ? ' (selected)' : '')"
|
|
63
|
+
>
|
|
64
|
+
<span class="dz-chip">{{ chip() }}</span>
|
|
65
|
+
|
|
66
|
+
@if (n.kind === 'container') {
|
|
67
|
+
@if (n.type === 'tabs') {
|
|
68
|
+
<div class="dz-tabbar">
|
|
69
|
+
@for (tab of container().children; track tab.key; let i = $index) {
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="dz-tab"
|
|
73
|
+
[class.is-active]="activeTab() === i"
|
|
74
|
+
(click)="setTab(i, $event)"
|
|
75
|
+
>
|
|
76
|
+
{{ tab.label || 'Tab ' + (i + 1) }}
|
|
77
|
+
</button>
|
|
78
|
+
}
|
|
79
|
+
</div>
|
|
80
|
+
@for (tab of container().children; track tab.key; let i = $index) {
|
|
81
|
+
@if (activeTab() === i) {
|
|
82
|
+
<div class="dz-children">
|
|
83
|
+
<mform-designer-node
|
|
84
|
+
[node]="tab"
|
|
85
|
+
[path]="childPath(i)"
|
|
86
|
+
[engine]="engine()"
|
|
87
|
+
[selected]="selected()"
|
|
88
|
+
(selectNode)="selectNode.emit($event)"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} @else if (n.type === 'columns' && container().columns?.length) {
|
|
94
|
+
<div class="dz-columns" [style.grid-template-columns]="columnsTemplate()">
|
|
95
|
+
@for (column of container().columns; track $index) {
|
|
96
|
+
<div class="dz-column">
|
|
97
|
+
@for (childIndex of column.children; track childIndex) {
|
|
98
|
+
<mform-designer-node
|
|
99
|
+
[node]="container().children[childIndex]!"
|
|
100
|
+
[path]="childPath(childIndex)"
|
|
101
|
+
[engine]="engine()"
|
|
102
|
+
[selected]="selected()"
|
|
103
|
+
(selectNode)="selectNode.emit($event)"
|
|
104
|
+
/>
|
|
105
|
+
}
|
|
106
|
+
@if (column.children.length === 0) {
|
|
107
|
+
<div class="dz-empty">empty column</div>
|
|
108
|
+
}
|
|
109
|
+
</div>
|
|
110
|
+
}
|
|
111
|
+
</div>
|
|
112
|
+
} @else {
|
|
113
|
+
<div class="dz-children">
|
|
114
|
+
@for (child of container().children; track child.key; let i = $index) {
|
|
115
|
+
<mform-designer-node
|
|
116
|
+
[node]="child"
|
|
117
|
+
[path]="childPath(i)"
|
|
118
|
+
[engine]="engine()"
|
|
119
|
+
[selected]="selected()"
|
|
120
|
+
(selectNode)="selectNode.emit($event)"
|
|
121
|
+
/>
|
|
122
|
+
} @empty {
|
|
123
|
+
<div class="dz-empty">empty — select and add fields</div>
|
|
124
|
+
}
|
|
125
|
+
</div>
|
|
126
|
+
}
|
|
127
|
+
} @else {
|
|
128
|
+
<div class="dz-leaf" inert>
|
|
129
|
+
<mform-node [node]="n" [engine]="engine()" [tick]="0" />
|
|
130
|
+
</div>
|
|
131
|
+
}
|
|
132
|
+
</div>
|
|
133
|
+
`, isInline: true, dependencies: [{ kind: "component", type: DesignerNodeComponent, selector: "mform-designer-node", inputs: ["node", "path", "engine", "selected"], outputs: ["selectNode"] }, { kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
134
|
+
}
|
|
135
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DesignerNodeComponent, decorators: [{
|
|
136
|
+
type: Component,
|
|
137
|
+
args: [{
|
|
138
|
+
selector: 'mform-designer-node',
|
|
139
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
140
|
+
encapsulation: ViewEncapsulation.None,
|
|
141
|
+
imports: [FormNodeComponent],
|
|
142
|
+
template: `
|
|
143
|
+
@let n = node();
|
|
144
|
+
<div
|
|
145
|
+
class="dz-node"
|
|
146
|
+
[class.is-selected]="isSelected()"
|
|
147
|
+
[class.is-container]="n.kind === 'container'"
|
|
148
|
+
(click)="onSelect($event)"
|
|
149
|
+
role="button"
|
|
150
|
+
tabindex="0"
|
|
151
|
+
(keydown.enter)="onSelect($event)"
|
|
152
|
+
[attr.aria-label]="chip() + (isSelected() ? ' (selected)' : '')"
|
|
153
|
+
>
|
|
154
|
+
<span class="dz-chip">{{ chip() }}</span>
|
|
155
|
+
|
|
156
|
+
@if (n.kind === 'container') {
|
|
157
|
+
@if (n.type === 'tabs') {
|
|
158
|
+
<div class="dz-tabbar">
|
|
159
|
+
@for (tab of container().children; track tab.key; let i = $index) {
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="dz-tab"
|
|
163
|
+
[class.is-active]="activeTab() === i"
|
|
164
|
+
(click)="setTab(i, $event)"
|
|
165
|
+
>
|
|
166
|
+
{{ tab.label || 'Tab ' + (i + 1) }}
|
|
167
|
+
</button>
|
|
168
|
+
}
|
|
169
|
+
</div>
|
|
170
|
+
@for (tab of container().children; track tab.key; let i = $index) {
|
|
171
|
+
@if (activeTab() === i) {
|
|
172
|
+
<div class="dz-children">
|
|
173
|
+
<mform-designer-node
|
|
174
|
+
[node]="tab"
|
|
175
|
+
[path]="childPath(i)"
|
|
176
|
+
[engine]="engine()"
|
|
177
|
+
[selected]="selected()"
|
|
178
|
+
(selectNode)="selectNode.emit($event)"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} @else if (n.type === 'columns' && container().columns?.length) {
|
|
184
|
+
<div class="dz-columns" [style.grid-template-columns]="columnsTemplate()">
|
|
185
|
+
@for (column of container().columns; track $index) {
|
|
186
|
+
<div class="dz-column">
|
|
187
|
+
@for (childIndex of column.children; track childIndex) {
|
|
188
|
+
<mform-designer-node
|
|
189
|
+
[node]="container().children[childIndex]!"
|
|
190
|
+
[path]="childPath(childIndex)"
|
|
191
|
+
[engine]="engine()"
|
|
192
|
+
[selected]="selected()"
|
|
193
|
+
(selectNode)="selectNode.emit($event)"
|
|
194
|
+
/>
|
|
195
|
+
}
|
|
196
|
+
@if (column.children.length === 0) {
|
|
197
|
+
<div class="dz-empty">empty column</div>
|
|
198
|
+
}
|
|
199
|
+
</div>
|
|
200
|
+
}
|
|
201
|
+
</div>
|
|
202
|
+
} @else {
|
|
203
|
+
<div class="dz-children">
|
|
204
|
+
@for (child of container().children; track child.key; let i = $index) {
|
|
205
|
+
<mform-designer-node
|
|
206
|
+
[node]="child"
|
|
207
|
+
[path]="childPath(i)"
|
|
208
|
+
[engine]="engine()"
|
|
209
|
+
[selected]="selected()"
|
|
210
|
+
(selectNode)="selectNode.emit($event)"
|
|
211
|
+
/>
|
|
212
|
+
} @empty {
|
|
213
|
+
<div class="dz-empty">empty — select and add fields</div>
|
|
214
|
+
}
|
|
215
|
+
</div>
|
|
216
|
+
}
|
|
217
|
+
} @else {
|
|
218
|
+
<div class="dz-leaf" inert>
|
|
219
|
+
<mform-node [node]="n" [engine]="engine()" [tick]="0" />
|
|
220
|
+
</div>
|
|
221
|
+
}
|
|
222
|
+
</div>
|
|
223
|
+
`,
|
|
224
|
+
}]
|
|
225
|
+
}], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], selectNode: [{ type: i0.Output, args: ["selectNode"] }] } });
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Property panel of the selected node. Emits immutable patches — the shell
|
|
229
|
+
* owns the document, history and validation.
|
|
230
|
+
*/
|
|
231
|
+
class InspectorComponent {
|
|
232
|
+
node = input$1.required(...(ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []));
|
|
233
|
+
schema = input$1.required(...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
|
|
234
|
+
nodePatch = output();
|
|
235
|
+
field() {
|
|
236
|
+
return this.node();
|
|
237
|
+
}
|
|
238
|
+
static_() {
|
|
239
|
+
return this.node();
|
|
240
|
+
}
|
|
241
|
+
hasOptions() {
|
|
242
|
+
return ['select', 'radio', 'checkbox-group'].includes(this.node().type);
|
|
243
|
+
}
|
|
244
|
+
otherInputPaths = computed(() => collectInputs(this.schema())
|
|
245
|
+
.map((entry) => entry.path)
|
|
246
|
+
.filter((path) => !path.includes('*')), ...(ngDevMode ? [{ debugName: "otherInputPaths" }] : /* istanbul ignore next */ []));
|
|
247
|
+
emitPatch(patch) {
|
|
248
|
+
this.nodePatch.emit({ patch });
|
|
249
|
+
}
|
|
250
|
+
inputValue(event) {
|
|
251
|
+
return event.target.value;
|
|
252
|
+
}
|
|
253
|
+
patchText(member, event) {
|
|
254
|
+
const value = this.inputValue(event).trim();
|
|
255
|
+
if (member === 'key' && value === '')
|
|
256
|
+
return; // key can never be emptied
|
|
257
|
+
this.emitPatch({ [member]: value === '' ? undefined : value });
|
|
258
|
+
}
|
|
259
|
+
patchFlag(member, event) {
|
|
260
|
+
const checked = event.target.checked;
|
|
261
|
+
this.emitPatch({ [member]: checked ? true : undefined });
|
|
262
|
+
}
|
|
263
|
+
defaultValueText() {
|
|
264
|
+
const value = this.field().defaultValue;
|
|
265
|
+
return value === undefined || value === null ? '' : String(value);
|
|
266
|
+
}
|
|
267
|
+
patchDefault(event) {
|
|
268
|
+
const raw = this.inputValue(event);
|
|
269
|
+
if (raw === '') {
|
|
270
|
+
this.emitPatch({ defaultValue: undefined });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const field = this.field();
|
|
274
|
+
let value = raw;
|
|
275
|
+
if (field.dataType === 'number')
|
|
276
|
+
value = Number(raw);
|
|
277
|
+
if (field.dataType === 'boolean')
|
|
278
|
+
value = raw === 'true';
|
|
279
|
+
this.emitPatch({ defaultValue: value });
|
|
280
|
+
}
|
|
281
|
+
// -- validators -----------------------------------------------------------
|
|
282
|
+
validators() {
|
|
283
|
+
return this.field().validators ?? [];
|
|
284
|
+
}
|
|
285
|
+
isRequired() {
|
|
286
|
+
return this.validators().some((v) => v.type === 'required');
|
|
287
|
+
}
|
|
288
|
+
toggleRequired(event) {
|
|
289
|
+
const checked = event.target.checked;
|
|
290
|
+
const rest = this.validators().filter((v) => v.type !== 'required');
|
|
291
|
+
this.emitValidators(checked ? [{ type: 'required' }, ...rest] : rest);
|
|
292
|
+
}
|
|
293
|
+
validatorValue(type) {
|
|
294
|
+
const spec = this.validators().find((v) => v.type === type);
|
|
295
|
+
return spec && 'value' in spec ? String(spec.value) : '';
|
|
296
|
+
}
|
|
297
|
+
patchValidator(type, event) {
|
|
298
|
+
const raw = this.inputValue(event);
|
|
299
|
+
const rest = this.validators().filter((v) => v.type !== type);
|
|
300
|
+
this.emitValidators(raw === '' ? rest : [...rest, { type, value: Number(raw) }]);
|
|
301
|
+
}
|
|
302
|
+
patternValue() {
|
|
303
|
+
const spec = this.validators().find((v) => v.type === 'pattern');
|
|
304
|
+
return spec && 'value' in spec ? String(spec.value) : '';
|
|
305
|
+
}
|
|
306
|
+
patchPattern(event) {
|
|
307
|
+
const raw = this.inputValue(event);
|
|
308
|
+
const rest = this.validators().filter((v) => v.type !== 'pattern');
|
|
309
|
+
this.emitValidators(raw === '' ? rest : [...rest, { type: 'pattern', value: raw }]);
|
|
310
|
+
}
|
|
311
|
+
emitValidators(validators) {
|
|
312
|
+
this.emitPatch({ validators: validators.length ? validators : undefined });
|
|
313
|
+
}
|
|
314
|
+
// -- options --------------------------------------------------------------
|
|
315
|
+
patchOption(index, member, event) {
|
|
316
|
+
const options = [...(this.field().options ?? [])];
|
|
317
|
+
options[index] = { ...options[index], [member]: this.inputValue(event) };
|
|
318
|
+
this.emitPatch({ options });
|
|
319
|
+
}
|
|
320
|
+
addOption() {
|
|
321
|
+
const options = [...(this.field().options ?? [])];
|
|
322
|
+
options.push({ label: `Option ${options.length + 1}`, value: `option${options.length + 1}` });
|
|
323
|
+
this.emitPatch({ options });
|
|
324
|
+
}
|
|
325
|
+
removeOption(index) {
|
|
326
|
+
const options = (this.field().options ?? []).filter((_, i) => i !== index);
|
|
327
|
+
this.emitPatch({ options: options.length ? options : undefined });
|
|
328
|
+
}
|
|
329
|
+
// -- columns --------------------------------------------------------------
|
|
330
|
+
columnWidths() {
|
|
331
|
+
return (this.node().columns ?? []).map((c) => c.width).join(', ');
|
|
332
|
+
}
|
|
333
|
+
patchColumnWidths(event) {
|
|
334
|
+
const container = this.node();
|
|
335
|
+
const widths = this.inputValue(event)
|
|
336
|
+
.split(',')
|
|
337
|
+
.map((part) => Number(part.trim()))
|
|
338
|
+
.filter((width) => Number.isFinite(width) && width > 0);
|
|
339
|
+
if (widths.length === 0)
|
|
340
|
+
return;
|
|
341
|
+
const existing = container.columns ?? [];
|
|
342
|
+
const columns = widths.map((width, i) => ({
|
|
343
|
+
width,
|
|
344
|
+
children: existing[i]?.children ?? [],
|
|
345
|
+
}));
|
|
346
|
+
// children in dropped columns are re-attached to the last remaining one
|
|
347
|
+
for (let i = widths.length; i < existing.length; i++) {
|
|
348
|
+
columns[columns.length - 1].children.push(...existing[i].children);
|
|
349
|
+
}
|
|
350
|
+
this.emitPatch({ columns });
|
|
351
|
+
}
|
|
352
|
+
// -- conditional ----------------------------------------------------------
|
|
353
|
+
simpleCondition() {
|
|
354
|
+
const condition = this.node().conditional;
|
|
355
|
+
return condition && 'field' in condition ? condition : null;
|
|
356
|
+
}
|
|
357
|
+
conditionExpression() {
|
|
358
|
+
const condition = this.node().conditional;
|
|
359
|
+
return condition && 'expression' in condition ? condition.expression : '';
|
|
360
|
+
}
|
|
361
|
+
conditionValueText() {
|
|
362
|
+
const value = this.simpleCondition()?.value;
|
|
363
|
+
return value === undefined || value === null ? '' : String(value);
|
|
364
|
+
}
|
|
365
|
+
patchConditionField(event) {
|
|
366
|
+
const field = this.inputValue(event);
|
|
367
|
+
if (field === '') {
|
|
368
|
+
this.emitPatch({ conditional: undefined });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const current = this.simpleCondition();
|
|
372
|
+
const conditional = {
|
|
373
|
+
show: current?.show ?? true,
|
|
374
|
+
op: current?.op ?? 'eq',
|
|
375
|
+
field,
|
|
376
|
+
value: current?.value ?? '',
|
|
377
|
+
};
|
|
378
|
+
this.emitPatch({ conditional });
|
|
379
|
+
}
|
|
380
|
+
patchConditionValue(event) {
|
|
381
|
+
const current = this.simpleCondition();
|
|
382
|
+
if (!current)
|
|
383
|
+
return;
|
|
384
|
+
const raw = this.inputValue(event);
|
|
385
|
+
const value = raw === 'true' ? true : raw === 'false' ? false : raw;
|
|
386
|
+
this.emitPatch({ conditional: { ...current, value } });
|
|
387
|
+
}
|
|
388
|
+
patchConditionExpression(event) {
|
|
389
|
+
const expression = this.inputValue(event).trim();
|
|
390
|
+
if (expression === '') {
|
|
391
|
+
// clearing the expression keeps a simple condition, if one existed
|
|
392
|
+
this.emitPatch({ conditional: this.simpleCondition() ?? undefined });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.emitPatch({ conditional: { show: true, expression } });
|
|
396
|
+
}
|
|
397
|
+
// -- advanced (calculate / mask) ------------------------------------------
|
|
398
|
+
patchCalculate(event) {
|
|
399
|
+
const expression = this.inputValue(event).trim();
|
|
400
|
+
this.emitPatch({ calculate: expression === '' ? undefined : expression });
|
|
401
|
+
}
|
|
402
|
+
patchMask(event) {
|
|
403
|
+
const mask = this.inputValue(event).trim();
|
|
404
|
+
this.emitPatch({ format: mask === '' ? undefined : { mask } });
|
|
405
|
+
}
|
|
406
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
407
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InspectorComponent, isStandalone: true, selector: "mform-inspector", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { nodePatch: "nodePatch" }, ngImport: i0, template: `
|
|
408
|
+
@let n = node();
|
|
409
|
+
<div class="dz-inspector">
|
|
410
|
+
<h3>{{ n.type }} <code>{{ n.key }}</code></h3>
|
|
411
|
+
|
|
412
|
+
<label class="dz-prop">
|
|
413
|
+
<span>Key</span>
|
|
414
|
+
<input type="text" [value]="n.key" (change)="patchText('key', $event)" />
|
|
415
|
+
</label>
|
|
416
|
+
<label class="dz-prop">
|
|
417
|
+
<span>Label</span>
|
|
418
|
+
<input type="text" [value]="n.label ?? ''" (change)="patchText('label', $event)" />
|
|
419
|
+
</label>
|
|
420
|
+
<label class="dz-prop">
|
|
421
|
+
<span>Description</span>
|
|
422
|
+
<input type="text" [value]="n.description ?? ''" (change)="patchText('description', $event)" />
|
|
423
|
+
</label>
|
|
424
|
+
|
|
425
|
+
@if (n.kind === 'input') {
|
|
426
|
+
<label class="dz-prop">
|
|
427
|
+
<span>Placeholder</span>
|
|
428
|
+
<input type="text" [value]="field().placeholder ?? ''" (change)="patchText('placeholder', $event)" />
|
|
429
|
+
</label>
|
|
430
|
+
<label class="dz-prop">
|
|
431
|
+
<span>Default value</span>
|
|
432
|
+
<input type="text" [value]="defaultValueText()" (change)="patchDefault($event)" />
|
|
433
|
+
</label>
|
|
434
|
+
<label class="dz-check">
|
|
435
|
+
<input type="checkbox" [checked]="isRequired()" (change)="toggleRequired($event)" />
|
|
436
|
+
<span>Required</span>
|
|
437
|
+
</label>
|
|
438
|
+
|
|
439
|
+
<details class="dz-section" open>
|
|
440
|
+
<summary>Validation</summary>
|
|
441
|
+
<label class="dz-prop"><span>Min length</span>
|
|
442
|
+
<input type="number" [value]="validatorValue('minLength')" (change)="patchValidator('minLength', $event)" /></label>
|
|
443
|
+
<label class="dz-prop"><span>Max length</span>
|
|
444
|
+
<input type="number" [value]="validatorValue('maxLength')" (change)="patchValidator('maxLength', $event)" /></label>
|
|
445
|
+
<label class="dz-prop"><span>Min</span>
|
|
446
|
+
<input type="number" [value]="validatorValue('min')" (change)="patchValidator('min', $event)" /></label>
|
|
447
|
+
<label class="dz-prop"><span>Max</span>
|
|
448
|
+
<input type="number" [value]="validatorValue('max')" (change)="patchValidator('max', $event)" /></label>
|
|
449
|
+
<label class="dz-prop"><span>Pattern (regex)</span>
|
|
450
|
+
<input type="text" [value]="patternValue()" (change)="patchPattern($event)" /></label>
|
|
451
|
+
</details>
|
|
452
|
+
|
|
453
|
+
@if (hasOptions()) {
|
|
454
|
+
<details class="dz-section" open>
|
|
455
|
+
<summary>Options</summary>
|
|
456
|
+
@for (option of field().options ?? []; track $index; let i = $index) {
|
|
457
|
+
<div class="dz-option-row">
|
|
458
|
+
<input type="text" placeholder="Label" [value]="option.label"
|
|
459
|
+
(change)="patchOption(i, 'label', $event)" />
|
|
460
|
+
<input type="text" placeholder="Value" [value]="option.value"
|
|
461
|
+
(change)="patchOption(i, 'value', $event)" />
|
|
462
|
+
<button type="button" (click)="removeOption(i)" aria-label="Remove option">✕</button>
|
|
463
|
+
</div>
|
|
464
|
+
}
|
|
465
|
+
<button type="button" class="dz-add" (click)="addOption()">+ Option</button>
|
|
466
|
+
</details>
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@if (n.kind === 'static' && n.type === 'content') {
|
|
471
|
+
<label class="dz-prop">
|
|
472
|
+
<span>Content (HTML)</span>
|
|
473
|
+
<textarea rows="5" [value]="static_().content ?? ''" (change)="patchText('content', $event)"></textarea>
|
|
474
|
+
</label>
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@if (n.kind === 'container' && n.type === 'columns') {
|
|
478
|
+
<label class="dz-prop">
|
|
479
|
+
<span>Column widths (of 12, comma-separated)</span>
|
|
480
|
+
<input type="text" [value]="columnWidths()" (change)="patchColumnWidths($event)" />
|
|
481
|
+
</label>
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
<details class="dz-section">
|
|
485
|
+
<summary>Visibility</summary>
|
|
486
|
+
<label class="dz-check">
|
|
487
|
+
<input type="checkbox" [checked]="n.hidden === true" (change)="patchFlag('hidden', $event)" />
|
|
488
|
+
<span>Hidden</span>
|
|
489
|
+
</label>
|
|
490
|
+
<label class="dz-check">
|
|
491
|
+
<input type="checkbox" [checked]="n.disabled === true" (change)="patchFlag('disabled', $event)" />
|
|
492
|
+
<span>Disabled</span>
|
|
493
|
+
</label>
|
|
494
|
+
<label class="dz-prop">
|
|
495
|
+
<span>Show when field…</span>
|
|
496
|
+
<select [value]="simpleCondition()?.field ?? ''" (change)="patchConditionField($event)">
|
|
497
|
+
<option value="">(always visible)</option>
|
|
498
|
+
@for (path of otherInputPaths(); track path) {
|
|
499
|
+
<option [value]="path" [selected]="simpleCondition()?.field === path">{{ path }}</option>
|
|
500
|
+
}
|
|
501
|
+
</select>
|
|
502
|
+
</label>
|
|
503
|
+
@if (simpleCondition()) {
|
|
504
|
+
<label class="dz-prop">
|
|
505
|
+
<span>…equals value</span>
|
|
506
|
+
<input type="text" [value]="conditionValueText()" (change)="patchConditionValue($event)" />
|
|
507
|
+
</label>
|
|
508
|
+
}
|
|
509
|
+
<label class="dz-prop">
|
|
510
|
+
<span>Show when expression (advanced)</span>
|
|
511
|
+
<input
|
|
512
|
+
type="text"
|
|
513
|
+
placeholder="ex.: total > 1000 && status == 'ok'"
|
|
514
|
+
[value]="conditionExpression()"
|
|
515
|
+
(change)="patchConditionExpression($event)"
|
|
516
|
+
/>
|
|
517
|
+
</label>
|
|
518
|
+
</details>
|
|
519
|
+
|
|
520
|
+
@if (n.kind === 'input') {
|
|
521
|
+
<details class="dz-section">
|
|
522
|
+
<summary>Advanced</summary>
|
|
523
|
+
<label class="dz-prop">
|
|
524
|
+
<span>Calculated value (expression)</span>
|
|
525
|
+
<input
|
|
526
|
+
type="text"
|
|
527
|
+
placeholder="ex.: qty * price"
|
|
528
|
+
[value]="field().calculate ?? ''"
|
|
529
|
+
(change)="patchCalculate($event)"
|
|
530
|
+
/>
|
|
531
|
+
</label>
|
|
532
|
+
<label class="dz-prop">
|
|
533
|
+
<span>Input mask (0=digit A=letter S=any)</span>
|
|
534
|
+
<input
|
|
535
|
+
type="text"
|
|
536
|
+
placeholder="ex.: 000.000.000-00"
|
|
537
|
+
[value]="field().format?.mask ?? ''"
|
|
538
|
+
(change)="patchMask($event)"
|
|
539
|
+
/>
|
|
540
|
+
</label>
|
|
541
|
+
</details>
|
|
542
|
+
}
|
|
543
|
+
</div>
|
|
544
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
545
|
+
}
|
|
546
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InspectorComponent, decorators: [{
|
|
547
|
+
type: Component,
|
|
548
|
+
args: [{
|
|
549
|
+
selector: 'mform-inspector',
|
|
550
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
551
|
+
encapsulation: ViewEncapsulation.None,
|
|
552
|
+
template: `
|
|
553
|
+
@let n = node();
|
|
554
|
+
<div class="dz-inspector">
|
|
555
|
+
<h3>{{ n.type }} <code>{{ n.key }}</code></h3>
|
|
556
|
+
|
|
557
|
+
<label class="dz-prop">
|
|
558
|
+
<span>Key</span>
|
|
559
|
+
<input type="text" [value]="n.key" (change)="patchText('key', $event)" />
|
|
560
|
+
</label>
|
|
561
|
+
<label class="dz-prop">
|
|
562
|
+
<span>Label</span>
|
|
563
|
+
<input type="text" [value]="n.label ?? ''" (change)="patchText('label', $event)" />
|
|
564
|
+
</label>
|
|
565
|
+
<label class="dz-prop">
|
|
566
|
+
<span>Description</span>
|
|
567
|
+
<input type="text" [value]="n.description ?? ''" (change)="patchText('description', $event)" />
|
|
568
|
+
</label>
|
|
569
|
+
|
|
570
|
+
@if (n.kind === 'input') {
|
|
571
|
+
<label class="dz-prop">
|
|
572
|
+
<span>Placeholder</span>
|
|
573
|
+
<input type="text" [value]="field().placeholder ?? ''" (change)="patchText('placeholder', $event)" />
|
|
574
|
+
</label>
|
|
575
|
+
<label class="dz-prop">
|
|
576
|
+
<span>Default value</span>
|
|
577
|
+
<input type="text" [value]="defaultValueText()" (change)="patchDefault($event)" />
|
|
578
|
+
</label>
|
|
579
|
+
<label class="dz-check">
|
|
580
|
+
<input type="checkbox" [checked]="isRequired()" (change)="toggleRequired($event)" />
|
|
581
|
+
<span>Required</span>
|
|
582
|
+
</label>
|
|
583
|
+
|
|
584
|
+
<details class="dz-section" open>
|
|
585
|
+
<summary>Validation</summary>
|
|
586
|
+
<label class="dz-prop"><span>Min length</span>
|
|
587
|
+
<input type="number" [value]="validatorValue('minLength')" (change)="patchValidator('minLength', $event)" /></label>
|
|
588
|
+
<label class="dz-prop"><span>Max length</span>
|
|
589
|
+
<input type="number" [value]="validatorValue('maxLength')" (change)="patchValidator('maxLength', $event)" /></label>
|
|
590
|
+
<label class="dz-prop"><span>Min</span>
|
|
591
|
+
<input type="number" [value]="validatorValue('min')" (change)="patchValidator('min', $event)" /></label>
|
|
592
|
+
<label class="dz-prop"><span>Max</span>
|
|
593
|
+
<input type="number" [value]="validatorValue('max')" (change)="patchValidator('max', $event)" /></label>
|
|
594
|
+
<label class="dz-prop"><span>Pattern (regex)</span>
|
|
595
|
+
<input type="text" [value]="patternValue()" (change)="patchPattern($event)" /></label>
|
|
596
|
+
</details>
|
|
597
|
+
|
|
598
|
+
@if (hasOptions()) {
|
|
599
|
+
<details class="dz-section" open>
|
|
600
|
+
<summary>Options</summary>
|
|
601
|
+
@for (option of field().options ?? []; track $index; let i = $index) {
|
|
602
|
+
<div class="dz-option-row">
|
|
603
|
+
<input type="text" placeholder="Label" [value]="option.label"
|
|
604
|
+
(change)="patchOption(i, 'label', $event)" />
|
|
605
|
+
<input type="text" placeholder="Value" [value]="option.value"
|
|
606
|
+
(change)="patchOption(i, 'value', $event)" />
|
|
607
|
+
<button type="button" (click)="removeOption(i)" aria-label="Remove option">✕</button>
|
|
608
|
+
</div>
|
|
609
|
+
}
|
|
610
|
+
<button type="button" class="dz-add" (click)="addOption()">+ Option</button>
|
|
611
|
+
</details>
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
@if (n.kind === 'static' && n.type === 'content') {
|
|
616
|
+
<label class="dz-prop">
|
|
617
|
+
<span>Content (HTML)</span>
|
|
618
|
+
<textarea rows="5" [value]="static_().content ?? ''" (change)="patchText('content', $event)"></textarea>
|
|
619
|
+
</label>
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@if (n.kind === 'container' && n.type === 'columns') {
|
|
623
|
+
<label class="dz-prop">
|
|
624
|
+
<span>Column widths (of 12, comma-separated)</span>
|
|
625
|
+
<input type="text" [value]="columnWidths()" (change)="patchColumnWidths($event)" />
|
|
626
|
+
</label>
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
<details class="dz-section">
|
|
630
|
+
<summary>Visibility</summary>
|
|
631
|
+
<label class="dz-check">
|
|
632
|
+
<input type="checkbox" [checked]="n.hidden === true" (change)="patchFlag('hidden', $event)" />
|
|
633
|
+
<span>Hidden</span>
|
|
634
|
+
</label>
|
|
635
|
+
<label class="dz-check">
|
|
636
|
+
<input type="checkbox" [checked]="n.disabled === true" (change)="patchFlag('disabled', $event)" />
|
|
637
|
+
<span>Disabled</span>
|
|
638
|
+
</label>
|
|
639
|
+
<label class="dz-prop">
|
|
640
|
+
<span>Show when field…</span>
|
|
641
|
+
<select [value]="simpleCondition()?.field ?? ''" (change)="patchConditionField($event)">
|
|
642
|
+
<option value="">(always visible)</option>
|
|
643
|
+
@for (path of otherInputPaths(); track path) {
|
|
644
|
+
<option [value]="path" [selected]="simpleCondition()?.field === path">{{ path }}</option>
|
|
645
|
+
}
|
|
646
|
+
</select>
|
|
647
|
+
</label>
|
|
648
|
+
@if (simpleCondition()) {
|
|
649
|
+
<label class="dz-prop">
|
|
650
|
+
<span>…equals value</span>
|
|
651
|
+
<input type="text" [value]="conditionValueText()" (change)="patchConditionValue($event)" />
|
|
652
|
+
</label>
|
|
653
|
+
}
|
|
654
|
+
<label class="dz-prop">
|
|
655
|
+
<span>Show when expression (advanced)</span>
|
|
656
|
+
<input
|
|
657
|
+
type="text"
|
|
658
|
+
placeholder="ex.: total > 1000 && status == 'ok'"
|
|
659
|
+
[value]="conditionExpression()"
|
|
660
|
+
(change)="patchConditionExpression($event)"
|
|
661
|
+
/>
|
|
662
|
+
</label>
|
|
663
|
+
</details>
|
|
664
|
+
|
|
665
|
+
@if (n.kind === 'input') {
|
|
666
|
+
<details class="dz-section">
|
|
667
|
+
<summary>Advanced</summary>
|
|
668
|
+
<label class="dz-prop">
|
|
669
|
+
<span>Calculated value (expression)</span>
|
|
670
|
+
<input
|
|
671
|
+
type="text"
|
|
672
|
+
placeholder="ex.: qty * price"
|
|
673
|
+
[value]="field().calculate ?? ''"
|
|
674
|
+
(change)="patchCalculate($event)"
|
|
675
|
+
/>
|
|
676
|
+
</label>
|
|
677
|
+
<label class="dz-prop">
|
|
678
|
+
<span>Input mask (0=digit A=letter S=any)</span>
|
|
679
|
+
<input
|
|
680
|
+
type="text"
|
|
681
|
+
placeholder="ex.: 000.000.000-00"
|
|
682
|
+
[value]="field().format?.mask ?? ''"
|
|
683
|
+
(change)="patchMask($event)"
|
|
684
|
+
/>
|
|
685
|
+
</label>
|
|
686
|
+
</details>
|
|
687
|
+
}
|
|
688
|
+
</div>
|
|
689
|
+
`,
|
|
690
|
+
}]
|
|
691
|
+
}], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], nodePatch: [{ type: i0.Output, args: ["nodePatch"] }] } });
|
|
692
|
+
|
|
693
|
+
function input(type, dataType, label, extra) {
|
|
694
|
+
return (schema) => ({
|
|
695
|
+
kind: 'input',
|
|
696
|
+
key: generateKey(schema, type),
|
|
697
|
+
type,
|
|
698
|
+
label,
|
|
699
|
+
dataType,
|
|
700
|
+
...extra,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
const DEFAULT_CATALOG = [
|
|
704
|
+
{
|
|
705
|
+
label: 'Basic',
|
|
706
|
+
entries: [
|
|
707
|
+
{ type: 'text', label: 'Text', icon: 'Aa', create: input('text', 'string', 'Text') },
|
|
708
|
+
{ type: 'textarea', label: 'Text area', icon: '¶', create: input('textarea', 'string', 'Text area') },
|
|
709
|
+
{ type: 'number', label: 'Number', icon: '#', create: input('number', 'number', 'Number') },
|
|
710
|
+
{ type: 'currency', label: 'Currency', icon: '$', create: input('currency', 'number', 'Currency') },
|
|
711
|
+
{ type: 'checkbox', label: 'Checkbox', icon: '☑', create: input('checkbox', 'boolean', 'Checkbox') },
|
|
712
|
+
{ type: 'email', label: 'Email', icon: '@', create: input('email', 'string', 'Email', { validators: [{ type: 'email' }] }) },
|
|
713
|
+
{ type: 'phone', label: 'Phone', icon: '☎', create: input('phone', 'string', 'Phone') },
|
|
714
|
+
{ type: 'password', label: 'Password', icon: '••', create: input('password', 'string', 'Password') },
|
|
715
|
+
],
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
label: 'Choices',
|
|
719
|
+
entries: [
|
|
720
|
+
{
|
|
721
|
+
type: 'select', label: 'Select', icon: '▾',
|
|
722
|
+
create: (s) => ({ ...input('select', 'string', 'Select')(s), options: [
|
|
723
|
+
{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' },
|
|
724
|
+
] }),
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
type: 'radio', label: 'Radio', icon: '◉',
|
|
728
|
+
create: (s) => ({ ...input('radio', 'string', 'Radio')(s), options: [
|
|
729
|
+
{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' },
|
|
730
|
+
] }),
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
type: 'checkbox-group', label: 'Checkbox group', icon: '☷',
|
|
734
|
+
create: (s) => ({ ...input('checkbox-group', 'object', 'Checkbox group')(s), options: [
|
|
735
|
+
{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' },
|
|
736
|
+
] }),
|
|
737
|
+
},
|
|
738
|
+
{ type: 'tags', label: 'Tags', icon: '⌗', create: input('tags', 'array', 'Tags') },
|
|
739
|
+
{
|
|
740
|
+
type: 'reference', label: 'Reference', icon: '⌕',
|
|
741
|
+
create: (s) => ({
|
|
742
|
+
...input('reference', 'string', 'Reference')(s),
|
|
743
|
+
optionsSource: { type: 'provider', name: 'reference' },
|
|
744
|
+
}),
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
label: 'Date & files',
|
|
750
|
+
entries: [
|
|
751
|
+
{ type: 'datetime', label: 'Date & time', icon: '📅', create: input('datetime', 'date', 'Date & time') },
|
|
752
|
+
{ type: 'day', label: 'Date', icon: '▦', create: input('day', 'string', 'Date') },
|
|
753
|
+
{ type: 'time', label: 'Time', icon: '◷', create: input('time', 'string', 'Time') },
|
|
754
|
+
{ type: 'file', label: 'File', icon: '📎', create: input('file', 'array', 'File') },
|
|
755
|
+
],
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
label: 'Layout',
|
|
759
|
+
entries: [
|
|
760
|
+
{
|
|
761
|
+
type: 'panel', label: 'Panel', icon: '▭',
|
|
762
|
+
create: (s) => ({ kind: 'container', key: generateKey(s, 'panel'), type: 'panel',
|
|
763
|
+
dataScope: 'inherit', label: 'Panel', children: [] }),
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
type: 'columns', label: 'Columns', icon: '▥',
|
|
767
|
+
create: (s) => ({ kind: 'container', key: generateKey(s, 'columns'), type: 'columns',
|
|
768
|
+
dataScope: 'inherit', children: [], columns: [
|
|
769
|
+
{ width: 6, children: [] }, { width: 6, children: [] },
|
|
770
|
+
] }),
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
type: 'tabs', label: 'Tabs', icon: '⧉',
|
|
774
|
+
create: (s) => {
|
|
775
|
+
const key = generateKey(s, 'tabs');
|
|
776
|
+
return { kind: 'container', key, type: 'tabs', dataScope: 'inherit', children: [
|
|
777
|
+
{ kind: 'container', key: `${key}-tab-1`, type: 'panel', dataScope: 'inherit', label: 'Tab 1', children: [] },
|
|
778
|
+
{ kind: 'container', key: `${key}-tab-2`, type: 'panel', dataScope: 'inherit', label: 'Tab 2', children: [] },
|
|
779
|
+
] };
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: 'group', label: 'Data group', icon: '⧈',
|
|
784
|
+
create: (s) => ({ kind: 'container', key: generateKey(s, 'group'), type: 'group',
|
|
785
|
+
dataScope: 'nested', label: 'Group', children: [] }),
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
type: 'datagrid', label: 'Data grid', icon: '☰',
|
|
789
|
+
create: (s) => ({ kind: 'container', key: generateKey(s, 'datagrid'), type: 'datagrid',
|
|
790
|
+
dataScope: 'array', label: 'Data grid', children: [] }),
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
label: 'Static',
|
|
796
|
+
entries: [
|
|
797
|
+
{
|
|
798
|
+
type: 'content', label: 'Content', icon: '¶¶',
|
|
799
|
+
create: (s) => ({ kind: 'static', key: generateKey(s, 'content'), type: 'content',
|
|
800
|
+
content: '<p>Content</p>' }),
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
type: 'button', label: 'Submit button', icon: '⏎',
|
|
804
|
+
create: (s) => ({ kind: 'static', key: generateKey(s, 'button'), type: 'button',
|
|
805
|
+
label: 'Submit', props: { action: 'submit' } }),
|
|
806
|
+
},
|
|
807
|
+
],
|
|
808
|
+
},
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
const EMPTY_SCHEMA = { version: 1, fields: [] };
|
|
812
|
+
const HISTORY_LIMIT = 50;
|
|
813
|
+
/**
|
|
814
|
+
* `<mform-designer>` — visual authoring of form schemas.
|
|
815
|
+
*
|
|
816
|
+
* The designer edits a DOCUMENT (the schema): every change goes through the
|
|
817
|
+
* core's immutable edit operations, feeding undo/redo and `schemaChanged`.
|
|
818
|
+
* Persistence, publishing and permissions belong to the host.
|
|
819
|
+
*/
|
|
820
|
+
class FormDesignerComponent {
|
|
821
|
+
/** Initial native schema to edit. */
|
|
822
|
+
schema = input$1(null, ...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
|
|
823
|
+
/** Initial foreign definition (converted through importers). */
|
|
824
|
+
source = input$1(null, ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
|
|
825
|
+
/** Extra palette groups contributed by the host. */
|
|
826
|
+
extraCatalog = input$1([], ...(ngDevMode ? [{ debugName: "extraCatalog" }] : /* istanbul ignore next */ []));
|
|
827
|
+
/** Emitted after every committed edit (undo/redo included). */
|
|
828
|
+
schemaChanged = output();
|
|
829
|
+
draft = signal(EMPTY_SCHEMA, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
830
|
+
past = signal([], ...(ngDevMode ? [{ debugName: "past" }] : /* istanbul ignore next */ []));
|
|
831
|
+
future = signal([], ...(ngDevMode ? [{ debugName: "future" }] : /* istanbul ignore next */ []));
|
|
832
|
+
selected = signal(null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
833
|
+
preview = signal(false, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
834
|
+
previewData = signal({}, ...(ngDevMode ? [{ debugName: "previewData" }] : /* istanbul ignore next */ []));
|
|
835
|
+
dialog = signal(null, ...(ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []));
|
|
836
|
+
importError = signal('', ...(ngDevMode ? [{ debugName: "importError" }] : /* istanbul ignore next */ []));
|
|
837
|
+
showDiagnostics = signal(false, ...(ngDevMode ? [{ debugName: "showDiagnostics" }] : /* istanbul ignore next */ []));
|
|
838
|
+
catalog = computed(() => [...DEFAULT_CATALOG, ...this.extraCatalog()], ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
|
|
839
|
+
diagnostics = computed(() => validateSchema(this.draft()), ...(ngDevMode ? [{ debugName: "diagnostics" }] : /* istanbul ignore next */ []));
|
|
840
|
+
exportedJson = computed(() => JSON.stringify(this.draft(), null, 2), ...(ngDevMode ? [{ debugName: "exportedJson" }] : /* istanbul ignore next */ []));
|
|
841
|
+
/** Disposable engine driving the canvas leaves (defaults visible). */
|
|
842
|
+
canvasEngine = computed(() => createFormEngine(this.draft()), ...(ngDevMode ? [{ debugName: "canvasEngine" }] : /* istanbul ignore next */ []));
|
|
843
|
+
selectedNode = computed(() => {
|
|
844
|
+
const path = this.selected();
|
|
845
|
+
return path ? (getNodeAt(this.draft(), path) ?? null) : null;
|
|
846
|
+
}, ...(ngDevMode ? [{ debugName: "selectedNode" }] : /* istanbul ignore next */ []));
|
|
847
|
+
/** Flat structure tree for navigation. */
|
|
848
|
+
tree = computed(() => {
|
|
849
|
+
const items = [];
|
|
850
|
+
const visit = (nodes, depth, prefix) => {
|
|
851
|
+
nodes.forEach((node, i) => {
|
|
852
|
+
const path = [...prefix, i];
|
|
853
|
+
items.push({
|
|
854
|
+
id: path.join('.'),
|
|
855
|
+
label: `${node.label ?? node.key} · ${node.type}`,
|
|
856
|
+
depth,
|
|
857
|
+
path,
|
|
858
|
+
});
|
|
859
|
+
if (node.kind === 'container')
|
|
860
|
+
visit(node.children, depth + 1, path);
|
|
861
|
+
});
|
|
862
|
+
};
|
|
863
|
+
visit(this.draft().fields, 0, []);
|
|
864
|
+
return items;
|
|
865
|
+
}, ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
|
|
866
|
+
constructor() {
|
|
867
|
+
// (Re)load the document whenever the host hands a new definition in.
|
|
868
|
+
effect(() => {
|
|
869
|
+
const native = this.schema();
|
|
870
|
+
const source = this.source();
|
|
871
|
+
// Hosts often feed `schemaChanged` back into [schema] (controlled
|
|
872
|
+
// usage) — the echo of our own draft must not reset history/selection.
|
|
873
|
+
if (native && native === this.draft())
|
|
874
|
+
return;
|
|
875
|
+
let initial = EMPTY_SCHEMA;
|
|
876
|
+
if (native) {
|
|
877
|
+
initial = native;
|
|
878
|
+
}
|
|
879
|
+
else if (source !== null && source !== undefined) {
|
|
880
|
+
try {
|
|
881
|
+
initial = importForm(source, [createLegacyImporter()]).schema;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
initial = EMPTY_SCHEMA;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
this.draft.set(initial);
|
|
888
|
+
this.past.set([]);
|
|
889
|
+
this.future.set([]);
|
|
890
|
+
this.selected.set(null);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
// -- document mutation ----------------------------------------------------
|
|
894
|
+
commit(next) {
|
|
895
|
+
this.past.update((stack) => [...stack.slice(-(HISTORY_LIMIT - 1)), this.draft()]);
|
|
896
|
+
this.future.set([]);
|
|
897
|
+
this.draft.set(next);
|
|
898
|
+
this.schemaChanged.emit(next);
|
|
899
|
+
}
|
|
900
|
+
undo() {
|
|
901
|
+
const past = this.past();
|
|
902
|
+
if (past.length === 0)
|
|
903
|
+
return;
|
|
904
|
+
this.future.update((stack) => [this.draft(), ...stack]);
|
|
905
|
+
this.past.set(past.slice(0, -1));
|
|
906
|
+
this.draft.set(past[past.length - 1]);
|
|
907
|
+
this.selected.set(null);
|
|
908
|
+
this.schemaChanged.emit(this.draft());
|
|
909
|
+
}
|
|
910
|
+
redo() {
|
|
911
|
+
const future = this.future();
|
|
912
|
+
if (future.length === 0)
|
|
913
|
+
return;
|
|
914
|
+
this.past.update((stack) => [...stack, this.draft()]);
|
|
915
|
+
this.future.set(future.slice(1));
|
|
916
|
+
this.draft.set(future[0]);
|
|
917
|
+
this.selected.set(null);
|
|
918
|
+
this.schemaChanged.emit(this.draft());
|
|
919
|
+
}
|
|
920
|
+
add(entry) {
|
|
921
|
+
const node = entry.create(this.draft());
|
|
922
|
+
const target = this.insertionPath();
|
|
923
|
+
this.commit(insertNode(this.draft(), target, node));
|
|
924
|
+
this.select(target);
|
|
925
|
+
}
|
|
926
|
+
/** New nodes land inside a selected container, after a selected leaf, or at the end. */
|
|
927
|
+
insertionPath() {
|
|
928
|
+
const selected = this.selected();
|
|
929
|
+
if (!selected)
|
|
930
|
+
return [this.draft().fields.length];
|
|
931
|
+
const node = getNodeAt(this.draft(), selected);
|
|
932
|
+
if (node?.kind === 'container' && node.type !== 'columns') {
|
|
933
|
+
return [...selected, node.children.length];
|
|
934
|
+
}
|
|
935
|
+
const { parent, index } = parentOf(selected);
|
|
936
|
+
return [...parent, index + 1];
|
|
937
|
+
}
|
|
938
|
+
removeSelected() {
|
|
939
|
+
const selected = this.selected();
|
|
940
|
+
if (!selected)
|
|
941
|
+
return;
|
|
942
|
+
this.commit(removeNode(this.draft(), selected));
|
|
943
|
+
this.selected.set(null);
|
|
944
|
+
}
|
|
945
|
+
duplicateSelected() {
|
|
946
|
+
const selected = this.selected();
|
|
947
|
+
if (!selected)
|
|
948
|
+
return;
|
|
949
|
+
this.commit(duplicateNode(this.draft(), selected));
|
|
950
|
+
}
|
|
951
|
+
moveSelected(delta) {
|
|
952
|
+
const selected = this.selected();
|
|
953
|
+
if (!selected)
|
|
954
|
+
return;
|
|
955
|
+
const { parent, index } = parentOf(selected);
|
|
956
|
+
const siblings = parent.length === 0
|
|
957
|
+
? this.draft().fields
|
|
958
|
+
: getNodeAt(this.draft(), parent)?.children ?? [];
|
|
959
|
+
const target = index + delta;
|
|
960
|
+
if (target < 0 || target >= siblings.length)
|
|
961
|
+
return;
|
|
962
|
+
this.commit(moveNode(this.draft(), selected, [...parent, target]));
|
|
963
|
+
this.select([...parent, target]);
|
|
964
|
+
}
|
|
965
|
+
applyPatch(event) {
|
|
966
|
+
const selected = this.selected();
|
|
967
|
+
if (!selected)
|
|
968
|
+
return;
|
|
969
|
+
this.commit(updateNode(this.draft(), selected, event.patch));
|
|
970
|
+
}
|
|
971
|
+
// -- selection ------------------------------------------------------------
|
|
972
|
+
select(path) {
|
|
973
|
+
this.selected.set(path);
|
|
974
|
+
}
|
|
975
|
+
isSelectedPath(path) {
|
|
976
|
+
return this.selected()?.join('.') === path.join('.');
|
|
977
|
+
}
|
|
978
|
+
// -- import / export ------------------------------------------------------
|
|
979
|
+
openExport() {
|
|
980
|
+
this.dialog.set(this.dialog() === 'export' ? null : 'export');
|
|
981
|
+
}
|
|
982
|
+
doImport(raw) {
|
|
983
|
+
try {
|
|
984
|
+
const parsed = JSON.parse(raw);
|
|
985
|
+
const { schema } = importForm(parsed, [createLegacyImporter()]);
|
|
986
|
+
this.commit(schema);
|
|
987
|
+
this.selected.set(null);
|
|
988
|
+
this.dialog.set(null);
|
|
989
|
+
this.importError.set('');
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
this.importError.set('Could not import: ' + String(error));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// -- keyboard -------------------------------------------------------------
|
|
996
|
+
onKeydown(event) {
|
|
997
|
+
const target = event.target;
|
|
998
|
+
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName))
|
|
999
|
+
return;
|
|
1000
|
+
const key = event.key.toLowerCase();
|
|
1001
|
+
if (event.ctrlKey && key === 'z' && !event.shiftKey) {
|
|
1002
|
+
event.preventDefault();
|
|
1003
|
+
this.undo();
|
|
1004
|
+
}
|
|
1005
|
+
else if ((event.ctrlKey && key === 'y') || (event.ctrlKey && event.shiftKey && key === 'z')) {
|
|
1006
|
+
event.preventDefault();
|
|
1007
|
+
this.redo();
|
|
1008
|
+
}
|
|
1009
|
+
else if (event.ctrlKey && key === 'd') {
|
|
1010
|
+
event.preventDefault();
|
|
1011
|
+
this.duplicateSelected();
|
|
1012
|
+
}
|
|
1013
|
+
else if (key === 'delete') {
|
|
1014
|
+
event.preventDefault();
|
|
1015
|
+
this.removeSelected();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormDesignerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1019
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormDesignerComponent, isStandalone: true, selector: "mform-designer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, extraCatalog: { classPropertyName: "extraCatalog", publicName: "extraCatalog", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { schemaChanged: "schemaChanged" }, ngImport: i0, template: `
|
|
1020
|
+
<div class="dz-shell" (keydown)="onKeydown($event)">
|
|
1021
|
+
<header class="dz-toolbar">
|
|
1022
|
+
<div class="dz-tools">
|
|
1023
|
+
<button type="button" (click)="undo()" [disabled]="past().length === 0" title="Undo (Ctrl+Z)">↶</button>
|
|
1024
|
+
<button type="button" (click)="redo()" [disabled]="future().length === 0" title="Redo (Ctrl+Y)">↷</button>
|
|
1025
|
+
<span class="dz-sep"></span>
|
|
1026
|
+
<button type="button" (click)="moveSelected(-1)" [disabled]="!selected()" title="Move up">↑</button>
|
|
1027
|
+
<button type="button" (click)="moveSelected(1)" [disabled]="!selected()" title="Move down">↓</button>
|
|
1028
|
+
<button type="button" (click)="duplicateSelected()" [disabled]="!selected()" title="Duplicate (Ctrl+D)">⧉</button>
|
|
1029
|
+
<button type="button" (click)="removeSelected()" [disabled]="!selected()" title="Delete (Del)">🗑</button>
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="dz-tools">
|
|
1032
|
+
@if (diagnostics().length > 0) {
|
|
1033
|
+
<button type="button" class="dz-diag-badge" (click)="showDiagnostics.set(!showDiagnostics())">
|
|
1034
|
+
⚠ {{ diagnostics().length }}
|
|
1035
|
+
</button>
|
|
1036
|
+
}
|
|
1037
|
+
<button type="button" (click)="dialog.set(dialog() === 'import' ? null : 'import')">Import</button>
|
|
1038
|
+
<button type="button" (click)="openExport()">Export</button>
|
|
1039
|
+
<button type="button" class="dz-primary" (click)="preview.set(!preview())">
|
|
1040
|
+
{{ preview() ? 'Design' : 'Preview' }}
|
|
1041
|
+
</button>
|
|
1042
|
+
</div>
|
|
1043
|
+
</header>
|
|
1044
|
+
|
|
1045
|
+
@if (showDiagnostics() && diagnostics().length > 0) {
|
|
1046
|
+
<div class="dz-diagnostics" role="alert">
|
|
1047
|
+
@for (diag of diagnostics(); track $index) {
|
|
1048
|
+
<div class="dz-diag" [class.is-warning]="diag.severity === 'warning'">
|
|
1049
|
+
<code>{{ diag.code }}</code> {{ diag.message }} <em>{{ diag.path }}</em>
|
|
1050
|
+
</div>
|
|
1051
|
+
}
|
|
1052
|
+
</div>
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
@if (dialog() === 'export') {
|
|
1056
|
+
<div class="dz-dialog">
|
|
1057
|
+
<h4>Schema JSON</h4>
|
|
1058
|
+
<textarea readonly rows="12">{{ exportedJson() }}</textarea>
|
|
1059
|
+
<div class="dz-dialog-actions">
|
|
1060
|
+
<button type="button" (click)="dialog.set(null)">Close</button>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
}
|
|
1064
|
+
@if (dialog() === 'import') {
|
|
1065
|
+
<div class="dz-dialog">
|
|
1066
|
+
<h4>Import definition (native schema or supported formats)</h4>
|
|
1067
|
+
<textarea rows="12" #importArea placeholder='{"version":1,"fields":[…]} or a legacy definition'></textarea>
|
|
1068
|
+
@if (importError()) {
|
|
1069
|
+
<p class="dz-import-error" role="alert">{{ importError() }}</p>
|
|
1070
|
+
}
|
|
1071
|
+
<div class="dz-dialog-actions">
|
|
1072
|
+
<button type="button" (click)="dialog.set(null)">Cancel</button>
|
|
1073
|
+
<button type="button" class="dz-primary" (click)="doImport(importArea.value)">Import</button>
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
@if (preview()) {
|
|
1079
|
+
<div class="dz-preview">
|
|
1080
|
+
<mform-renderer [schema]="draft()" (valueChanged)="previewData.set($event)" />
|
|
1081
|
+
<details class="dz-preview-data">
|
|
1082
|
+
<summary>Test data</summary>
|
|
1083
|
+
<pre>{{ previewData() | json }}</pre>
|
|
1084
|
+
</details>
|
|
1085
|
+
</div>
|
|
1086
|
+
} @else {
|
|
1087
|
+
<div class="dz-body">
|
|
1088
|
+
<aside class="dz-palette">
|
|
1089
|
+
<h4>Fields</h4>
|
|
1090
|
+
@for (group of catalog(); track group.label) {
|
|
1091
|
+
<div class="dz-group">
|
|
1092
|
+
<span class="dz-group-label">{{ group.label }}</span>
|
|
1093
|
+
<div class="dz-group-entries">
|
|
1094
|
+
@for (entry of group.entries; track entry.type) {
|
|
1095
|
+
<button type="button" class="dz-palette-item" (click)="add(entry)"
|
|
1096
|
+
[title]="'Add ' + entry.label">
|
|
1097
|
+
<span class="dz-icon">{{ entry.icon }}</span>{{ entry.label }}
|
|
1098
|
+
</button>
|
|
1099
|
+
}
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
}
|
|
1103
|
+
<h4>Structure</h4>
|
|
1104
|
+
<div class="dz-tree" role="tree">
|
|
1105
|
+
@for (item of tree(); track item.id) {
|
|
1106
|
+
<button
|
|
1107
|
+
type="button"
|
|
1108
|
+
role="treeitem"
|
|
1109
|
+
class="dz-tree-item"
|
|
1110
|
+
[class.is-selected]="isSelectedPath(item.path)"
|
|
1111
|
+
[style.padding-left.px]="8 + item.depth * 14"
|
|
1112
|
+
(click)="select(item.path)"
|
|
1113
|
+
>
|
|
1114
|
+
{{ item.label }}
|
|
1115
|
+
</button>
|
|
1116
|
+
} @empty {
|
|
1117
|
+
<span class="dz-empty">no fields yet</span>
|
|
1118
|
+
}
|
|
1119
|
+
</div>
|
|
1120
|
+
</aside>
|
|
1121
|
+
|
|
1122
|
+
<main class="dz-canvas" (click)="select(null)">
|
|
1123
|
+
@for (node of draft().fields; track node.key; let i = $index) {
|
|
1124
|
+
<mform-designer-node
|
|
1125
|
+
[node]="node"
|
|
1126
|
+
[path]="[i]"
|
|
1127
|
+
[engine]="canvasEngine()"
|
|
1128
|
+
[selected]="selected()"
|
|
1129
|
+
(selectNode)="select($event)"
|
|
1130
|
+
/>
|
|
1131
|
+
} @empty {
|
|
1132
|
+
<div class="dz-canvas-empty">
|
|
1133
|
+
Click a field in the palette to start building.
|
|
1134
|
+
</div>
|
|
1135
|
+
}
|
|
1136
|
+
</main>
|
|
1137
|
+
|
|
1138
|
+
<aside class="dz-props">
|
|
1139
|
+
@if (selectedNode(); as node) {
|
|
1140
|
+
<mform-inspector
|
|
1141
|
+
[node]="node"
|
|
1142
|
+
[schema]="draft()"
|
|
1143
|
+
(nodePatch)="applyPatch($event)"
|
|
1144
|
+
/>
|
|
1145
|
+
} @else {
|
|
1146
|
+
<div class="dz-empty dz-props-empty">Select a field to edit its properties.</div>
|
|
1147
|
+
}
|
|
1148
|
+
</aside>
|
|
1149
|
+
</div>
|
|
1150
|
+
}
|
|
1151
|
+
</div>
|
|
1152
|
+
`, isInline: true, styles: [".dz-shell{display:flex;flex-direction:column;height:100%;min-height:480px;font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e);background:var(--mform-bg, #f6f5fa);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.dz-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-tools{display:flex;align-items:center;gap:6px}.dz-toolbar button{font:inherit;font-size:13px;padding:6px 10px;border:1px solid transparent;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-toolbar button:hover:not(:disabled){background:var(--mform-bg, #f6f5fa)}.dz-toolbar button:disabled{opacity:.35;cursor:default}.dz-toolbar .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-sep{width:1px;height:18px;background:var(--mform-border, #d9d5e6)}.dz-diag-badge{color:#b45309;font-weight:600}.dz-diagnostics{max-height:140px;overflow-y:auto;padding:8px 14px;background:#fdf5ef;border-bottom:1px solid var(--mform-border, #d9d5e6);font-size:12px}.dz-diag{padding:2px 0;color:#9a3412}.dz-diag.is-warning{color:#92400e}.dz-diag code{font-weight:600;margin-right:6px}.dz-diag em{color:var(--mform-muted, #6b6580);margin-left:6px}.dz-dialog{padding:14px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-dialog h4{margin:0 0 8px;font-size:13px}.dz-dialog textarea{width:100%;font-family:ui-monospace,Consolas,monospace;font-size:12px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical}.dz-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.dz-dialog-actions button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:none;cursor:pointer}.dz-dialog-actions .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-color:transparent}.dz-import-error{color:var(--mform-danger, #c62842);font-size:12px}.dz-body{display:grid;grid-template-columns:240px 1fr 280px;flex:1;min-height:0}.dz-palette{border-right:1px solid var(--mform-border, #d9d5e6);background:var(--mform-surface, #fff);padding:12px;overflow-y:auto}.dz-palette h4{margin:4px 0 8px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580)}.dz-group{margin-bottom:10px}.dz-group-label{font-size:11px;color:var(--mform-muted, #6b6580)}.dz-group-entries{display:flex;flex-direction:column;gap:2px;margin-top:4px}.dz-palette-item{display:flex;align-items:center;gap:8px;font:inherit;font-size:13px;text-align:left;padding:6px 8px;border:none;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-palette-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-icon{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px;font-size:11px}.dz-tree{display:flex;flex-direction:column;gap:1px}.dz-tree-item{font:inherit;font-size:12px;text-align:left;padding:4px 8px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dz-tree-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-tree-item.is-selected{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-canvas{padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.dz-canvas-empty,.dz-empty{color:var(--mform-muted, #6b6580);font-size:13px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:18px;text-align:center}.dz-node{position:relative;border:1px solid transparent;border-radius:var(--mform-radius, 10px);padding:20px 10px 10px;cursor:pointer;background:var(--mform-surface, #fff)}.dz-node:hover{border-color:var(--mform-border, #d9d5e6)}.dz-node.is-selected{border-color:var(--mform-primary, #6d4bd0);box-shadow:0 0 0 1px var(--mform-primary, #6d4bd0)}.dz-chip{position:absolute;top:2px;left:10px;font-size:10px;color:var(--mform-muted, #6b6580);pointer-events:none}.dz-node.is-selected>.dz-chip{color:var(--mform-primary, #6d4bd0);font-weight:600}.dz-children{display:flex;flex-direction:column;gap:8px}.dz-columns{display:grid;gap:10px}.dz-column{display:flex;flex-direction:column;gap:8px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:8px;padding:8px;min-height:40px}.dz-tabbar{display:flex;gap:4px;margin-bottom:8px;border-bottom:2px solid var(--mform-border, #d9d5e6)}.dz-tab{font:inherit;font-size:12px;padding:5px 12px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.dz-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.dz-leaf{pointer-events:none}.dz-props{border-left:1px solid var(--mform-border, #d9d5e6);background:var(--mform-surface, #fff);overflow-y:auto}.dz-props-empty{margin:12px}.dz-inspector{padding:12px;display:flex;flex-direction:column;gap:10px}.dz-inspector h3{margin:0;font-size:13px}.dz-inspector h3 code{color:var(--mform-muted, #6b6580);font-weight:400}.dz-prop{display:flex;flex-direction:column;gap:4px;font-size:12px}.dz-prop span{color:var(--mform-muted, #6b6580)}.dz-prop input,.dz-prop select,.dz-prop textarea{font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:#fff;width:100%;box-sizing:border-box}.dz-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.dz-check input{accent-color:var(--mform-primary, #6d4bd0)}.dz-section{border-top:1px solid var(--mform-border, #d9d5e6);padding-top:8px}.dz-section summary{font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px}.dz-section .dz-prop{margin-bottom:8px}.dz-option-row{display:flex;gap:6px;margin-bottom:6px}.dz-option-row input{flex:1;min-width:0;font-size:12px;padding:6px 8px;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px}.dz-option-row button{border:none;background:none;cursor:pointer;color:var(--mform-muted, #6b6580)}.dz-add{font:inherit;font-size:12px;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);background:none;border-radius:8px;padding:6px 10px;cursor:pointer}.dz-preview{padding:20px;overflow-y:auto}.dz-preview-data{margin-top:16px;font-size:12px}.dz-preview-data pre{background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;overflow-x:auto}\n"], dependencies: [{ kind: "component", type: DesignerNodeComponent, selector: "mform-designer-node", inputs: ["node", "path", "engine", "selected"], outputs: ["selectNode"] }, { kind: "component", type: InspectorComponent, selector: "mform-inspector", inputs: ["node", "schema"], outputs: ["nodePatch"] }, { kind: "component", type: FormRendererComponent, selector: "mform-renderer", inputs: ["schema", "source", "initialData", "optionsProviders", "messages", "validators", "mode", "submitHandler", "labels"], outputs: ["submitted", "valueChanged"] }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1153
|
+
}
|
|
1154
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormDesignerComponent, decorators: [{
|
|
1155
|
+
type: Component,
|
|
1156
|
+
args: [{ selector: 'mform-designer', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [DesignerNodeComponent, InspectorComponent, FormRendererComponent, JsonPipe], template: `
|
|
1157
|
+
<div class="dz-shell" (keydown)="onKeydown($event)">
|
|
1158
|
+
<header class="dz-toolbar">
|
|
1159
|
+
<div class="dz-tools">
|
|
1160
|
+
<button type="button" (click)="undo()" [disabled]="past().length === 0" title="Undo (Ctrl+Z)">↶</button>
|
|
1161
|
+
<button type="button" (click)="redo()" [disabled]="future().length === 0" title="Redo (Ctrl+Y)">↷</button>
|
|
1162
|
+
<span class="dz-sep"></span>
|
|
1163
|
+
<button type="button" (click)="moveSelected(-1)" [disabled]="!selected()" title="Move up">↑</button>
|
|
1164
|
+
<button type="button" (click)="moveSelected(1)" [disabled]="!selected()" title="Move down">↓</button>
|
|
1165
|
+
<button type="button" (click)="duplicateSelected()" [disabled]="!selected()" title="Duplicate (Ctrl+D)">⧉</button>
|
|
1166
|
+
<button type="button" (click)="removeSelected()" [disabled]="!selected()" title="Delete (Del)">🗑</button>
|
|
1167
|
+
</div>
|
|
1168
|
+
<div class="dz-tools">
|
|
1169
|
+
@if (diagnostics().length > 0) {
|
|
1170
|
+
<button type="button" class="dz-diag-badge" (click)="showDiagnostics.set(!showDiagnostics())">
|
|
1171
|
+
⚠ {{ diagnostics().length }}
|
|
1172
|
+
</button>
|
|
1173
|
+
}
|
|
1174
|
+
<button type="button" (click)="dialog.set(dialog() === 'import' ? null : 'import')">Import</button>
|
|
1175
|
+
<button type="button" (click)="openExport()">Export</button>
|
|
1176
|
+
<button type="button" class="dz-primary" (click)="preview.set(!preview())">
|
|
1177
|
+
{{ preview() ? 'Design' : 'Preview' }}
|
|
1178
|
+
</button>
|
|
1179
|
+
</div>
|
|
1180
|
+
</header>
|
|
1181
|
+
|
|
1182
|
+
@if (showDiagnostics() && diagnostics().length > 0) {
|
|
1183
|
+
<div class="dz-diagnostics" role="alert">
|
|
1184
|
+
@for (diag of diagnostics(); track $index) {
|
|
1185
|
+
<div class="dz-diag" [class.is-warning]="diag.severity === 'warning'">
|
|
1186
|
+
<code>{{ diag.code }}</code> {{ diag.message }} <em>{{ diag.path }}</em>
|
|
1187
|
+
</div>
|
|
1188
|
+
}
|
|
1189
|
+
</div>
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
@if (dialog() === 'export') {
|
|
1193
|
+
<div class="dz-dialog">
|
|
1194
|
+
<h4>Schema JSON</h4>
|
|
1195
|
+
<textarea readonly rows="12">{{ exportedJson() }}</textarea>
|
|
1196
|
+
<div class="dz-dialog-actions">
|
|
1197
|
+
<button type="button" (click)="dialog.set(null)">Close</button>
|
|
1198
|
+
</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
}
|
|
1201
|
+
@if (dialog() === 'import') {
|
|
1202
|
+
<div class="dz-dialog">
|
|
1203
|
+
<h4>Import definition (native schema or supported formats)</h4>
|
|
1204
|
+
<textarea rows="12" #importArea placeholder='{"version":1,"fields":[…]} or a legacy definition'></textarea>
|
|
1205
|
+
@if (importError()) {
|
|
1206
|
+
<p class="dz-import-error" role="alert">{{ importError() }}</p>
|
|
1207
|
+
}
|
|
1208
|
+
<div class="dz-dialog-actions">
|
|
1209
|
+
<button type="button" (click)="dialog.set(null)">Cancel</button>
|
|
1210
|
+
<button type="button" class="dz-primary" (click)="doImport(importArea.value)">Import</button>
|
|
1211
|
+
</div>
|
|
1212
|
+
</div>
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
@if (preview()) {
|
|
1216
|
+
<div class="dz-preview">
|
|
1217
|
+
<mform-renderer [schema]="draft()" (valueChanged)="previewData.set($event)" />
|
|
1218
|
+
<details class="dz-preview-data">
|
|
1219
|
+
<summary>Test data</summary>
|
|
1220
|
+
<pre>{{ previewData() | json }}</pre>
|
|
1221
|
+
</details>
|
|
1222
|
+
</div>
|
|
1223
|
+
} @else {
|
|
1224
|
+
<div class="dz-body">
|
|
1225
|
+
<aside class="dz-palette">
|
|
1226
|
+
<h4>Fields</h4>
|
|
1227
|
+
@for (group of catalog(); track group.label) {
|
|
1228
|
+
<div class="dz-group">
|
|
1229
|
+
<span class="dz-group-label">{{ group.label }}</span>
|
|
1230
|
+
<div class="dz-group-entries">
|
|
1231
|
+
@for (entry of group.entries; track entry.type) {
|
|
1232
|
+
<button type="button" class="dz-palette-item" (click)="add(entry)"
|
|
1233
|
+
[title]="'Add ' + entry.label">
|
|
1234
|
+
<span class="dz-icon">{{ entry.icon }}</span>{{ entry.label }}
|
|
1235
|
+
</button>
|
|
1236
|
+
}
|
|
1237
|
+
</div>
|
|
1238
|
+
</div>
|
|
1239
|
+
}
|
|
1240
|
+
<h4>Structure</h4>
|
|
1241
|
+
<div class="dz-tree" role="tree">
|
|
1242
|
+
@for (item of tree(); track item.id) {
|
|
1243
|
+
<button
|
|
1244
|
+
type="button"
|
|
1245
|
+
role="treeitem"
|
|
1246
|
+
class="dz-tree-item"
|
|
1247
|
+
[class.is-selected]="isSelectedPath(item.path)"
|
|
1248
|
+
[style.padding-left.px]="8 + item.depth * 14"
|
|
1249
|
+
(click)="select(item.path)"
|
|
1250
|
+
>
|
|
1251
|
+
{{ item.label }}
|
|
1252
|
+
</button>
|
|
1253
|
+
} @empty {
|
|
1254
|
+
<span class="dz-empty">no fields yet</span>
|
|
1255
|
+
}
|
|
1256
|
+
</div>
|
|
1257
|
+
</aside>
|
|
1258
|
+
|
|
1259
|
+
<main class="dz-canvas" (click)="select(null)">
|
|
1260
|
+
@for (node of draft().fields; track node.key; let i = $index) {
|
|
1261
|
+
<mform-designer-node
|
|
1262
|
+
[node]="node"
|
|
1263
|
+
[path]="[i]"
|
|
1264
|
+
[engine]="canvasEngine()"
|
|
1265
|
+
[selected]="selected()"
|
|
1266
|
+
(selectNode)="select($event)"
|
|
1267
|
+
/>
|
|
1268
|
+
} @empty {
|
|
1269
|
+
<div class="dz-canvas-empty">
|
|
1270
|
+
Click a field in the palette to start building.
|
|
1271
|
+
</div>
|
|
1272
|
+
}
|
|
1273
|
+
</main>
|
|
1274
|
+
|
|
1275
|
+
<aside class="dz-props">
|
|
1276
|
+
@if (selectedNode(); as node) {
|
|
1277
|
+
<mform-inspector
|
|
1278
|
+
[node]="node"
|
|
1279
|
+
[schema]="draft()"
|
|
1280
|
+
(nodePatch)="applyPatch($event)"
|
|
1281
|
+
/>
|
|
1282
|
+
} @else {
|
|
1283
|
+
<div class="dz-empty dz-props-empty">Select a field to edit its properties.</div>
|
|
1284
|
+
}
|
|
1285
|
+
</aside>
|
|
1286
|
+
</div>
|
|
1287
|
+
}
|
|
1288
|
+
</div>
|
|
1289
|
+
`, styles: [".dz-shell{display:flex;flex-direction:column;height:100%;min-height:480px;font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e);background:var(--mform-bg, #f6f5fa);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.dz-toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-tools{display:flex;align-items:center;gap:6px}.dz-toolbar button{font:inherit;font-size:13px;padding:6px 10px;border:1px solid transparent;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-toolbar button:hover:not(:disabled){background:var(--mform-bg, #f6f5fa)}.dz-toolbar button:disabled{opacity:.35;cursor:default}.dz-toolbar .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-sep{width:1px;height:18px;background:var(--mform-border, #d9d5e6)}.dz-diag-badge{color:#b45309;font-weight:600}.dz-diagnostics{max-height:140px;overflow-y:auto;padding:8px 14px;background:#fdf5ef;border-bottom:1px solid var(--mform-border, #d9d5e6);font-size:12px}.dz-diag{padding:2px 0;color:#9a3412}.dz-diag.is-warning{color:#92400e}.dz-diag code{font-weight:600;margin-right:6px}.dz-diag em{color:var(--mform-muted, #6b6580);margin-left:6px}.dz-dialog{padding:14px;background:var(--mform-surface, #fff);border-bottom:1px solid var(--mform-border, #d9d5e6)}.dz-dialog h4{margin:0 0 8px;font-size:13px}.dz-dialog textarea{width:100%;font-family:ui-monospace,Consolas,monospace;font-size:12px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical}.dz-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.dz-dialog-actions button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:none;cursor:pointer}.dz-dialog-actions .dz-primary{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-color:transparent}.dz-import-error{color:var(--mform-danger, #c62842);font-size:12px}.dz-body{display:grid;grid-template-columns:240px 1fr 280px;flex:1;min-height:0}.dz-palette{border-right:1px solid var(--mform-border, #d9d5e6);background:var(--mform-surface, #fff);padding:12px;overflow-y:auto}.dz-palette h4{margin:4px 0 8px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--mform-muted, #6b6580)}.dz-group{margin-bottom:10px}.dz-group-label{font-size:11px;color:var(--mform-muted, #6b6580)}.dz-group-entries{display:flex;flex-direction:column;gap:2px;margin-top:4px}.dz-palette-item{display:flex;align-items:center;gap:8px;font:inherit;font-size:13px;text-align:left;padding:6px 8px;border:none;border-radius:8px;background:none;color:inherit;cursor:pointer}.dz-palette-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-icon{width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px;font-size:11px}.dz-tree{display:flex;flex-direction:column;gap:1px}.dz-tree-item{font:inherit;font-size:12px;text-align:left;padding:4px 8px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dz-tree-item:hover{background:var(--mform-bg, #f6f5fa)}.dz-tree-item.is-selected{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.dz-canvas{padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:10px}.dz-canvas-empty,.dz-empty{color:var(--mform-muted, #6b6580);font-size:13px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:18px;text-align:center}.dz-node{position:relative;border:1px solid transparent;border-radius:var(--mform-radius, 10px);padding:20px 10px 10px;cursor:pointer;background:var(--mform-surface, #fff)}.dz-node:hover{border-color:var(--mform-border, #d9d5e6)}.dz-node.is-selected{border-color:var(--mform-primary, #6d4bd0);box-shadow:0 0 0 1px var(--mform-primary, #6d4bd0)}.dz-chip{position:absolute;top:2px;left:10px;font-size:10px;color:var(--mform-muted, #6b6580);pointer-events:none}.dz-node.is-selected>.dz-chip{color:var(--mform-primary, #6d4bd0);font-weight:600}.dz-children{display:flex;flex-direction:column;gap:8px}.dz-columns{display:grid;gap:10px}.dz-column{display:flex;flex-direction:column;gap:8px;border:1px dashed var(--mform-border, #d9d5e6);border-radius:8px;padding:8px;min-height:40px}.dz-tabbar{display:flex;gap:4px;margin-bottom:8px;border-bottom:2px solid var(--mform-border, #d9d5e6)}.dz-tab{font:inherit;font-size:12px;padding:5px 12px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.dz-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.dz-leaf{pointer-events:none}.dz-props{border-left:1px solid var(--mform-border, #d9d5e6);background:var(--mform-surface, #fff);overflow-y:auto}.dz-props-empty{margin:12px}.dz-inspector{padding:12px;display:flex;flex-direction:column;gap:10px}.dz-inspector h3{margin:0;font-size:13px}.dz-inspector h3 code{color:var(--mform-muted, #6b6580);font-weight:400}.dz-prop{display:flex;flex-direction:column;gap:4px;font-size:12px}.dz-prop span{color:var(--mform-muted, #6b6580)}.dz-prop input,.dz-prop select,.dz-prop textarea{font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;background:#fff;width:100%;box-sizing:border-box}.dz-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer}.dz-check input{accent-color:var(--mform-primary, #6d4bd0)}.dz-section{border-top:1px solid var(--mform-border, #d9d5e6);padding-top:8px}.dz-section summary{font-size:12px;font-weight:600;cursor:pointer;margin-bottom:8px}.dz-section .dz-prop{margin-bottom:8px}.dz-option-row{display:flex;gap:6px;margin-bottom:6px}.dz-option-row input{flex:1;min-width:0;font-size:12px;padding:6px 8px;border:1px solid var(--mform-border, #d9d5e6);border-radius:6px}.dz-option-row button{border:none;background:none;cursor:pointer;color:var(--mform-muted, #6b6580)}.dz-add{font:inherit;font-size:12px;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);background:none;border-radius:8px;padding:6px 10px;cursor:pointer}.dz-preview{padding:20px;overflow-y:auto}.dz-preview-data{margin-top:16px;font-size:12px}.dz-preview-data pre{background:var(--mform-surface, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:8px;padding:10px;overflow-x:auto}\n"] }]
|
|
1290
|
+
}], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], extraCatalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraCatalog", required: false }] }], schemaChanged: [{ type: i0.Output, args: ["schemaChanged"] }] } });
|
|
1291
|
+
|
|
1292
|
+
/*
|
|
1293
|
+
* Designer entry point (`@mosaicoo/form-angular/designer`).
|
|
1294
|
+
*
|
|
1295
|
+
* Authoring lives behind this secondary entry so applications that only
|
|
1296
|
+
* RENDER forms never load designer code — required for federated hosts and
|
|
1297
|
+
* lazy loading.
|
|
1298
|
+
*/
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Generated bundle index. Do not edit.
|
|
1302
|
+
*/
|
|
1303
|
+
|
|
1304
|
+
export { DEFAULT_CATALOG, DesignerNodeComponent, FormDesignerComponent, InspectorComponent };
|
|
1305
|
+
//# sourceMappingURL=mosaicoo-form-angular-designer.mjs.map
|