ng-form-foundry 0.3.0 → 0.3.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/fesm2022/ng-form-foundry.mjs +553 -339
- package/fesm2022/ng-form-foundry.mjs.map +1 -1
- package/index.d.ts +250 -145
- package/package.json +1 -1
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
import * as i1 from '@angular/forms';
|
|
2
2
|
import { Validators, FormControl, FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { Input, Component,
|
|
4
|
+
import { Input, Component, output, EventEmitter, inject, ChangeDetectorRef, forwardRef, ViewChildren, Output, HostBinding, input, model, computed, ElementRef, effect, untracked } from '@angular/core';
|
|
5
5
|
import * as i2 from '@angular/material/form-field';
|
|
6
6
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
7
|
-
import * as
|
|
7
|
+
import * as i5 from '@angular/material/input';
|
|
8
8
|
import { MatInputModule, MatPrefix } from '@angular/material/input';
|
|
9
9
|
import * as i4 from '@angular/material/checkbox';
|
|
10
10
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
11
11
|
import * as i3 from '@angular/material/select';
|
|
12
12
|
import { MatSelectModule } from '@angular/material/select';
|
|
13
|
-
import * as
|
|
13
|
+
import * as i1$1 from '@angular/material/icon';
|
|
14
14
|
import { MatIconModule } from '@angular/material/icon';
|
|
15
|
-
import * as
|
|
15
|
+
import * as i2$1 from '@angular/material/button';
|
|
16
16
|
import { MatButtonModule } from '@angular/material/button';
|
|
17
17
|
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
|
|
18
18
|
import { MatDialog } from '@angular/material/dialog';
|
|
19
19
|
import * as i2$2 from '@angular/material/expansion';
|
|
20
20
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
21
|
-
import { MatCardModule } from '@angular/material/card';
|
|
22
21
|
import { NgTemplateOutlet } from '@angular/common';
|
|
23
|
-
import * as
|
|
22
|
+
import * as i3$1 from '@angular/material/menu';
|
|
24
23
|
import { MatMenuModule } from '@angular/material/menu';
|
|
25
24
|
|
|
26
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The control name that records which case of a {@link NodeChoice} is active.
|
|
27
|
+
* The name is reserved: it cannot be used as a case field name (the builder
|
|
28
|
+
* throws) or as a map entry key (the entry helpers reject it).
|
|
29
|
+
*/
|
|
27
30
|
const CASE_KEY = '__case';
|
|
28
31
|
|
|
29
32
|
// --- type guards
|
|
@@ -45,6 +48,20 @@ function isChoice(node) {
|
|
|
45
48
|
function isMap(node) {
|
|
46
49
|
return node.kind === 'map';
|
|
47
50
|
}
|
|
51
|
+
/** Whether the node is an optional (presence) node: its key is data, absent until enabled. */
|
|
52
|
+
function hasPresence(node) {
|
|
53
|
+
return ((node.kind === 'leaf' || node.kind === 'nodeGroup' || node.kind === 'map' || node.kind === 'choice') &&
|
|
54
|
+
node.presence === true);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a presence child should start absent: there is no initial data
|
|
58
|
+
* object, or its key is missing from it. A key that is present with an explicit
|
|
59
|
+
* `null` value keeps its control — `null` is a value (a nullable leaf's), while
|
|
60
|
+
* presence is about the *absent key*.
|
|
61
|
+
*/
|
|
62
|
+
function presenceAbsent(initial, key) {
|
|
63
|
+
return initial == null || !(key in initial);
|
|
64
|
+
}
|
|
48
65
|
function enumValidator(choices) {
|
|
49
66
|
const set = new Set(choices);
|
|
50
67
|
return (ctrl) => ctrl.value == null || set.has(ctrl.value) ? null : { enum: true };
|
|
@@ -162,6 +179,12 @@ function buildNodeGroupControl(group, initial) {
|
|
|
162
179
|
const controls = {};
|
|
163
180
|
for (const key in group.children) {
|
|
164
181
|
const child = group.children[key];
|
|
182
|
+
// An absent presence child gets no control at all, so it is absent from the
|
|
183
|
+
// form value until enabled. Because every nested group is built through this
|
|
184
|
+
// function — plain children, list items, map values, choice case fields —
|
|
185
|
+
// presence is honored at any depth.
|
|
186
|
+
if (hasPresence(child) && presenceAbsent(initial, key))
|
|
187
|
+
continue;
|
|
165
188
|
// Forward only this child's slice of the initial data, keyed by the child's
|
|
166
189
|
// record key. Passing the whole `initial` object seeds every leaf with the
|
|
167
190
|
// parent record and prevents list builders from sizing to the real data.
|
|
@@ -175,33 +198,24 @@ function buildNodeGroupListControl(list, initial = null) {
|
|
|
175
198
|
const values = Array.isArray(initial) ? initial : [null];
|
|
176
199
|
return new FormArray(values.map((v) => buildNodeGroupControl(list.type, v)));
|
|
177
200
|
}
|
|
178
|
-
/**
|
|
179
|
-
* Build the `AbstractControl` for a single schema node.
|
|
180
|
-
*
|
|
181
|
-
* Dispatches on `node.kind`: a `leaf` becomes a `FormControl`, a `leafList` a
|
|
182
|
-
* `FormArray` of controls, a `nodeGroup` a nested `FormGroup`, and a
|
|
183
|
-
* `nodeGroupList` a `FormArray` of groups. `initial` is the runtime value for
|
|
184
|
-
* this node — a scalar for a leaf, an array for a list, an object for a group —
|
|
185
|
-
* and seeds the control's value (falling back to the node's `default`).
|
|
186
|
-
*
|
|
187
|
-
* Most callers use {@link buildFormFromSchema}; this is exposed for building a
|
|
188
|
-
* control from a single non-root node.
|
|
189
|
-
*/
|
|
190
|
-
/**
|
|
191
|
-
* Build the FormGroup for a choice: a `__case` control holding the active case
|
|
192
|
-
* name plus that case's field controls. Only the active case's fields are built,
|
|
193
|
-
* matching the inline YANG encoding; switching the case swaps them.
|
|
194
|
-
*/
|
|
195
201
|
/**
|
|
196
202
|
* Normalize a {@link ChoiceCase} to a field record. A field record is returned
|
|
197
203
|
* as-is; a single node (a leaf-bodied case, e.g. a scalar `anyOf` branch) becomes
|
|
198
204
|
* a one-field record keyed by the node's `name`. The discriminant is a top-level
|
|
199
205
|
* `kind` string, which a field record never has (its keys are field names).
|
|
206
|
+
*
|
|
207
|
+
* Throws when a case field is keyed `__case`: that name is reserved for the
|
|
208
|
+
* choice discriminator ({@link CASE_KEY}) and a field under it would silently
|
|
209
|
+
* clobber the active-case control.
|
|
200
210
|
*/
|
|
201
211
|
function caseFields(body) {
|
|
202
|
-
|
|
212
|
+
const fields = typeof body.kind === 'string'
|
|
203
213
|
? { [body.name]: body }
|
|
204
214
|
: body;
|
|
215
|
+
if (CASE_KEY in fields) {
|
|
216
|
+
throw new Error(`"${CASE_KEY}" is reserved for the choice discriminator and cannot name a case field`);
|
|
217
|
+
}
|
|
218
|
+
return fields;
|
|
205
219
|
}
|
|
206
220
|
/**
|
|
207
221
|
* The active case of a choice: an explicit `__case` in the initial value, else
|
|
@@ -234,12 +248,21 @@ function inferChoiceCase(choice, initial) {
|
|
|
234
248
|
}
|
|
235
249
|
return best;
|
|
236
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Build the FormGroup for a choice: a `__case` control holding the active case
|
|
253
|
+
* name plus that case's field controls. Only the active case's fields are built,
|
|
254
|
+
* matching the inline YANG encoding; switching the case swaps them.
|
|
255
|
+
*/
|
|
237
256
|
function buildChoiceControl(choice, initial) {
|
|
238
257
|
const active = resolveChoiceCase(choice, initial);
|
|
239
258
|
const controls = { [CASE_KEY]: new FormControl(active ?? null) };
|
|
240
259
|
if (active && choice.cases[active]) {
|
|
241
260
|
const caseChildren = caseFields(choice.cases[active]);
|
|
242
261
|
for (const key in caseChildren) {
|
|
262
|
+
// Case fields honor presence like any group's children: an absent
|
|
263
|
+
// presence field gets no control.
|
|
264
|
+
if (hasPresence(caseChildren[key]) && presenceAbsent(initial, key))
|
|
265
|
+
continue;
|
|
243
266
|
controls[key] = buildControl(caseChildren[key], initial?.[key]);
|
|
244
267
|
}
|
|
245
268
|
}
|
|
@@ -248,18 +271,24 @@ function buildChoiceControl(choice, initial) {
|
|
|
248
271
|
/**
|
|
249
272
|
* Switch a choice's FormGroup to `caseName`: sets `__case`, removes every other
|
|
250
273
|
* control, and builds `caseName`'s fields (normalized via {@link caseFields})
|
|
251
|
-
* with their defaults.
|
|
274
|
+
* with their defaults. Presence fields of the new case start absent — the
|
|
275
|
+
* switch carries no data that could make them present. An unknown case name
|
|
276
|
+
* leaves only `__case`. The swap is atomic: one value change fires, and every
|
|
277
|
+
* observable snapshot has fields matching its discriminator.
|
|
252
278
|
*/
|
|
253
279
|
function switchChoiceCase(group, choice, caseName) {
|
|
254
|
-
group.get(CASE_KEY)?.setValue(caseName);
|
|
280
|
+
group.get(CASE_KEY)?.setValue(caseName, { emitEvent: false });
|
|
255
281
|
for (const name of Object.keys(group.controls)) {
|
|
256
282
|
if (name !== CASE_KEY)
|
|
257
|
-
group.removeControl(name);
|
|
283
|
+
group.removeControl(name, { emitEvent: false });
|
|
258
284
|
}
|
|
259
285
|
const caseChildren = choice.cases[caseName] ? caseFields(choice.cases[caseName]) : {};
|
|
260
286
|
for (const name in caseChildren) {
|
|
261
|
-
|
|
287
|
+
if (hasPresence(caseChildren[name]))
|
|
288
|
+
continue;
|
|
289
|
+
group.addControl(name, buildControl(caseChildren[name]), { emitEvent: false });
|
|
262
290
|
}
|
|
291
|
+
group.updateValueAndValidity();
|
|
263
292
|
}
|
|
264
293
|
/**
|
|
265
294
|
* Append a map entry built from `map.value` and return its committed key, or
|
|
@@ -273,11 +302,14 @@ function addMapEntry(group, map, key) {
|
|
|
273
302
|
return null;
|
|
274
303
|
let committed;
|
|
275
304
|
if (key != null) {
|
|
276
|
-
|
|
305
|
+
const trimmed = key.trim();
|
|
306
|
+
// `__case` is reserved for the choice discriminator; as an entry key it
|
|
307
|
+
// would make the map group indistinguishable from a choice group.
|
|
308
|
+
if (!trimmed || trimmed === CASE_KEY || group.contains(trimmed))
|
|
277
309
|
return null;
|
|
278
|
-
if (map.keyPattern && !new RegExp(map.keyPattern).test(
|
|
310
|
+
if (map.keyPattern && !new RegExp(map.keyPattern).test(trimmed))
|
|
279
311
|
return null;
|
|
280
|
-
committed =
|
|
312
|
+
committed = trimmed;
|
|
281
313
|
}
|
|
282
314
|
else {
|
|
283
315
|
let n = Object.keys(group.controls).length + 1;
|
|
@@ -290,21 +322,35 @@ function addMapEntry(group, map, key) {
|
|
|
290
322
|
}
|
|
291
323
|
/**
|
|
292
324
|
* Rename entry `oldKey` to `newKey.trim()`, preserving the control instance
|
|
293
|
-
* (
|
|
294
|
-
*
|
|
295
|
-
*
|
|
325
|
+
* (so the value survives) and the entry's position in the group's key order —
|
|
326
|
+
* the order `getRawValue()` serializes and the tree editor renders. Returns
|
|
327
|
+
* whether the rename was committed: an empty, reserved (`__case`), unchanged,
|
|
328
|
+
* duplicate, or `keyPattern`-violating key is a no-op, leaving the entry under
|
|
329
|
+
* its current name. Entry keys are looked up verbatim — never via
|
|
330
|
+
* `AbstractControl.get`, which would split keys like `10.0.0.1` into
|
|
331
|
+
* dot-delimited paths. Emits a single value change.
|
|
296
332
|
*/
|
|
297
333
|
function renameMapEntry(group, map, oldKey, newKey) {
|
|
298
334
|
const committed = newKey.trim();
|
|
299
|
-
if (!committed || committed === oldKey || group.contains(committed))
|
|
335
|
+
if (!committed || committed === CASE_KEY || committed === oldKey || group.contains(committed))
|
|
300
336
|
return false;
|
|
301
337
|
if (map.keyPattern && !new RegExp(map.keyPattern).test(committed))
|
|
302
338
|
return false;
|
|
303
|
-
const control = group.
|
|
339
|
+
const control = group.controls[oldKey];
|
|
304
340
|
if (!control)
|
|
305
341
|
return false;
|
|
306
|
-
|
|
307
|
-
|
|
342
|
+
// Re-key in place: swap the name, then re-append every key that followed so
|
|
343
|
+
// the renamed entry does not jump to the end of the key order.
|
|
344
|
+
const following = Object.keys(group.controls);
|
|
345
|
+
following.splice(0, following.indexOf(oldKey) + 1);
|
|
346
|
+
group.removeControl(oldKey, { emitEvent: false });
|
|
347
|
+
group.addControl(committed, control, { emitEvent: false });
|
|
348
|
+
for (const key of following) {
|
|
349
|
+
const sibling = group.controls[key];
|
|
350
|
+
group.removeControl(key, { emitEvent: false });
|
|
351
|
+
group.addControl(key, sibling, { emitEvent: false });
|
|
352
|
+
}
|
|
353
|
+
group.updateValueAndValidity();
|
|
308
354
|
return true;
|
|
309
355
|
}
|
|
310
356
|
/** Remove entry `key` unless the map is at `minEntries`. Returns whether it was removed. */
|
|
@@ -316,11 +362,46 @@ function removeMapEntry(group, map, key) {
|
|
|
316
362
|
group.removeControl(key);
|
|
317
363
|
return true;
|
|
318
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* The map's own constraints as a group validator: entry count against
|
|
367
|
+
* `minEntries`/`maxEntries` and every entry key against `keyPattern`. The UI
|
|
368
|
+
* gates prevent most violations; the validator reports the ones that slip
|
|
369
|
+
* through (seeded wire data, generated `keyN` placeholders awaiting a rename).
|
|
370
|
+
*/
|
|
371
|
+
function mapValidator(map) {
|
|
372
|
+
let re = null;
|
|
373
|
+
if (map.keyPattern) {
|
|
374
|
+
try {
|
|
375
|
+
re = new RegExp(map.keyPattern);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
re = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return (ctrl) => {
|
|
382
|
+
const keys = Object.keys(ctrl.controls);
|
|
383
|
+
const errors = {};
|
|
384
|
+
if (map.minEntries != null && keys.length < map.minEntries) {
|
|
385
|
+
errors['minEntries'] = { required: map.minEntries, actual: keys.length };
|
|
386
|
+
}
|
|
387
|
+
if (map.maxEntries != null && keys.length > map.maxEntries) {
|
|
388
|
+
errors['maxEntries'] = { allowed: map.maxEntries, actual: keys.length };
|
|
389
|
+
}
|
|
390
|
+
if (re) {
|
|
391
|
+
const invalidKeys = keys.filter((k) => !re.test(k));
|
|
392
|
+
if (invalidKeys.length)
|
|
393
|
+
errors['keyPattern'] = { pattern: map.keyPattern, keys: invalidKeys };
|
|
394
|
+
}
|
|
395
|
+
return Object.keys(errors).length ? errors : null;
|
|
396
|
+
};
|
|
397
|
+
}
|
|
319
398
|
/**
|
|
320
399
|
* Build the FormGroup for a map: one control per entry, keyed by the entry key,
|
|
321
400
|
* each built from the map's shared `value` schema. Because the entry keys are the
|
|
322
401
|
* control names, `getRawValue()` is the map object directly. Empty when no
|
|
323
|
-
* initial object is supplied; the renderer adds/removes/renames entries.
|
|
402
|
+
* initial object is supplied; the renderer adds/removes/renames entries. The
|
|
403
|
+
* group carries {@link mapValidator}, so `keyPattern`/`minEntries`/`maxEntries`
|
|
404
|
+
* violations surface as validation errors.
|
|
324
405
|
*/
|
|
325
406
|
function buildMapControl(map, initial) {
|
|
326
407
|
const controls = {};
|
|
@@ -328,8 +409,20 @@ function buildMapControl(map, initial) {
|
|
|
328
409
|
for (const key of Object.keys(source)) {
|
|
329
410
|
controls[key] = buildControl(map.value, source[key]);
|
|
330
411
|
}
|
|
331
|
-
return new FormGroup(controls);
|
|
412
|
+
return new FormGroup(controls, { validators: mapValidator(map) });
|
|
332
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Build the `AbstractControl` for a single schema node.
|
|
416
|
+
*
|
|
417
|
+
* Dispatches on `node.kind`: a `leaf` becomes a `FormControl`, a `leafList` a
|
|
418
|
+
* `FormArray` of controls, a `nodeGroup` a nested `FormGroup`, and a
|
|
419
|
+
* `nodeGroupList` a `FormArray` of groups. `initial` is the runtime value for
|
|
420
|
+
* this node — a scalar for a leaf, an array for a list, an object for a group —
|
|
421
|
+
* and seeds the control's value (falling back to the node's `default`).
|
|
422
|
+
*
|
|
423
|
+
* Most callers use {@link buildFormFromSchema}; this is exposed for building a
|
|
424
|
+
* control from a single non-root node.
|
|
425
|
+
*/
|
|
333
426
|
function buildControl(node, initial) {
|
|
334
427
|
if (isLeaf(node)) {
|
|
335
428
|
return buildLeafControl(node, initial);
|
|
@@ -362,41 +455,19 @@ function buildControl(node, initial) {
|
|
|
362
455
|
* is an optional value object keyed by the schema's `children` keys; each child
|
|
363
456
|
* control is seeded from its matching slice (falling back to the node `default`).
|
|
364
457
|
*
|
|
458
|
+
* Presence nodes whose key is absent from `initial` get no control, at any depth
|
|
459
|
+
* — plain children, list items, map values, and choice case fields alike — so
|
|
460
|
+
* they are absent from the form value until enabled. A key present with an
|
|
461
|
+
* explicit `null` keeps its control: `null` is a value, absence is the missing
|
|
462
|
+
* key.
|
|
463
|
+
*
|
|
365
464
|
* Inference only holds when `schema`'s literal type is preserved. Author schemas
|
|
366
465
|
* with {@link defineSchema} or a `satisfies NodeGroup` annotation — never
|
|
367
466
|
* `const schema: NodeGroup = ...`, which widens `children` and erases the field
|
|
368
467
|
* names and value types.
|
|
369
468
|
*/
|
|
370
|
-
/**
|
|
371
|
-
* Remove presence nodes that have no initial value so they are absent from the
|
|
372
|
-
* built form (and from `form.value`) until the user toggles them on. Applies to
|
|
373
|
-
* presence groups, leaves, maps, and choices. Runs on the fully-built, attached
|
|
374
|
-
* tree. `removeControl` is used rather than `disable` because a disabled control
|
|
375
|
-
* is still included in `FormGroup.value`.
|
|
376
|
-
*/
|
|
377
|
-
function applyPresence(group, schema, initial) {
|
|
378
|
-
for (const key in schema.children) {
|
|
379
|
-
const child = schema.children[key];
|
|
380
|
-
const childInitial = initial?.[key];
|
|
381
|
-
if (child.kind === 'leaf' || child.kind === 'map' || child.kind === 'choice') {
|
|
382
|
-
if (child.presence && childInitial == null)
|
|
383
|
-
group.removeControl(key);
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (child.kind !== 'nodeGroup')
|
|
387
|
-
continue;
|
|
388
|
-
if (child.presence && childInitial == null) {
|
|
389
|
-
group.removeControl(key);
|
|
390
|
-
}
|
|
391
|
-
else if (group.get(key) instanceof FormGroup) {
|
|
392
|
-
applyPresence(group.get(key), child, childInitial);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
469
|
function buildFormFromSchema(schema, initial = null) {
|
|
397
|
-
|
|
398
|
-
applyPresence(group, schema, initial);
|
|
399
|
-
return group;
|
|
470
|
+
return buildNodeGroupControl(schema, initial);
|
|
400
471
|
}
|
|
401
472
|
/**
|
|
402
473
|
* Capture a schema literal with its exact type while checking it against
|
|
@@ -423,11 +494,11 @@ class LeafEnumRendererComponent {
|
|
|
423
494
|
remove;
|
|
424
495
|
editable = true;
|
|
425
496
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafEnumRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
426
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafEnumRendererComponent, isStandalone: true, selector: "nff-leaf-enum-renderer", inputs: { leafEnum: "leafEnum", initialValue: "initialValue", control: "control", removable: "removable", remove: "remove", editable: "editable" }, ngImport: i0, template: "<mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>\n <mat-select [formControl]=\"control\" value=\"{{
|
|
497
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafEnumRendererComponent, isStandalone: true, selector: "nff-leaf-enum-renderer", inputs: { leafEnum: "leafEnum", initialValue: "initialValue", control: "control", removable: "removable", remove: "remove", editable: "editable" }, ngImport: i0, template: "<mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>\n @if (editable) {\n <mat-select [formControl]=\"control\">\n @for (option of leafEnum.enum; track $index) {\n <mat-option [value]=\"option\">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>\n }\n </mat-select>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change. -->\n <mat-select [value]=\"control.value\" disabled>\n @for (option of leafEnum.enum; track $index) {\n <mat-option [value]=\"option\">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>\n }\n </mat-select>\n }\n</mat-form-field>\n", styles: [":host{flex:1;min-width:0;width:100%}mat-form-field{width:100%}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }] });
|
|
427
498
|
}
|
|
428
499
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafEnumRendererComponent, decorators: [{
|
|
429
500
|
type: Component,
|
|
430
|
-
args: [{ selector: 'nff-leaf-enum-renderer', standalone: true, imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule], template: "<mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>\n <mat-select [formControl]=\"control\" value=\"{{
|
|
501
|
+
args: [{ selector: 'nff-leaf-enum-renderer', standalone: true, imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule], template: "<mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>\n @if (editable) {\n <mat-select [formControl]=\"control\">\n @for (option of leafEnum.enum; track $index) {\n <mat-option [value]=\"option\">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>\n }\n </mat-select>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change. -->\n <mat-select [value]=\"control.value\" disabled>\n @for (option of leafEnum.enum; track $index) {\n <mat-option [value]=\"option\">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>\n }\n </mat-select>\n }\n</mat-form-field>\n", styles: [":host{flex:1;min-width:0;width:100%}mat-form-field{width:100%}\n"] }]
|
|
431
502
|
}], propDecorators: { leafEnum: [{
|
|
432
503
|
type: Input
|
|
433
504
|
}], initialValue: [{
|
|
@@ -446,20 +517,15 @@ class LeafRendererComponent {
|
|
|
446
517
|
elementRef;
|
|
447
518
|
leaf_;
|
|
448
519
|
control = new FormControl();
|
|
449
|
-
initialValue;
|
|
450
520
|
removable = false;
|
|
451
521
|
editable = true;
|
|
452
522
|
/** Focus the field once rendered — for fields the user just added (e.g. an enabled presence leaf). */
|
|
453
523
|
autofocus = false;
|
|
454
|
-
|
|
524
|
+
/** Emitted when the user removes this field (e.g. an optional presence leaf). */
|
|
525
|
+
remove = output();
|
|
455
526
|
constructor(elementRef) {
|
|
456
527
|
this.elementRef = elementRef;
|
|
457
528
|
}
|
|
458
|
-
ngOnInit() {
|
|
459
|
-
if (this.initialValue) {
|
|
460
|
-
this.control.patchValue(this.initialValue);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
529
|
ngAfterViewInit() {
|
|
464
530
|
if (this.autofocus) {
|
|
465
531
|
// Deferred out of the change-detection pass: focusing flips the form
|
|
@@ -504,7 +570,7 @@ class LeafRendererComponent {
|
|
|
504
570
|
return 'Invalid value';
|
|
505
571
|
}
|
|
506
572
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
507
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafRendererComponent, isStandalone: true, selector: "nff-leaf-renderer", inputs: { leaf_: "leaf_", control: "control",
|
|
573
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafRendererComponent, isStandalone: true, selector: "nff-leaf-renderer", inputs: { leaf_: "leaf_", control: "control", removable: "removable", editable: "editable", autofocus: "autofocus" }, outputs: { remove: "remove" }, ngImport: i0, template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n @if (fieldEditable) {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change.\n The control itself is never disable()d \u2014 a disabled control would\n still appear in the form value with different semantics. -->\n <mat-checkbox [checked]=\"control.value === true\" disabled>{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n }\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove\" [attr.aria-label]=\"'Remove ' + ('label' in leaf_ && leaf_.label ? leaf_.label : 'name' in leaf_ ? leaf_.name : 'field')\" (click)=\"remove.emit()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%;gap:8px}mat-form-field{flex:1;min-width:0}.remove-button{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}\n"], dependencies: [{ kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: LeafEnumRendererComponent, selector: "nff-leaf-enum-renderer", inputs: ["leafEnum", "initialValue", "control", "removable", "remove", "editable"] }] });
|
|
508
574
|
}
|
|
509
575
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, decorators: [{
|
|
510
576
|
type: Component,
|
|
@@ -518,22 +584,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
518
584
|
MatButtonModule,
|
|
519
585
|
MatTooltip,
|
|
520
586
|
LeafEnumRendererComponent,
|
|
521
|
-
], template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove\" (click)=\"remove.emit(
|
|
587
|
+
], template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [formControl]=\"control\">\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n @if (fieldEditable) {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change.\n The control itself is never disable()d \u2014 a disabled control would\n still appear in the form value with different semantics. -->\n <mat-checkbox [checked]=\"control.value === true\" disabled>{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n }\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove\" [attr.aria-label]=\"'Remove ' + ('label' in leaf_ && leaf_.label ? leaf_.label : 'name' in leaf_ ? leaf_.name : 'field')\" (click)=\"remove.emit()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%;gap:8px}mat-form-field{flex:1;min-width:0}.remove-button{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}\n"] }]
|
|
522
588
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { leaf_: [{
|
|
523
589
|
type: Input
|
|
524
590
|
}], control: [{
|
|
525
591
|
type: Input
|
|
526
|
-
}], initialValue: [{
|
|
527
|
-
type: Input
|
|
528
592
|
}], removable: [{
|
|
529
593
|
type: Input
|
|
530
594
|
}], editable: [{
|
|
531
595
|
type: Input
|
|
532
596
|
}], autofocus: [{
|
|
533
597
|
type: Input
|
|
534
|
-
}], remove: [{
|
|
535
|
-
type: Output
|
|
536
|
-
}] } });
|
|
598
|
+
}], remove: [{ type: i0.Output, args: ["remove"] }] } });
|
|
537
599
|
|
|
538
600
|
class NodeGroupListRendererComponent {
|
|
539
601
|
nodeGroupList;
|
|
@@ -597,7 +659,7 @@ class NodeGroupListRendererComponent {
|
|
|
597
659
|
}
|
|
598
660
|
}
|
|
599
661
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
600
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeGroupListRendererComponent, isStandalone: true, selector: "nff-node-group-list-renderer", inputs: { nodeGroupList: "nodeGroupList", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems", maxItems: "maxItems" }, outputs: { message: "message" }, viewQueries: [{ propertyName: "items", predicate: i0.forwardRef(() => DynamicRecursiveFormComponent), descendants: true }], ngImport: i0, template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > minItems\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"addItem\"\n />\n </div>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback"], outputs: ["remove", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatTooltipModule) }] });
|
|
662
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeGroupListRendererComponent, isStandalone: true, selector: "nff-node-group-list-renderer", inputs: { nodeGroupList: "nodeGroupList", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems", maxItems: "maxItems" }, outputs: { message: "message" }, viewQueries: [{ propertyName: "items", predicate: i0.forwardRef(() => DynamicRecursiveFormComponent), descendants: true }], ngImport: i0, template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > minItems\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"addItem\"\n />\n </div>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback", "focusLeaf"], outputs: ["remove", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatTooltipModule) }] });
|
|
601
663
|
}
|
|
602
664
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, decorators: [{
|
|
603
665
|
type: Component,
|
|
@@ -639,7 +701,7 @@ class AnonLeafRendererComponent {
|
|
|
639
701
|
this.remove.emit();
|
|
640
702
|
}
|
|
641
703
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AnonLeafRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
642
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: AnonLeafRendererComponent, isStandalone: true, selector: "nff-anon-leaf-renderer", inputs: { AnonLeaf: "AnonLeaf", initialValue: "initialValue", control: "control", removable: "removable", editable: "editable", index: "index", label: "label" }, outputs: { remove: "remove" }, ngImport: i0, template: "<div class=\"anon-leaf-container\">\n@if (AnonLeaf.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"text\" >\n @if (removable) {\n }\n </mat-form-field>\n} @else if (AnonLeaf.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"number\">\n </mat-form-field>\n} @else if (AnonLeaf.type === 'boolean') {\n <mat-checkbox [
|
|
704
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: AnonLeafRendererComponent, isStandalone: true, selector: "nff-anon-leaf-renderer", inputs: { AnonLeaf: "AnonLeaf", initialValue: "initialValue", control: "control", removable: "removable", editable: "editable", index: "index", label: "label" }, outputs: { remove: "remove" }, ngImport: i0, template: "<div class=\"anon-leaf-container\">\n@if (AnonLeaf.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"text\" >\n @if (removable) {\n }\n </mat-form-field>\n} @else if (AnonLeaf.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"number\">\n </mat-form-field>\n} @else if (AnonLeaf.type === 'boolean') {\n @if (editable) {\n <mat-checkbox [formControl]=\"control\">value</mat-checkbox>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change. -->\n <mat-checkbox [checked]=\"control.value === true\" disabled>value</mat-checkbox>\n }\n}\n @else if (AnonLeaf.type === 'enum' && 'enum' in AnonLeaf) {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n @if (editable) {\n <mat-select [formControl]=\"control\">\n @for (option of (AnonLeaf.type === 'enum' ? AnonLeaf.enum : []); track $index) {\n <mat-option [value]=\"option\">{{ option }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [value]=\"control.value\" disabled>\n @for (option of (AnonLeaf.type === 'enum' ? AnonLeaf.enum : []); track $index) {\n <mat-option [value]=\"option\">{{ option }}</mat-option>\n }\n </mat-select>\n }\n </mat-form-field>\n}\n</div>\n", styles: [".anon-leaf-container{display:flex;flex-direction:row;gap:8px;align-items:center;flex:1 1 0}mat-form-field{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatButtonModule }] });
|
|
643
705
|
}
|
|
644
706
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AnonLeafRendererComponent, decorators: [{
|
|
645
707
|
type: Component,
|
|
@@ -651,7 +713,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
651
713
|
MatCheckboxModule,
|
|
652
714
|
MatInputModule,
|
|
653
715
|
MatButtonModule,
|
|
654
|
-
], template: "<div class=\"anon-leaf-container\">\n@if (AnonLeaf.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"text\" >\n @if (removable) {\n }\n </mat-form-field>\n} @else if (AnonLeaf.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"number\">\n </mat-form-field>\n} @else if (AnonLeaf.type === 'boolean') {\n <mat-checkbox [
|
|
716
|
+
], template: "<div class=\"anon-leaf-container\">\n@if (AnonLeaf.type === 'string') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"text\" >\n @if (removable) {\n }\n </mat-form-field>\n} @else if (AnonLeaf.type === 'number') {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n <input [readonly]=\"!editable\"\n [formControl]=\"control\"\n matInput type=\"number\">\n </mat-form-field>\n} @else if (AnonLeaf.type === 'boolean') {\n @if (editable) {\n <mat-checkbox [formControl]=\"control\">value</mat-checkbox>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change. -->\n <mat-checkbox [checked]=\"control.value === true\" disabled>value</mat-checkbox>\n }\n}\n @else if (AnonLeaf.type === 'enum' && 'enum' in AnonLeaf) {\n <mat-form-field [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ label }} #{{ index + 1}}</mat-label>\n @if (editable) {\n <mat-select [formControl]=\"control\">\n @for (option of (AnonLeaf.type === 'enum' ? AnonLeaf.enum : []); track $index) {\n <mat-option [value]=\"option\">{{ option }}</mat-option>\n }\n </mat-select>\n } @else {\n <mat-select [value]=\"control.value\" disabled>\n @for (option of (AnonLeaf.type === 'enum' ? AnonLeaf.enum : []); track $index) {\n <mat-option [value]=\"option\">{{ option }}</mat-option>\n }\n </mat-select>\n }\n </mat-form-field>\n}\n</div>\n", styles: [".anon-leaf-container{display:flex;flex-direction:row;gap:8px;align-items:center;flex:1 1 0}mat-form-field{flex:1}\n"] }]
|
|
655
717
|
}], propDecorators: { AnonLeaf: [{
|
|
656
718
|
type: Input
|
|
657
719
|
}], initialValue: [{
|
|
@@ -706,7 +768,7 @@ class LeafListRendererComponent {
|
|
|
706
768
|
this.formArray.push(new FormControl());
|
|
707
769
|
}
|
|
708
770
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
709
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafListRendererComponent, isStandalone: true, selector: "nff-leaf-list-renderer", inputs: { leaf_: "leaf_", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems" }, outputs: { message: "message" }, host: { properties: { "class.stacked": "this.stacked" } }, ngImport: i0, template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{width:100%}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type:
|
|
771
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafListRendererComponent, isStandalone: true, selector: "nff-leaf-list-renderer", inputs: { leaf_: "leaf_", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems" }, outputs: { message: "message" }, host: { properties: { "class.stacked": "this.stacked" } }, ngImport: i0, template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{width:100%}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: AnonLeafRendererComponent, selector: "nff-anon-leaf-renderer", inputs: ["AnonLeaf", "initialValue", "control", "removable", "editable", "index", "label"], outputs: ["remove"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }] });
|
|
710
772
|
}
|
|
711
773
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
|
|
712
774
|
type: Component,
|
|
@@ -715,7 +777,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
715
777
|
AnonLeafRendererComponent,
|
|
716
778
|
MatButtonModule,
|
|
717
779
|
MatPrefix,
|
|
718
|
-
], template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{width:100%}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}\n"] }]
|
|
780
|
+
], template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{width:100%}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}\n"] }]
|
|
719
781
|
}], propDecorators: { leaf_: [{
|
|
720
782
|
type: Input
|
|
721
783
|
}], initialValue: [{
|
|
@@ -733,37 +795,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
733
795
|
args: ['class.stacked']
|
|
734
796
|
}] } });
|
|
735
797
|
|
|
736
|
-
function nodeGroupChildrenList(schema) {
|
|
737
|
-
const children = schema.children ?? {};
|
|
738
|
-
return Object.entries(children).map(([key, value]) => ({
|
|
739
|
-
key,
|
|
740
|
-
value: value,
|
|
741
|
-
}));
|
|
742
|
-
}
|
|
743
|
-
function nodeGroupChildrenLeafs(schema) {
|
|
744
|
-
const children = schema.children ?? {};
|
|
745
|
-
return Object.entries(children).reduce((acc, [key, value]) => {
|
|
746
|
-
if (value.kind === 'leaf') {
|
|
747
|
-
acc.push({ key, value: value });
|
|
748
|
-
}
|
|
749
|
-
return acc;
|
|
750
|
-
}, []);
|
|
751
|
-
}
|
|
752
|
-
function removeNullAndEmptyArrays(value) {
|
|
753
|
-
if (Array.isArray(value)) {
|
|
754
|
-
const cleanedArray = value
|
|
755
|
-
.map(removeNullAndEmptyArrays)
|
|
756
|
-
.filter(v => v != null && (typeof v !== 'object' || Object.keys(v).length > 0));
|
|
757
|
-
return cleanedArray.length > 0 ? cleanedArray : undefined;
|
|
758
|
-
}
|
|
759
|
-
if (value && typeof value === 'object') {
|
|
760
|
-
const cleanedObject = Object.fromEntries(Object.entries(value)
|
|
761
|
-
.map(([k, v]) => [k, removeNullAndEmptyArrays(v)])
|
|
762
|
-
.filter(([_, v]) => v != null && (typeof v !== 'object' || Object.keys(v).length > 0)));
|
|
763
|
-
return Object.keys(cleanedObject).length > 0 ? cleanedObject : undefined;
|
|
764
|
-
}
|
|
765
|
-
return value != null ? value : undefined;
|
|
766
|
-
}
|
|
767
798
|
function asFormControl(control) {
|
|
768
799
|
return control;
|
|
769
800
|
}
|
|
@@ -787,8 +818,12 @@ class NodeMapRendererComponent {
|
|
|
787
818
|
editable = true;
|
|
788
819
|
/** Ordered view of entry keys, kept stable across renames (`addControl` appends). */
|
|
789
820
|
entryKeys = [];
|
|
790
|
-
|
|
791
|
-
|
|
821
|
+
ngOnChanges(changes) {
|
|
822
|
+
// Re-sync whenever the bound group changes: a host may rebind the renderer
|
|
823
|
+
// to another map (e.g. the tree editor swapping documents) while this
|
|
824
|
+
// component instance survives.
|
|
825
|
+
if (changes['formGroup'])
|
|
826
|
+
this.entryKeys = Object.keys(this.formGroup.controls);
|
|
792
827
|
}
|
|
793
828
|
/** The value schema as a leaf (used when `value.kind === 'leaf'`). */
|
|
794
829
|
get valueLeaf() {
|
|
@@ -830,7 +865,7 @@ class NodeMapRendererComponent {
|
|
|
830
865
|
asFormControl = asFormControl;
|
|
831
866
|
asFormGroup = asFormGroup;
|
|
832
867
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
833
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeMapRendererComponent, isStandalone: true, selector: "nff-node-map-renderer", inputs: { nodeMap: "nodeMap", formGroup: "formGroup", editable: "editable" }, ngImport: i0, template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value)\"\n >\n </mat-form-field>\n\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.
|
|
868
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeMapRendererComponent, isStandalone: true, selector: "nff-node-map-renderer", inputs: { nodeMap: "nodeMap", formGroup: "formGroup", editable: "editable" }, usesOnChanges: true, ngImport: i0, template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [attr.aria-label]=\"'Remove ' + key + ' entry'\" (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton aria-label=\"Add entry\" (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i1$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatInputModule) }, { kind: "directive", type: i0.forwardRef(() => i5.MatInput), selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback", "focusLeaf"], outputs: ["remove", "editableChange"] }] });
|
|
834
869
|
}
|
|
835
870
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, decorators: [{
|
|
836
871
|
type: Component,
|
|
@@ -843,7 +878,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
843
878
|
// node-map-renderer and dynamic-recursive-form import each other (a map value
|
|
844
879
|
// may be a group); forwardRef defers the reference to break the cycle.
|
|
845
880
|
forwardRef(() => DynamicRecursiveFormComponent),
|
|
846
|
-
], template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value)\"\n >\n </mat-form-field>\n\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.
|
|
881
|
+
], template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [attr.aria-label]=\"'Remove ' + key + ' entry'\" (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton aria-label=\"Add entry\" (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}\n"] }]
|
|
847
882
|
}], propDecorators: { nodeMap: [{
|
|
848
883
|
type: Input
|
|
849
884
|
}], formGroup: [{
|
|
@@ -871,12 +906,26 @@ class DynamicRecursiveFormComponent {
|
|
|
871
906
|
editable = model(false, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
872
907
|
/** Invoked with {@link index} to append a new sibling form to a parent list. */
|
|
873
908
|
addButtonCallback = input(null, ...(ngDevMode ? [{ debugName: "addButtonCallback" }] : []));
|
|
909
|
+
/**
|
|
910
|
+
* Key of a presence leaf whose field should grab focus when it renders — lets
|
|
911
|
+
* a host that added the control itself (e.g. the tree editor's optionals
|
|
912
|
+
* menu) hand focus to the new field, like the form's own add button does.
|
|
913
|
+
*/
|
|
914
|
+
focusLeaf = input(null, ...(ngDevMode ? [{ debugName: "focusLeaf" }] : []));
|
|
874
915
|
/** True when the schema is a root group (rendered flat, without a wrapping card). */
|
|
875
916
|
root = computed(() => this.schema().root ?? false, ...(ngDevMode ? [{ debugName: "root" }] : []));
|
|
876
917
|
ngOnInit() {
|
|
877
918
|
const initial = this.initialValue();
|
|
878
919
|
if (initial) {
|
|
879
|
-
this.formGroup()
|
|
920
|
+
const group = this.formGroup();
|
|
921
|
+
// Presence keys carried by the initial value need controls first:
|
|
922
|
+
// patchValue silently skips keys that have none, dropping the data.
|
|
923
|
+
for (const [key, child] of Object.entries(this.schema().children ?? {})) {
|
|
924
|
+
if ('presence' in child && child.presence && key in initial && !group.get(key)) {
|
|
925
|
+
group.addControl(key, buildControl(child, initial[key]));
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
group.patchValue(initial);
|
|
880
929
|
}
|
|
881
930
|
}
|
|
882
931
|
get nodeGroupChildrenList() {
|
|
@@ -889,59 +938,44 @@ class DynamicRecursiveFormComponent {
|
|
|
889
938
|
emitRemoveEvent() {
|
|
890
939
|
this.remove.emit();
|
|
891
940
|
}
|
|
892
|
-
/**
|
|
893
|
-
* Add or remove a presence group's control on this form. Removing it drops the
|
|
894
|
-
* group from `form.value`; adding it rebuilds the sub-group from its schema.
|
|
895
|
-
*/
|
|
896
|
-
togglePresence(key, schema, present) {
|
|
897
|
-
const group = this.formGroup();
|
|
898
|
-
if (present) {
|
|
899
|
-
if (!group.get(key)) {
|
|
900
|
-
group.addControl(key, buildFormFromSchema(schema));
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
else if (group.get(key)) {
|
|
904
|
-
group.removeControl(key);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
941
|
/** The key of the presence leaf the user just enabled; its field grabs focus when rendered. */
|
|
908
942
|
presenceFocusKey = null;
|
|
909
943
|
/**
|
|
910
|
-
* Add or remove a presence
|
|
911
|
-
* leaf
|
|
912
|
-
*
|
|
944
|
+
* Add or remove a presence node's control on this form — any presence kind:
|
|
945
|
+
* group, leaf, map, or choice. Removing it drops the key from `form.value`;
|
|
946
|
+
* adding it builds the control fresh from its schema (nested presence
|
|
947
|
+
* children start absent).
|
|
913
948
|
*/
|
|
914
|
-
|
|
949
|
+
toggleNodePresence(key, schema, present) {
|
|
915
950
|
const group = this.formGroup();
|
|
916
951
|
if (present) {
|
|
917
952
|
if (!group.get(key)) {
|
|
918
953
|
group.addControl(key, buildControl(schema));
|
|
919
|
-
this.presenceFocusKey = key;
|
|
920
954
|
}
|
|
921
955
|
}
|
|
922
956
|
else if (group.get(key)) {
|
|
923
957
|
group.removeControl(key);
|
|
924
|
-
if (this.presenceFocusKey === key)
|
|
925
|
-
this.presenceFocusKey = null;
|
|
926
958
|
}
|
|
927
959
|
}
|
|
928
960
|
/**
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* map, or a choice group holding `__case`) from its schema.
|
|
961
|
+
* {@link toggleNodePresence} for a presence leaf, additionally focusing the
|
|
962
|
+
* rendered field when the toggle just created it.
|
|
932
963
|
*/
|
|
933
|
-
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
964
|
+
toggleLeafPresence(key, schema, present) {
|
|
965
|
+
const had = !!this.formGroup().get(key);
|
|
966
|
+
this.toggleNodePresence(key, schema, present);
|
|
967
|
+
if (present && !had) {
|
|
968
|
+
this.presenceFocusKey = key;
|
|
969
|
+
// Retire the request once the field has rendered and taken focus, so a
|
|
970
|
+
// later re-creation of the same renderer cannot steal focus again.
|
|
971
|
+
setTimeout(() => {
|
|
972
|
+
if (this.presenceFocusKey === key)
|
|
973
|
+
this.presenceFocusKey = null;
|
|
974
|
+
});
|
|
942
975
|
}
|
|
976
|
+
if (!present && this.presenceFocusKey === key)
|
|
977
|
+
this.presenceFocusKey = null;
|
|
943
978
|
}
|
|
944
|
-
CASE_KEY = CASE_KEY;
|
|
945
979
|
objectKeys(obj) {
|
|
946
980
|
return Object.keys(obj);
|
|
947
981
|
}
|
|
@@ -978,7 +1012,7 @@ class DynamicRecursiveFormComponent {
|
|
|
978
1012
|
asFormArray = asFormArray;
|
|
979
1013
|
asFormControl = asFormControl;
|
|
980
1014
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
981
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: DynamicRecursiveFormComponent, isStandalone: true, selector: "nff-dynamic-recursive-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, ngImport: i0, template: "@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(child.name))\n [removable]=false\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n }\n\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(child.name))\n [removable]=false\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n <mat-accordion multi>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ title() ?? child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </mat-accordion>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%;margin:4px 0}.section-actions{justify-content:flex-end;align-items:center;gap:4px}.presence-leaf-add{flex:0 1 auto;align-self:flex-start;margin-top:8px}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}nff-leaf-list-renderer{flex:1}.mat-expansion-panel-content,.mat-expansion-panel-content:hover{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.node-groups-container{display:flex;flex-direction:column;flex-basis:100%}.section-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;width:100%}mat-card{padding:0;width:100%}.mat-card-no-broder{border:0}mat-card-content{box-sizing:border-box;padding:16px}mat-card-header{display:flex;flex-direction:row;align-items:flex-start;padding:16px;gap:16px}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-transition .5s}.card-actions{display:flex;flex-direction:row;align-items:center;gap:4px}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}.flattened-style-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;align-items:flex-start}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "minItems", "maxItems"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatus), selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatusGroup), selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i0.forwardRef(() => i1.FormGroupDirective), selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i0.forwardRef(() => i1.FormControlName), selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatAccordion), selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelDescription), selector: "mat-panel-description" }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i4$1.MatButton), selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i0.forwardRef(() => i4$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i0.forwardRef(() => NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatCardModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatCheckboxModule) }, { kind: "component", type: i0.forwardRef(() => i4.MatCheckbox), selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatSelectModule) }, { kind: "component", type: i0.forwardRef(() => i3.MatSelect), selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i0.forwardRef(() => i3.MatOption), selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
|
|
1015
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: DynamicRecursiveFormComponent, isStandalone: true, selector: "nff-dynamic-recursive-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null }, focusLeaf: { classPropertyName: "focusLeaf", publicName: "focusLeaf", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, ngImport: i0, template: "@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%;margin:4px 0}.presence-leaf-add{flex:0 1 auto;align-self:flex-start;margin-top:8px}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}nff-leaf-list-renderer{flex:1}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback", "focusLeaf"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "minItems", "maxItems"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatusGroup), selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i0.forwardRef(() => i1.FormGroupDirective), selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelDescription), selector: "mat-panel-description" }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i1$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatButton), selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i0.forwardRef(() => NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatCheckboxModule) }, { kind: "component", type: i0.forwardRef(() => i4.MatCheckbox), selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatSelectModule) }, { kind: "component", type: i0.forwardRef(() => i3.MatSelect), selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i0.forwardRef(() => i3.MatOption), selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
|
|
982
1016
|
}
|
|
983
1017
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
|
|
984
1018
|
type: Component,
|
|
@@ -995,27 +1029,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
995
1029
|
MatIconModule,
|
|
996
1030
|
MatButtonModule,
|
|
997
1031
|
NgTemplateOutlet,
|
|
998
|
-
MatCardModule,
|
|
999
1032
|
MatCheckboxModule,
|
|
1000
1033
|
MatFormFieldModule,
|
|
1001
1034
|
MatSelectModule,
|
|
1002
1035
|
MatTooltip,
|
|
1003
|
-
], selector: 'nff-dynamic-recursive-form', standalone: true, template: "@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(child.name))\n [removable]=false\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n }\n\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <!-- Present: the field takes the add button's place in the row; its\n remove control drops the leaf back to the unchecked state. -->\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [autofocus]=\"presenceFocusKey === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n [disabled]=\"!editable()\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n } @else if (child.kind == 'leaf') {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(child.name))\n [removable]=false\n [editable]=\"editable()\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n />\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\" [formGroup]=\"choiceGroup\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [formControlName]=\"CASE_KEY\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n <mat-accordion multi>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ title() ?? child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"togglePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(child.name))\"\n [initialValue]=\"formGroup().get(child.name)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </mat-accordion>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%;margin:4px 0}.section-actions{justify-content:flex-end;align-items:center;gap:4px}.presence-leaf-add{flex:0 1 auto;align-self:flex-start;margin-top:8px}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}nff-leaf-list-renderer{flex:1}.mat-expansion-panel-content,.mat-expansion-panel-content:hover{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.node-groups-container{display:flex;flex-direction:column;flex-basis:100%}.section-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;width:100%}mat-card{padding:0;width:100%}.mat-card-no-broder{border:0}mat-card-content{box-sizing:border-box;padding:16px}mat-card-header{display:flex;flex-direction:row;align-items:flex-start;padding:16px;gap:16px}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-transition .5s}.card-actions{display:flex;flex-direction:row;align-items:center;gap:4px}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}.flattened-style-actions{display:flex;flex-direction:row;gap:8px;justify-content:flex-end;align-items:flex-start}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}\n"] }]
|
|
1004
|
-
}], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }, { type: i0.Output, args: ["editableChange"] }], addButtonCallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonCallback", required: false }] }] } });
|
|
1036
|
+
], selector: 'nff-dynamic-recursive-form', standalone: true, template: "@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton matTooltip=\"+ Add new {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <div class=\"fields\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n </div>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%;margin:4px 0}.presence-leaf-add{flex:0 1 auto;align-self:flex-start;margin-top:8px}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}nff-leaf-list-renderer{flex:1}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}\n"] }]
|
|
1037
|
+
}], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }, { type: i0.Output, args: ["editableChange"] }], addButtonCallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonCallback", required: false }] }], focusLeaf: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusLeaf", required: false }] }] } });
|
|
1005
1038
|
|
|
1006
1039
|
/**
|
|
1007
1040
|
* A tree/detail editor for a schema-built form: the structure (groups, lists,
|
|
1008
|
-
* maps, choices) is a tree on the left, and selecting a node
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1041
|
+
* maps, choices) is a tree on the left, and selecting a node renders that
|
|
1042
|
+
* node's **entire subtree** on the right as a **flat list of sections** — the
|
|
1043
|
+
* node's own fields first, then every descendant's fields, each separated by a
|
|
1044
|
+
* breadcrumb heading (`Service / Deploy scope / …`) instead of nested panels.
|
|
1045
|
+
* Leaf fields render through {@link DynamicRecursiveFormComponent} with a
|
|
1046
|
+
* leaf-only schema slice; choice selectors, map rows, and add controls render
|
|
1047
|
+
* inline in their section. The tree adds row conveniences of its own: `+` on
|
|
1048
|
+
* list and map rows, a delete control on removable rows, and a
|
|
1049
|
+
* "+ Optional field" menu for absent presence children.
|
|
1050
|
+
*
|
|
1051
|
+
* The tree is **derived state**: any structural change to the form — made
|
|
1052
|
+
* through the tree rows or through the detail sections — triggers a rebuild (a
|
|
1053
|
+
* cheap shape signature over `valueChanges` detects it). Node ids are stable
|
|
1054
|
+
* paths, so expansion and selection survive rebuilds. Swapping the `schema` or
|
|
1055
|
+
* `formGroup` input rebinds the editor to the new pair, resetting expansion
|
|
1056
|
+
* and selection.
|
|
1013
1057
|
*
|
|
1014
1058
|
* The component draws no outer container — only a divider between the tree and
|
|
1015
|
-
* detail panes — so the embedding client owns the surrounding chrome.
|
|
1016
|
-
* is built once from the `schema`/`formGroup` provided at initialization.
|
|
1017
|
-
* An alternative to the all-in-one {@link DynamicRecursiveFormComponent} for
|
|
1018
|
-
* large configs.
|
|
1059
|
+
* detail panes — so the embedding client owns the surrounding chrome.
|
|
1019
1060
|
*/
|
|
1020
1061
|
class ConfigEditorComponent {
|
|
1021
1062
|
/** The form-description schema whose structure the tree renders. */
|
|
@@ -1026,31 +1067,63 @@ class ConfigEditorComponent {
|
|
|
1026
1067
|
editable = input(true, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
1027
1068
|
root;
|
|
1028
1069
|
selected = null;
|
|
1070
|
+
/** The selected subtree as a flat, breadcrumb-separated section list. */
|
|
1071
|
+
sections = [];
|
|
1072
|
+
/** Root-to-selection trail for the detail-pane breadcrumb, computed once per selection. */
|
|
1073
|
+
breadcrumb = [];
|
|
1029
1074
|
expanded = new Set();
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1075
|
+
shape = '';
|
|
1076
|
+
changes;
|
|
1077
|
+
host = inject(ElementRef);
|
|
1078
|
+
/** Stable per-instance ids for the shape signature, so replacing a control (setControl) reads as a structural change. */
|
|
1079
|
+
controlIds = new WeakMap();
|
|
1080
|
+
controlIdSeq = 0;
|
|
1081
|
+
constructor() {
|
|
1082
|
+
// Rebind whenever the schema or form inputs change (a host loading another
|
|
1083
|
+
// config document): derived state resets against the new pair. Wrapped in
|
|
1084
|
+
// untracked so only the two inputs re-trigger the effect.
|
|
1085
|
+
effect(() => {
|
|
1086
|
+
const schema = this.schema();
|
|
1087
|
+
const group = this.formGroup();
|
|
1088
|
+
untracked(() => this.attach(schema, group));
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
ngOnDestroy() {
|
|
1092
|
+
this.changes?.unsubscribe();
|
|
1093
|
+
}
|
|
1094
|
+
/** Bind the editor to a schema/form pair: fresh tree, root selection, and shape-sync subscription. */
|
|
1095
|
+
attach(schema, group) {
|
|
1096
|
+
this.changes?.unsubscribe();
|
|
1097
|
+
this.expanded.clear();
|
|
1098
|
+
this.root = this.buildTree(schema, group, schema.label ?? schema.name, '');
|
|
1099
|
+
this.shape = this.shapeOf(group);
|
|
1034
1100
|
this.expanded.add(this.root.id);
|
|
1035
1101
|
this.select(this.root);
|
|
1102
|
+
// The detail sections mutate the FormGroup directly (presence toggles,
|
|
1103
|
+
// list items, map entries, case switches); a shape change there must
|
|
1104
|
+
// reflect in the tree.
|
|
1105
|
+
this.changes = group.valueChanges.subscribe(() => this.syncShape());
|
|
1036
1106
|
}
|
|
1037
|
-
select(node) {
|
|
1107
|
+
select(node, reveal = true) {
|
|
1038
1108
|
this.selected = node;
|
|
1039
|
-
|
|
1040
|
-
|
|
1109
|
+
this.sections = this.buildSections(node, []);
|
|
1110
|
+
this.breadcrumb = this.pathTo(node);
|
|
1111
|
+
// Navigating retires any pending just-added-leaf focus request.
|
|
1112
|
+
this.focusSectionId = null;
|
|
1041
1113
|
this.focusLeafKey = null;
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1114
|
+
// Reveal the selection: expand every ancestor so the row is visible, and
|
|
1115
|
+
// the node itself as it opens on the right. Structural re-syncs pass
|
|
1116
|
+
// reveal=false — they must not re-expand what the user collapsed.
|
|
1117
|
+
if (reveal) {
|
|
1118
|
+
for (const crumb of this.breadcrumb) {
|
|
1119
|
+
if (crumb !== node || this.hasExpandableContent(crumb))
|
|
1120
|
+
this.expanded.add(crumb.id);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1049
1123
|
}
|
|
1050
1124
|
/**
|
|
1051
1125
|
* Root-to-`target` path for the detail-pane breadcrumb (inclusive of both ends),
|
|
1052
|
-
* or an empty array if `target` is not in the current tree.
|
|
1053
|
-
* call so it stays correct after add/remove/presence mutations rebuild subtrees.
|
|
1126
|
+
* or an empty array if `target` is not in the current tree.
|
|
1054
1127
|
*/
|
|
1055
1128
|
pathTo(target) {
|
|
1056
1129
|
const walk = (node, trail) => {
|
|
@@ -1072,6 +1145,31 @@ class ConfigEditorComponent {
|
|
|
1072
1145
|
else
|
|
1073
1146
|
this.expanded.add(node.id);
|
|
1074
1147
|
}
|
|
1148
|
+
/** Keyboard access for tree rows: Enter/Space selects, ArrowRight expands, ArrowLeft collapses. */
|
|
1149
|
+
onRowKeydown(event, node) {
|
|
1150
|
+
// Keys pressed on the row's inner buttons keep their own meaning.
|
|
1151
|
+
if (event.target !== event.currentTarget)
|
|
1152
|
+
return;
|
|
1153
|
+
switch (event.key) {
|
|
1154
|
+
case 'Enter':
|
|
1155
|
+
case ' ':
|
|
1156
|
+
event.preventDefault();
|
|
1157
|
+
this.select(node);
|
|
1158
|
+
break;
|
|
1159
|
+
case 'ArrowRight':
|
|
1160
|
+
if (this.hasExpandableContent(node) && !this.expanded.has(node.id)) {
|
|
1161
|
+
event.preventDefault();
|
|
1162
|
+
this.expanded.add(node.id);
|
|
1163
|
+
}
|
|
1164
|
+
break;
|
|
1165
|
+
case 'ArrowLeft':
|
|
1166
|
+
if (this.expanded.has(node.id)) {
|
|
1167
|
+
event.preventDefault();
|
|
1168
|
+
this.expanded.delete(node.id);
|
|
1169
|
+
}
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1075
1173
|
/** Whether the row shows an expand twisty: it has child rows to reveal (children or an optionals menu row). */
|
|
1076
1174
|
hasExpandableContent(node) {
|
|
1077
1175
|
return node.children.length > 0 || (this.editable() && !!node.optionals?.length);
|
|
@@ -1084,73 +1182,70 @@ class ConfigEditorComponent {
|
|
|
1084
1182
|
hasError(node) {
|
|
1085
1183
|
return !!(node.group?.invalid || node.list?.array.invalid || node.map?.group.invalid);
|
|
1086
1184
|
}
|
|
1087
|
-
/** Append a new item to a list node's FormArray
|
|
1185
|
+
/** Append a new item to a list node's FormArray (up to `maxItems`), then select it. */
|
|
1088
1186
|
addItem(listNode) {
|
|
1089
1187
|
const list = listNode.list;
|
|
1090
1188
|
if (!list)
|
|
1091
1189
|
return;
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1190
|
+
if (list.maxItems != null && list.array.length >= list.maxItems)
|
|
1191
|
+
return;
|
|
1192
|
+
list.array.push(buildFormFromSchema(list.itemSchema));
|
|
1193
|
+
this.selectByPath(this.join(listNode.id, String(list.array.length - 1)));
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Remove a list item from its FormArray (down to `minItems`). List-item
|
|
1197
|
+
* identity is positional, so expansion state under the list is cleared —
|
|
1198
|
+
* otherwise it would silently migrate to the items that shift into the
|
|
1199
|
+
* removed indexes.
|
|
1200
|
+
*/
|
|
1102
1201
|
removeItem(listNode, item) {
|
|
1103
1202
|
if (!listNode.list || !item.removable)
|
|
1104
1203
|
return;
|
|
1105
1204
|
if (listNode.list.array.length <= listNode.list.minItems)
|
|
1106
1205
|
return;
|
|
1206
|
+
for (const id of [...this.expanded]) {
|
|
1207
|
+
if (id.startsWith(`${listNode.id}/`))
|
|
1208
|
+
this.expanded.delete(id);
|
|
1209
|
+
}
|
|
1107
1210
|
listNode.list.array.removeAt(item.removable.index);
|
|
1108
|
-
|
|
1109
|
-
this.
|
|
1110
|
-
this.reselectIfOrphaned(listNode);
|
|
1211
|
+
this.selectByPath(listNode.id);
|
|
1212
|
+
this.focusSelectedRow();
|
|
1111
1213
|
}
|
|
1112
|
-
/** The
|
|
1214
|
+
/** The just-added optional leaf (section path + key) whose field grabs focus when rendered. */
|
|
1215
|
+
focusSectionId = null;
|
|
1113
1216
|
focusLeafKey = null;
|
|
1114
|
-
/** Add an absent optional child from the menu: build its control
|
|
1217
|
+
/** Add an absent optional child from the menu: build its control and select the node it lands on. */
|
|
1115
1218
|
addOptional(node, entry) {
|
|
1116
1219
|
if (!node.group || node.group.get(entry.key))
|
|
1117
1220
|
return;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1221
|
+
node.group.addControl(entry.key, buildControl(entry.schema));
|
|
1222
|
+
// A leaf renders in the parent's detail pane; complex kinds become tree nodes.
|
|
1223
|
+
this.selectByPath(entry.schema.kind === 'leaf' ? node.id : this.join(node.id, entry.key));
|
|
1224
|
+
// Adding the last optional removes the menu row that held focus.
|
|
1225
|
+
if (entry.schema.kind !== 'leaf')
|
|
1226
|
+
this.focusSelectedRow();
|
|
1123
1227
|
if (entry.schema.kind === 'leaf') {
|
|
1124
|
-
|
|
1125
|
-
|
|
1228
|
+
// Set after selection (select() retires any pending request): the new
|
|
1229
|
+
// field should grab focus, like the form's own add button. Retired after
|
|
1230
|
+
// a tick so later re-renders of the section cannot steal focus again.
|
|
1231
|
+
this.focusSectionId = node.id;
|
|
1126
1232
|
this.focusLeafKey = entry.key;
|
|
1127
|
-
|
|
1233
|
+
setTimeout(() => {
|
|
1234
|
+
if (this.focusLeafKey === entry.key) {
|
|
1235
|
+
this.focusSectionId = null;
|
|
1236
|
+
this.focusLeafKey = null;
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1128
1239
|
}
|
|
1129
|
-
const child = this.buildChildNode(entry.schema, control, entry.label);
|
|
1130
|
-
if (!child)
|
|
1131
|
-
return;
|
|
1132
|
-
child.presenceRemovable = { entry };
|
|
1133
|
-
node.children.push(child);
|
|
1134
|
-
this.expanded.add(node.id);
|
|
1135
|
-
this.select(child);
|
|
1136
1240
|
}
|
|
1137
1241
|
/** Remove a present optional child node, returning its entry to the parent's menu. */
|
|
1138
1242
|
removeOptional(parent, node) {
|
|
1139
|
-
const
|
|
1140
|
-
if (!
|
|
1141
|
-
return;
|
|
1142
|
-
parent.group.removeControl(entry.key);
|
|
1143
|
-
parent.children.splice(parent.children.indexOf(node), 1);
|
|
1144
|
-
this.reinsertOptional(parent, entry);
|
|
1145
|
-
this.reselectIfOrphaned(parent);
|
|
1146
|
-
}
|
|
1147
|
-
/** Remove a present optional leaf from the detail pane, returning its entry to the menu. */
|
|
1148
|
-
removeOptionalLeaf(node, leaf) {
|
|
1149
|
-
if (!leaf.optional || !node.group)
|
|
1243
|
+
const key = node.presenceRemovable?.key;
|
|
1244
|
+
if (!key || !parent.group)
|
|
1150
1245
|
return;
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
this.
|
|
1246
|
+
parent.group.removeControl(key);
|
|
1247
|
+
this.selectByPath(parent.id);
|
|
1248
|
+
this.focusSelectedRow();
|
|
1154
1249
|
}
|
|
1155
1250
|
/** The active case name of a choice node, or null when none is selected. */
|
|
1156
1251
|
activeCase(node) {
|
|
@@ -1160,25 +1255,22 @@ class ConfigEditorComponent {
|
|
|
1160
1255
|
activeCaseLabel(node) {
|
|
1161
1256
|
const c = node.choice;
|
|
1162
1257
|
const active = this.activeCase(node);
|
|
1163
|
-
return c && active ?
|
|
1258
|
+
return c && active ? (c.schema.caseLabels?.[active] ?? active) : null;
|
|
1164
1259
|
}
|
|
1165
1260
|
/** The display label for a case: the schema's `caseLabels` entry, else the case name. */
|
|
1166
1261
|
caseLabel(choice, caseName) {
|
|
1167
1262
|
return choice.caseLabels?.[caseName] ?? caseName;
|
|
1168
1263
|
}
|
|
1169
|
-
/** Switch a choice node's active case
|
|
1264
|
+
/** Switch a choice node's active case; the structural sync rebuilds the tree and sections. */
|
|
1170
1265
|
switchTreeCase(node, caseName) {
|
|
1171
1266
|
const c = node.choice;
|
|
1172
1267
|
if (!c)
|
|
1173
1268
|
return;
|
|
1174
1269
|
switchChoiceCase(c.group, c.schema, caseName);
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
node.optionals = rebuilt.optionals;
|
|
1180
|
-
this.expanded.add(node.id);
|
|
1181
|
-
this.select(node);
|
|
1270
|
+
this.selectByPath(node.id);
|
|
1271
|
+
}
|
|
1272
|
+
objectKeys(obj) {
|
|
1273
|
+
return Object.keys(obj);
|
|
1182
1274
|
}
|
|
1183
1275
|
/** Append a new entry to a complex map node under a generated unique key, then select it. */
|
|
1184
1276
|
addTreeMapEntry(mapNode) {
|
|
@@ -1186,36 +1278,69 @@ class ConfigEditorComponent {
|
|
|
1186
1278
|
if (!m)
|
|
1187
1279
|
return;
|
|
1188
1280
|
const key = addMapEntry(m.group, m.schema);
|
|
1189
|
-
if (key
|
|
1190
|
-
|
|
1191
|
-
const child = this.buildChildNode(m.schema.value, m.group.get(key), key);
|
|
1192
|
-
if (!child)
|
|
1193
|
-
return;
|
|
1194
|
-
child.mapEntry = { mapGroup: m.group, mapSchema: m.schema, key };
|
|
1195
|
-
mapNode.children.push(child);
|
|
1196
|
-
this.expanded.add(mapNode.id);
|
|
1197
|
-
this.select(child);
|
|
1281
|
+
if (key != null)
|
|
1282
|
+
this.selectByPath(this.join(mapNode.id, key));
|
|
1198
1283
|
}
|
|
1199
|
-
/** Remove a complex map entry (down to `minEntries`)
|
|
1284
|
+
/** Remove a complex map entry (down to `minEntries`). */
|
|
1200
1285
|
removeTreeMapEntry(mapNode, entryNode) {
|
|
1201
1286
|
const m = mapNode.map;
|
|
1202
1287
|
const e = entryNode.mapEntry;
|
|
1203
1288
|
if (!m || !e || !removeMapEntry(m.group, m.schema, e.key))
|
|
1204
1289
|
return;
|
|
1205
|
-
|
|
1206
|
-
this.
|
|
1290
|
+
this.selectByPath(mapNode.id);
|
|
1291
|
+
this.focusSelectedRow();
|
|
1207
1292
|
}
|
|
1208
|
-
/**
|
|
1293
|
+
/**
|
|
1294
|
+
* Commit a rename-on-blur of a map entry's key; on success the entry is
|
|
1295
|
+
* selected under its new path and its fresh key field regains focus (the
|
|
1296
|
+
* rename re-renders the section under a new id, destroying the input that
|
|
1297
|
+
* held focus).
|
|
1298
|
+
*/
|
|
1209
1299
|
renameTreeMapEntry(entryNode, rawKey) {
|
|
1210
1300
|
const e = entryNode.mapEntry;
|
|
1211
1301
|
if (!e)
|
|
1212
1302
|
return;
|
|
1213
1303
|
if (renameMapEntry(e.mapGroup, e.mapSchema, e.key, rawKey)) {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1304
|
+
const parentPath = entryNode.id.slice(0, entryNode.id.lastIndexOf('/'));
|
|
1305
|
+
const newId = this.join(parentPath, rawKey.trim());
|
|
1306
|
+
// The entry's descendants keep their expansion under the new identity.
|
|
1307
|
+
for (const id of [...this.expanded]) {
|
|
1308
|
+
if (id === entryNode.id || id.startsWith(`${entryNode.id}/`)) {
|
|
1309
|
+
this.expanded.delete(id);
|
|
1310
|
+
this.expanded.add(newId + id.slice(entryNode.id.length));
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
this.selectByPath(newId);
|
|
1314
|
+
// Deferred so the rebuilt section's key field exists before focusing.
|
|
1315
|
+
setTimeout(() => this.host.nativeElement.querySelector('.detail .key-field input')?.focus());
|
|
1217
1316
|
}
|
|
1218
1317
|
}
|
|
1318
|
+
/** Move keyboard focus to the selected tree row once the action's re-render settles. */
|
|
1319
|
+
focusSelectedRow() {
|
|
1320
|
+
setTimeout(() => this.host.nativeElement.querySelector('.tree-row.selected')?.focus());
|
|
1321
|
+
}
|
|
1322
|
+
/** A muted hint for a section whose node currently renders no content of its own. */
|
|
1323
|
+
emptySectionHint(s) {
|
|
1324
|
+
const n = s.node;
|
|
1325
|
+
if (n.list && !n.children.length)
|
|
1326
|
+
return `No ${n.list.itemLabel} items.`;
|
|
1327
|
+
if (n.map?.complex && !n.children.length)
|
|
1328
|
+
return 'No entries.';
|
|
1329
|
+
if (n.map && !n.map.complex && !Object.keys(n.map.group.controls).length && !this.editable()) {
|
|
1330
|
+
return 'No entries.';
|
|
1331
|
+
}
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
/** Whether a list node is at `maxItems` (the add control is hidden). */
|
|
1335
|
+
listAtMax(node) {
|
|
1336
|
+
const l = node?.list;
|
|
1337
|
+
return !!l && l.maxItems != null && l.array.length >= l.maxItems;
|
|
1338
|
+
}
|
|
1339
|
+
/** Whether a list node is at `minItems` (item remove controls are hidden). */
|
|
1340
|
+
listAtMin(node) {
|
|
1341
|
+
const l = node?.list;
|
|
1342
|
+
return !!l && l.array.length <= l.minItems;
|
|
1343
|
+
}
|
|
1219
1344
|
/** Whether a map node is at `maxEntries` (the add control is hidden). */
|
|
1220
1345
|
mapAtMax(node) {
|
|
1221
1346
|
const m = node?.map;
|
|
@@ -1226,84 +1351,154 @@ class ConfigEditorComponent {
|
|
|
1226
1351
|
const m = node?.map;
|
|
1227
1352
|
return !!m && m.schema.minEntries != null && Object.keys(m.group.controls).length <= m.schema.minEntries;
|
|
1228
1353
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
listNode.children.forEach((child, i) => {
|
|
1238
|
-
if (child.removable)
|
|
1239
|
-
child.removable.index = i;
|
|
1240
|
-
child.label = `#${i + 1}`;
|
|
1241
|
-
});
|
|
1354
|
+
// --- derived-tree maintenance ----------------------------------------------
|
|
1355
|
+
/** Rebuild the tree from the current form structure if its shape changed. */
|
|
1356
|
+
syncShape() {
|
|
1357
|
+
const shape = this.shapeOf(this.formGroup());
|
|
1358
|
+
if (shape === this.shape)
|
|
1359
|
+
return;
|
|
1360
|
+
this.rebuild();
|
|
1361
|
+
this.reconcileSelection();
|
|
1242
1362
|
}
|
|
1243
|
-
/**
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1363
|
+
/**
|
|
1364
|
+
* A cheap structural signature of a control tree: group keys (plus the active
|
|
1365
|
+
* `__case`), array lengths, leaf placeholders. Value edits don't change it;
|
|
1366
|
+
* added/removed/renamed controls and case switches do. Reading `__case` from
|
|
1367
|
+
* any group is safe because the name is reserved (the builder rejects it as a
|
|
1368
|
+
* case field name or map entry key), so only choice groups carry it.
|
|
1369
|
+
*/
|
|
1370
|
+
shapeOf(control) {
|
|
1371
|
+
if (control instanceof FormGroup) {
|
|
1372
|
+
const inner = Object.keys(control.controls)
|
|
1373
|
+
.sort()
|
|
1374
|
+
.map((k) => `${k}:${this.shapeOf(control.controls[k])}`)
|
|
1375
|
+
.join(',');
|
|
1376
|
+
const active = control.get(CASE_KEY)?.value;
|
|
1377
|
+
return `{#${this.uidOf(control)}${typeof active === 'string' ? `=${active};` : ''}${inner}}`;
|
|
1378
|
+
}
|
|
1379
|
+
if (control instanceof FormArray) {
|
|
1380
|
+
return `[#${this.uidOf(control)}${control.controls.map((c) => this.shapeOf(c)).join(',')}]`;
|
|
1381
|
+
}
|
|
1382
|
+
return '.';
|
|
1383
|
+
}
|
|
1384
|
+
/** The instance id a container contributes to the shape signature. */
|
|
1385
|
+
uidOf(control) {
|
|
1386
|
+
let id = this.controlIds.get(control);
|
|
1387
|
+
if (id == null) {
|
|
1388
|
+
id = ++this.controlIdSeq;
|
|
1389
|
+
this.controlIds.set(control, id);
|
|
1390
|
+
}
|
|
1391
|
+
return id;
|
|
1256
1392
|
}
|
|
1257
|
-
/**
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1393
|
+
/** Rebuild the whole tree from schema + form and refresh the shape signature. */
|
|
1394
|
+
rebuild() {
|
|
1395
|
+
const schema = this.schema();
|
|
1396
|
+
this.root = this.buildTree(schema, this.formGroup(), schema.label ?? schema.name, '');
|
|
1397
|
+
this.shape = this.shapeOf(this.formGroup());
|
|
1398
|
+
}
|
|
1399
|
+
/** Re-point `selected` at the rebuilt tree: same path, else the closest surviving ancestor. */
|
|
1400
|
+
reconcileSelection() {
|
|
1401
|
+
this.selectByPath(this.selected?.id ?? '', false);
|
|
1402
|
+
}
|
|
1403
|
+
/** The node at `path` in the current tree, or null. Walks by id-prefix segments. */
|
|
1404
|
+
byPath(path) {
|
|
1405
|
+
if (path === this.root.id)
|
|
1406
|
+
return this.root;
|
|
1407
|
+
const walk = (node) => {
|
|
1408
|
+
for (const child of node.children) {
|
|
1409
|
+
if (child.id === path)
|
|
1410
|
+
return child;
|
|
1411
|
+
if (path.startsWith(child.id + '/'))
|
|
1412
|
+
return walk(child);
|
|
1413
|
+
}
|
|
1414
|
+
return null;
|
|
1263
1415
|
};
|
|
1416
|
+
return walk(this.root);
|
|
1417
|
+
}
|
|
1418
|
+
/** Select the node at `path`, falling back through its ancestors to the root. */
|
|
1419
|
+
selectByPath(path, reveal = true) {
|
|
1420
|
+
let target = this.byPath(path);
|
|
1421
|
+
let trimmed = path;
|
|
1422
|
+
while (!target && trimmed.includes('/')) {
|
|
1423
|
+
trimmed = trimmed.slice(0, trimmed.lastIndexOf('/'));
|
|
1424
|
+
target = this.byPath(trimmed);
|
|
1425
|
+
}
|
|
1426
|
+
this.select(target ?? this.root, reveal);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Flatten a subtree into detail sections, pre-order: the node's own fields
|
|
1430
|
+
* first, then each child as its own breadcrumb-headed section. No nesting
|
|
1431
|
+
* chrome — the headings are the boundaries between children.
|
|
1432
|
+
*/
|
|
1433
|
+
buildSections(node, trail) {
|
|
1434
|
+
const here = [...trail, node];
|
|
1435
|
+
const list = [{ node, trail: here, ...this.sectionContent(node) }];
|
|
1436
|
+
for (const child of node.children)
|
|
1437
|
+
list.push(...this.buildSections(child, here));
|
|
1438
|
+
return list;
|
|
1439
|
+
}
|
|
1440
|
+
/** A section's own renderable fields: a leaf-only schema slice and the group it binds to. */
|
|
1441
|
+
sectionContent(node) {
|
|
1442
|
+
if (node.choice) {
|
|
1443
|
+
const active = this.activeCase(node);
|
|
1444
|
+
const body = active && node.choice.schema.cases[active] ? this.caseAsGroup(node.choice.schema, active) : null;
|
|
1445
|
+
return { schema: body ? this.leafOnly(body) : null, group: node.choice.group };
|
|
1446
|
+
}
|
|
1447
|
+
if (node.schema && node.group) {
|
|
1448
|
+
return { schema: this.leafOnly(node.schema), group: node.group };
|
|
1449
|
+
}
|
|
1450
|
+
return { schema: null, group: null };
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* The node's own fields as a flattened schema: leaf and leafList children
|
|
1454
|
+
* only. Complex children are rendered as sections of their own, so the
|
|
1455
|
+
* embedded form never draws nested section chrome. Null when there are none.
|
|
1456
|
+
*/
|
|
1457
|
+
leafOnly(schema) {
|
|
1458
|
+
const children = {};
|
|
1459
|
+
for (const key of Object.keys(schema.children)) {
|
|
1460
|
+
const child = schema.children[key];
|
|
1461
|
+
if (child.kind === 'leaf' || child.kind === 'leafList')
|
|
1462
|
+
children[key] = child;
|
|
1463
|
+
}
|
|
1464
|
+
if (!Object.keys(children).length)
|
|
1465
|
+
return null;
|
|
1466
|
+
return { ...schema, root: false, children, appearance: { flatten: true } };
|
|
1264
1467
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
const leafLists = [];
|
|
1468
|
+
// --- tree construction -----------------------------------------------------
|
|
1469
|
+
buildTree(schema, group, label, path) {
|
|
1268
1470
|
const children = [];
|
|
1269
1471
|
const optionals = [];
|
|
1270
|
-
const
|
|
1271
|
-
keys.forEach((key, order) => {
|
|
1472
|
+
for (const key of Object.keys(schema.children)) {
|
|
1272
1473
|
const child = schema.children[key];
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
leaves.push({ key, node: child, optional: child.presence ? entry : undefined });
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
if (child.kind === 'leafList') {
|
|
1283
|
-
leafLists.push({ key, node: child });
|
|
1284
|
-
return;
|
|
1474
|
+
if (child.kind === 'leaf' || child.kind === 'leafList') {
|
|
1475
|
+
// Leaves render in the detail pane; an absent presence leaf is offered by the menu.
|
|
1476
|
+
if (child.kind === 'leaf' && child.presence && !group.get(key)) {
|
|
1477
|
+
optionals.push({ key, schema: child, label: this.labelOf(child, key) });
|
|
1478
|
+
}
|
|
1479
|
+
continue;
|
|
1285
1480
|
}
|
|
1286
1481
|
const presence = child.kind !== 'nodeGroupList' && child.presence;
|
|
1287
|
-
if (presence && !
|
|
1288
|
-
optionals.push(
|
|
1289
|
-
|
|
1482
|
+
if (presence && !group.get(key)) {
|
|
1483
|
+
optionals.push({ key, schema: child, label: this.labelOf(child, key) });
|
|
1484
|
+
continue;
|
|
1290
1485
|
}
|
|
1291
|
-
const node = this.buildChildNode(child,
|
|
1486
|
+
const node = this.buildChildNode(child, group.get(key), this.labelOf(child, key), this.join(path, key));
|
|
1292
1487
|
if (!node)
|
|
1293
|
-
|
|
1488
|
+
continue;
|
|
1294
1489
|
if (presence)
|
|
1295
|
-
node.presenceRemovable = {
|
|
1490
|
+
node.presenceRemovable = { key };
|
|
1296
1491
|
children.push(node);
|
|
1297
|
-
}
|
|
1298
|
-
const node = { id:
|
|
1492
|
+
}
|
|
1493
|
+
const node = { id: path, label, children, group, schema };
|
|
1299
1494
|
if (optionals.length)
|
|
1300
1495
|
node.optionals = optionals;
|
|
1301
1496
|
return node;
|
|
1302
1497
|
}
|
|
1303
1498
|
/** Build the tree node for a single non-leaf child, dispatching on its kind. Null when its control is missing. */
|
|
1304
|
-
buildChildNode(schema, control, label) {
|
|
1499
|
+
buildChildNode(schema, control, label, path) {
|
|
1305
1500
|
if (schema.kind === 'nodeGroup') {
|
|
1306
|
-
return control instanceof FormGroup ? this.buildTree(schema, control, label) : null;
|
|
1501
|
+
return control instanceof FormGroup ? this.buildTree(schema, control, label, path) : null;
|
|
1307
1502
|
}
|
|
1308
1503
|
if (schema.kind === 'nodeGroupList') {
|
|
1309
1504
|
const array = control;
|
|
@@ -1314,20 +1509,18 @@ class ConfigEditorComponent {
|
|
|
1314
1509
|
.map((item, i) => {
|
|
1315
1510
|
// Just "#n": the item sits under its list node, so repeating the
|
|
1316
1511
|
// item name (e.g. "Interface #1") only echoes the parent.
|
|
1317
|
-
const node = this.buildTree(schema.type, item, `#${i + 1}
|
|
1318
|
-
node.removable = {
|
|
1512
|
+
const node = this.buildTree(schema.type, item, `#${i + 1}`, this.join(path, String(i)));
|
|
1513
|
+
node.removable = { index: i };
|
|
1319
1514
|
return node;
|
|
1320
1515
|
})
|
|
1321
1516
|
: [];
|
|
1322
1517
|
return {
|
|
1323
|
-
id:
|
|
1518
|
+
id: path,
|
|
1324
1519
|
label,
|
|
1325
1520
|
children: items,
|
|
1326
|
-
leaves: [],
|
|
1327
|
-
leafLists: [],
|
|
1328
1521
|
group: null,
|
|
1329
1522
|
list: array instanceof FormArray
|
|
1330
|
-
? { array, itemSchema: schema.type, itemLabel, minItems: schema.minItems ?? 0 }
|
|
1523
|
+
? { array, itemSchema: schema.type, itemLabel, minItems: schema.minItems ?? 0, maxItems: schema.maxItems }
|
|
1331
1524
|
: undefined,
|
|
1332
1525
|
};
|
|
1333
1526
|
}
|
|
@@ -1336,8 +1529,9 @@ class ConfigEditorComponent {
|
|
|
1336
1529
|
return null;
|
|
1337
1530
|
const active = control.get(CASE_KEY)?.value;
|
|
1338
1531
|
const node = active && schema.cases[active]
|
|
1339
|
-
? this.buildTree(this.caseAsGroup(schema, active), control, label)
|
|
1340
|
-
: { id:
|
|
1532
|
+
? this.buildTree(this.caseAsGroup(schema, active), control, label, path)
|
|
1533
|
+
: { id: path, label, children: [], group: control };
|
|
1534
|
+
node.schema = undefined;
|
|
1341
1535
|
node.choice = { schema, group: control };
|
|
1342
1536
|
return node;
|
|
1343
1537
|
}
|
|
@@ -1351,36 +1545,57 @@ class ConfigEditorComponent {
|
|
|
1351
1545
|
const entries = complex
|
|
1352
1546
|
? Object.keys(control.controls)
|
|
1353
1547
|
.map((key) => {
|
|
1354
|
-
|
|
1355
|
-
|
|
1548
|
+
// Index access, not .get(): entry keys are arbitrary runtime data
|
|
1549
|
+
// and .get() would split a key like '10.0.0.1' into a dotted path.
|
|
1550
|
+
const entryNode = this.buildChildNode(schema.value, control.controls[key], key, this.join(path, key));
|
|
1551
|
+
if (entryNode) {
|
|
1356
1552
|
entryNode.mapEntry = { mapGroup: control, mapSchema: schema, key };
|
|
1553
|
+
}
|
|
1357
1554
|
return entryNode;
|
|
1358
1555
|
})
|
|
1359
1556
|
.filter((n) => n !== null)
|
|
1360
1557
|
: [];
|
|
1361
1558
|
return {
|
|
1362
|
-
id:
|
|
1559
|
+
id: path,
|
|
1363
1560
|
label,
|
|
1364
1561
|
children: entries,
|
|
1365
|
-
leaves: [],
|
|
1366
|
-
leafLists: [],
|
|
1367
1562
|
group: null,
|
|
1368
1563
|
map: { schema, group: control, complex },
|
|
1369
1564
|
};
|
|
1370
1565
|
}
|
|
1371
1566
|
return null;
|
|
1372
1567
|
}
|
|
1568
|
+
/** Synthetic group over a case's normalized fields, so a case body builds like any group. */
|
|
1569
|
+
caseAsGroup(choice, caseName) {
|
|
1570
|
+
return {
|
|
1571
|
+
kind: 'nodeGroup',
|
|
1572
|
+
name: choice.name,
|
|
1573
|
+
children: choice.cases[caseName] ? caseFields(choice.cases[caseName]) : {},
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1373
1576
|
/** A node's display label: its schema `label`, else its record key. */
|
|
1374
1577
|
labelOf(node, key) {
|
|
1375
1578
|
return ('label' in node ? node.label : undefined) ?? key;
|
|
1376
1579
|
}
|
|
1580
|
+
/** Join a parent path and a segment; the root's path is the empty string. */
|
|
1581
|
+
join(parent, segment) {
|
|
1582
|
+
const seg = this.escapeSeg(segment);
|
|
1583
|
+
return parent ? `${parent}/${seg}` : seg;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Escape a path segment: `/` is the id separator, and map entry keys are
|
|
1587
|
+
* arbitrary runtime data that may contain it. Labels stay unescaped — only
|
|
1588
|
+
* node identities encode.
|
|
1589
|
+
*/
|
|
1590
|
+
escapeSeg(segment) {
|
|
1591
|
+
return segment.replace(/%/g, '%25').replace(/\//g, '%2F');
|
|
1592
|
+
}
|
|
1377
1593
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1378
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: ConfigEditorComponent, isStandalone: true, selector: "nff-config-editor", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: true, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"editor\">\n <nav class=\"tree\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n [class.selected]=\"node === selected\"\n [class.error]=\"hasError(node)\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n >\n @if (hasExpandableContent(node)) {\n <button\n matIconButton\n class=\"twisty\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\n\n @if (editable() && node.list) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n matTooltip=\"Add entry\"\n (click)=\"addTreeMapEntry(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.removable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of pathTo(selected); track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </nav>\n\n @if (selected.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(selected)\" (selectionChange)=\"switchTreeCase(selected, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(selected) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (selected.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(selected, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (selected.map && !selected.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"selected.map.schema\"\n [formGroup]=\"selected.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (selected.group) {\n <div class=\"group-body\">\n <div class=\"fields\" [formGroup]=\"selected.group\">\n @for (leaf of selected.leaves; track leaf.key) {\n <nff-leaf-renderer\n [leaf_]=\"leaf.node\"\n [control]=\"asFormControl(selected.group.get(leaf.key))\"\n [editable]=\"editable()\"\n [removable]=\"!!leaf.optional\"\n [autofocus]=\"leaf.key === focusLeafKey\"\n (remove)=\"removeOptionalLeaf(selected, leaf)\"\n />\n }\n @for (list of selected.leafLists; track list.key) {\n <nff-leaf-list-renderer\n [leaf_]=\"list.node\"\n [formArray]=\"asFormArray(selected.group.get(list.key))\"\n [editable]=\"editable()\"\n />\n }\n @if (editable() && selected.optionals?.length) {\n <div class=\"detail-optional\">\n <button matButton [matMenuTriggerFor]=\"detailOptionalsMenu\">\n <mat-icon>add</mat-icon> Optional field\n </button>\n <mat-menu #detailOptionalsMenu=\"matMenu\">\n @for (opt of selected.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(selected, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n </div>\n }\n </div>\n @if (selected.children.length) {\n <nav class=\"child-links\">\n <h4 class=\"child-links-title\">Sections</h4>\n @for (child of selected.children; track child.id) {\n <button type=\"button\" class=\"item-link child-link\" [matTooltip]=\"child.label\" (click)=\"open(selected, child)\">\n <mat-icon>chevron_right</mat-icon>{{ child.label }}\n </button>\n }\n </nav>\n }\n </div>\n }\n\n @if (selected.list; as list) {\n @if (selected.children.length) {\n <ul class=\"item-list\">\n @for (item of selected.children; track item.id) {\n <li class=\"item-row\">\n <button type=\"button\" class=\"item-link\" [matTooltip]=\"item.label\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable() && item.removable) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No {{ list.itemLabel }} items yet.</p>\n }\n @if (editable()) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addItem(selected)\">\n <mat-icon>add</mat-icon> Add {{ list.itemLabel }}\n </button>\n </div>\n }\n }\n\n @if (selected.map && selected.map.complex) {\n @if (selected.children.length) {\n <ul class=\"item-list\">\n @for (item of selected.children; track item.id) {\n <li class=\"item-row\">\n <button type=\"button\" class=\"item-link\" [matTooltip]=\"item.label\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable() && !mapAtMin(selected)) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No entries yet.</p>\n }\n @if (editable() && !mapAtMax(selected)) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addTreeMapEntry(selected)\">\n <mat-icon>add</mat-icon> Add entry\n </button>\n </div>\n }\n }\n\n @if (\n !selected.choice &&\n !selected.mapEntry &&\n !selected.map &&\n !selected.list &&\n !selected.leaves.length &&\n !selected.leafLists.length &&\n !selected.children.length &&\n !(editable() && selected.optionals?.length)\n ) {\n <p class=\"empty\">This node has no fields.</p>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 360px;min-width:360px;overflow:auto;border-right:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12));padding:4px 12px 4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high, rgba(0, 0, 0, .04))}.tree-row.selected{background:var(--mat-sys-secondary-container, rgba(103, 80, 164, .12));font-weight:600}.tree-row.error .tree-label{color:var(--mat-sys-error, #b3261e)}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55))}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary, #6750a4)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55));font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .5))}.fields{display:flex;flex-direction:column;gap:4px}.case-select,.key-field{display:block;max-width:320px;margin-bottom:8px}.detail-optional{display:flex;justify-content:flex-start}.empty{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6));font-style:italic}.item-list{list-style:none;margin:0 0 8px;padding:0}.item-row{display:flex;align-items:center;gap:4px}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary, #6750a4);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}.remove-item{flex:0 0 auto}.list-actions{display:flex;justify-content:flex-end}.group-body{display:flex;align-items:flex-start;gap:24px}.group-body .fields{flex:1 1 auto;min-width:0}.child-links{flex:0 0 200px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding-left:16px;border-left:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12))}.child-links-title{margin:0 0 4px;font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6))}.child-link{display:inline-flex;align-items:center;gap:2px}.child-link mat-icon{font-size:18px;width:18px;height:18px}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i4$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: LeafRendererComponent, selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "initialValue", "removable", "editable", "autofocus"], outputs: ["remove"] }, { kind: "component", type: LeafListRendererComponent, selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems"], outputs: ["message"] }, { kind: "component", type: NodeMapRendererComponent, selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable"] }] });
|
|
1594
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: ConfigEditorComponent, isStandalone: true, selector: "nff-config-editor", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: true, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"editor\">\n <nav class=\"tree\" role=\"tree\" aria-label=\"Configuration structure\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n role=\"treeitem\"\n tabindex=\"0\"\n [class.selected]=\"node === selected\"\n [class.error]=\"hasError(node)\"\n [attr.aria-selected]=\"node === selected\"\n [attr.aria-expanded]=\"hasExpandableContent(node) ? expanded.has(node.id) : null\"\n [attr.aria-level]=\"depth + 1\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n (keydown)=\"onRowKeydown($event, node)\"\n >\n @if (hasExpandableContent(node)) {\n <button\n matIconButton\n class=\"twisty\"\n tabindex=\"-1\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\n @if (hasError(node)) {\n <!-- A non-color error signal beside the red row text. -->\n <mat-icon class=\"row-error-icon\" aria-hidden=\"false\" role=\"img\" aria-label=\"Has validation errors\"\n >error_outline</mat-icon\n >\n }\n\n @if (editable() && node.list && !listAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel\"\n [attr.aria-label]=\"'Add ' + node.list.itemLabel\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n matTooltip=\"Add entry\"\n [attr.aria-label]=\"'Add ' + node.label + ' entry'\"\n (click)=\"addTreeMapEntry(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.removable && !listAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove entry\"\n [attr.aria-label]=\"'Remove ' + node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of breadcrumb; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </nav>\n\n <!-- The selected subtree as a flat section list: each descendant's fields\n under a breadcrumb heading \u2014 the headings, not nesting, mark the\n boundary between one child and the next. -->\n @for (s of sections; track s.node.id) {\n @if (s.trail.length > 1) {\n <!-- A heading, not a nav landmark: dozens of identical \"Section\"\n landmarks would flood the assistive-tech landmark list. Its\n accessible name is the trail itself. -->\n <div class=\"section-heading\" role=\"heading\" aria-level=\"3\">\n @for (crumb of s.trail; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </div>\n }\n\n @if (s.node.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(s.node, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (s.node.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(s.node)\" (selectionChange)=\"switchTreeCase(s.node, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(s.node) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (s.schema && s.group) {\n <nff-dynamic-recursive-form\n [schema]=\"s.schema\"\n [formGroup]=\"s.group\"\n [editable]=\"editable()\"\n [focusLeaf]=\"s.node.id === focusSectionId ? focusLeafKey : null\"\n />\n }\n\n @if (s.node.map && !s.node.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"s.node.map.schema\"\n [formGroup]=\"s.node.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (editable() && s.node.map?.complex && !mapAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addTreeMapEntry(s.node)\"><mat-icon>add</mat-icon> Add entry</button>\n </div>\n }\n @if (editable() && s.node.list && !listAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addItem(s.node)\">\n <mat-icon>add</mat-icon> Add {{ s.node.list.itemLabel }}\n </button>\n </div>\n }\n @if (emptySectionHint(s); as hint) {\n <p class=\"empty\">{{ hint }}</p>\n }\n }\n\n @if (\n sections.length === 1 &&\n !sections[0].schema &&\n !sections[0].node.choice &&\n !sections[0].node.map &&\n !sections[0].node.list &&\n !sections[0].node.mapEntry\n ) {\n <p class=\"empty\">This node has no fields.</p>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 360px;min-width:360px;overflow:auto;border-right:1px solid var(--mat-sys-outline-variant);padding:4px 12px 4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high)}.tree-row.selected{background:var(--mat-sys-secondary-container);font-weight:600}.tree-row.error .tree-label{color:var(--mat-sys-error)}.row-error-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-error)}.tree-row:focus-visible{outline:2px solid var(--mat-sys-primary);outline-offset:-2px}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant)}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;color:var(--mat-sys-on-surface-variant);font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.row-btn.add{visibility:visible}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove,.tree-row:focus-within .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant)}.section-heading{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:20px 0 8px;padding-top:12px;border-top:1px solid var(--mat-sys-outline-variant);font-size:.95rem}.section-heading .crumb-current{font-weight:600}.key-field,.case-select{display:block;max-width:320px;margin-bottom:8px}.section-actions{display:flex;justify-content:flex-start;margin-bottom:8px}.empty{color:var(--mat-sys-on-surface-variant);font-style:italic}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DynamicRecursiveFormComponent, selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "addButtonCallback", "focusLeaf"], outputs: ["remove", "editableChange"] }, { kind: "component", type: NodeMapRendererComponent, selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable"] }] });
|
|
1379
1595
|
}
|
|
1380
1596
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, decorators: [{
|
|
1381
1597
|
type: Component,
|
|
1382
1598
|
args: [{ selector: 'nff-config-editor', standalone: true, imports: [
|
|
1383
|
-
ReactiveFormsModule,
|
|
1384
1599
|
NgTemplateOutlet,
|
|
1385
1600
|
MatIconModule,
|
|
1386
1601
|
MatButtonModule,
|
|
@@ -1389,11 +1604,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
1389
1604
|
MatInputModule,
|
|
1390
1605
|
MatSelectModule,
|
|
1391
1606
|
MatTooltip,
|
|
1392
|
-
|
|
1393
|
-
LeafListRendererComponent,
|
|
1607
|
+
DynamicRecursiveFormComponent,
|
|
1394
1608
|
NodeMapRendererComponent,
|
|
1395
|
-
], template: "<div class=\"editor\">\n <nav class=\"tree\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n [class.selected]=\"node === selected\"\n [class.error]=\"hasError(node)\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n >\n @if (hasExpandableContent(node)) {\n <button\n matIconButton\n class=\"twisty\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\n\n @if (editable() && node.list) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n matTooltip=\"Add entry\"\n (click)=\"addTreeMapEntry(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.removable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of pathTo(selected); track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </nav>\n\n @if (selected.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(selected)\" (selectionChange)=\"switchTreeCase(selected, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(selected) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (selected.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(selected, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (selected.map && !selected.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"selected.map.schema\"\n [formGroup]=\"selected.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (selected.group) {\n <div class=\"group-body\">\n <div class=\"fields\" [formGroup]=\"selected.group\">\n @for (leaf of selected.leaves; track leaf.key) {\n <nff-leaf-renderer\n [leaf_]=\"leaf.node\"\n [control]=\"asFormControl(selected.group.get(leaf.key))\"\n [editable]=\"editable()\"\n [removable]=\"!!leaf.optional\"\n [autofocus]=\"leaf.key === focusLeafKey\"\n (remove)=\"removeOptionalLeaf(selected, leaf)\"\n />\n }\n @for (list of selected.leafLists; track list.key) {\n <nff-leaf-list-renderer\n [leaf_]=\"list.node\"\n [formArray]=\"asFormArray(selected.group.get(list.key))\"\n [editable]=\"editable()\"\n />\n }\n @if (editable() && selected.optionals?.length) {\n <div class=\"detail-optional\">\n <button matButton [matMenuTriggerFor]=\"detailOptionalsMenu\">\n <mat-icon>add</mat-icon> Optional field\n </button>\n <mat-menu #detailOptionalsMenu=\"matMenu\">\n @for (opt of selected.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(selected, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n </div>\n }\n </div>\n @if (selected.children.length) {\n <nav class=\"child-links\">\n <h4 class=\"child-links-title\">Sections</h4>\n @for (child of selected.children; track child.id) {\n <button type=\"button\" class=\"item-link child-link\" [matTooltip]=\"child.label\" (click)=\"open(selected, child)\">\n <mat-icon>chevron_right</mat-icon>{{ child.label }}\n </button>\n }\n </nav>\n }\n </div>\n }\n\n @if (selected.list; as list) {\n @if (selected.children.length) {\n <ul class=\"item-list\">\n @for (item of selected.children; track item.id) {\n <li class=\"item-row\">\n <button type=\"button\" class=\"item-link\" [matTooltip]=\"item.label\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable() && item.removable) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove\"\n (click)=\"removeItem(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No {{ list.itemLabel }} items yet.</p>\n }\n @if (editable()) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addItem(selected)\">\n <mat-icon>add</mat-icon> Add {{ list.itemLabel }}\n </button>\n </div>\n }\n }\n\n @if (selected.map && selected.map.complex) {\n @if (selected.children.length) {\n <ul class=\"item-list\">\n @for (item of selected.children; track item.id) {\n <li class=\"item-row\">\n <button type=\"button\" class=\"item-link\" [matTooltip]=\"item.label\" (click)=\"open(selected, item)\">{{ item.label }}</button>\n @if (editable() && !mapAtMin(selected)) {\n <button\n matIconButton\n class=\"row-btn remove-item\"\n matTooltip=\"Remove entry\"\n (click)=\"removeTreeMapEntry(selected, item)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"empty\">No entries yet.</p>\n }\n @if (editable() && !mapAtMax(selected)) {\n <div class=\"list-actions\">\n <button matButton (click)=\"addTreeMapEntry(selected)\">\n <mat-icon>add</mat-icon> Add entry\n </button>\n </div>\n }\n }\n\n @if (\n !selected.choice &&\n !selected.mapEntry &&\n !selected.map &&\n !selected.list &&\n !selected.leaves.length &&\n !selected.leafLists.length &&\n !selected.children.length &&\n !(editable() && selected.optionals?.length)\n ) {\n <p class=\"empty\">This node has no fields.</p>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 360px;min-width:360px;overflow:auto;border-right:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12));padding:4px 12px 4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high, rgba(0, 0, 0, .04))}.tree-row.selected{background:var(--mat-sys-secondary-container, rgba(103, 80, 164, .12));font-weight:600}.tree-row.error .tree-label{color:var(--mat-sys-error, #b3261e)}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55))}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary, #6750a4)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .55));font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .5))}.fields{display:flex;flex-direction:column;gap:4px}.case-select,.key-field{display:block;max-width:320px;margin-bottom:8px}.detail-optional{display:flex;justify-content:flex-start}.empty{color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6));font-style:italic}.item-list{list-style:none;margin:0 0 8px;padding:0}.item-row{display:flex;align-items:center;gap:4px}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary, #6750a4);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}.remove-item{flex:0 0 auto}.list-actions{display:flex;justify-content:flex-end}.group-body{display:flex;align-items:flex-start;gap:24px}.group-body .fields{flex:1 1 auto;min-width:0}.child-links{flex:0 0 200px;display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding-left:16px;border-left:1px solid var(--mat-sys-outline-variant, rgba(0, 0, 0, .12))}.child-links-title{margin:0 0 4px;font-size:.75rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--mat-sys-on-surface-variant, rgba(0, 0, 0, .6))}.child-link{display:inline-flex;align-items:center;gap:2px}.child-link mat-icon{font-size:18px;width:18px;height:18px}\n"] }]
|
|
1396
|
-
}], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: true }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }] } });
|
|
1609
|
+
], template: "<div class=\"editor\">\n <nav class=\"tree\" role=\"tree\" aria-label=\"Configuration structure\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n role=\"treeitem\"\n tabindex=\"0\"\n [class.selected]=\"node === selected\"\n [class.error]=\"hasError(node)\"\n [attr.aria-selected]=\"node === selected\"\n [attr.aria-expanded]=\"hasExpandableContent(node) ? expanded.has(node.id) : null\"\n [attr.aria-level]=\"depth + 1\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n (keydown)=\"onRowKeydown($event, node)\"\n >\n @if (hasExpandableContent(node)) {\n <button\n matIconButton\n class=\"twisty\"\n tabindex=\"-1\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\n @if (hasError(node)) {\n <!-- A non-color error signal beside the red row text. -->\n <mat-icon class=\"row-error-icon\" aria-hidden=\"false\" role=\"img\" aria-label=\"Has validation errors\"\n >error_outline</mat-icon\n >\n }\n\n @if (editable() && node.list && !listAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel\"\n [attr.aria-label]=\"'Add ' + node.list.itemLabel\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n matTooltip=\"Add entry\"\n [attr.aria-label]=\"'Add ' + node.label + ' entry'\"\n (click)=\"addTreeMapEntry(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.removable && !listAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n matTooltip=\"Remove entry\"\n [attr.aria-label]=\"'Remove ' + node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of breadcrumb; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </nav>\n\n <!-- The selected subtree as a flat section list: each descendant's fields\n under a breadcrumb heading \u2014 the headings, not nesting, mark the\n boundary between one child and the next. -->\n @for (s of sections; track s.node.id) {\n @if (s.trail.length > 1) {\n <!-- A heading, not a nav landmark: dozens of identical \"Section\"\n landmarks would flood the assistive-tech landmark list. Its\n accessible name is the trail itself. -->\n <div class=\"section-heading\" role=\"heading\" aria-level=\"3\">\n @for (crumb of s.trail; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n </div>\n }\n\n @if (s.node.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(s.node, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (s.node.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(s.node)\" (selectionChange)=\"switchTreeCase(s.node, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(s.node) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (s.schema && s.group) {\n <nff-dynamic-recursive-form\n [schema]=\"s.schema\"\n [formGroup]=\"s.group\"\n [editable]=\"editable()\"\n [focusLeaf]=\"s.node.id === focusSectionId ? focusLeafKey : null\"\n />\n }\n\n @if (s.node.map && !s.node.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"s.node.map.schema\"\n [formGroup]=\"s.node.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (editable() && s.node.map?.complex && !mapAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addTreeMapEntry(s.node)\"><mat-icon>add</mat-icon> Add entry</button>\n </div>\n }\n @if (editable() && s.node.list && !listAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addItem(s.node)\">\n <mat-icon>add</mat-icon> Add {{ s.node.list.itemLabel }}\n </button>\n </div>\n }\n @if (emptySectionHint(s); as hint) {\n <p class=\"empty\">{{ hint }}</p>\n }\n }\n\n @if (\n sections.length === 1 &&\n !sections[0].schema &&\n !sections[0].node.choice &&\n !sections[0].node.map &&\n !sections[0].node.list &&\n !sections[0].node.mapEntry\n ) {\n <p class=\"empty\">This node has no fields.</p>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;gap:16px;min-height:320px}.tree{flex:0 0 360px;min-width:360px;overflow:auto;border-right:1px solid var(--mat-sys-outline-variant);padding:4px 12px 4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high)}.tree-row.selected{background:var(--mat-sys-secondary-container);font-weight:600}.tree-row.error .tree-label{color:var(--mat-sys-error)}.row-error-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-error)}.tree-row:focus-visible{outline:2px solid var(--mat-sys-primary);outline-offset:-2px}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant)}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;color:var(--mat-sys-on-surface-variant);font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.row-btn.add{visibility:visible}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove,.tree-row:focus-within .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant)}.section-heading{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:20px 0 8px;padding-top:12px;border-top:1px solid var(--mat-sys-outline-variant);font-size:.95rem}.section-heading .crumb-current{font-weight:600}.key-field,.case-select{display:block;max-width:320px;margin-bottom:8px}.section-actions{display:flex;justify-content:flex-start;margin-bottom:8px}.empty{color:var(--mat-sys-on-surface-variant);font-style:italic}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}\n"] }]
|
|
1610
|
+
}], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: true }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }] } });
|
|
1397
1611
|
|
|
1398
1612
|
/*
|
|
1399
1613
|
* Public API Surface of ng-form-foundry
|