ng-form-foundry 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/fesm2022/ng-form-foundry.mjs +603 -335
- package/fesm2022/ng-form-foundry.mjs.map +1 -1
- package/index.d.ts +272 -146
- 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,73 @@ 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
|
*/
|
|
469
|
+
function buildFormFromSchema(schema, initial = null) {
|
|
470
|
+
return buildNodeGroupControl(schema, initial);
|
|
471
|
+
}
|
|
370
472
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* is
|
|
473
|
+
* The wire value at `node`: `value` rebuilt with every choice discriminator
|
|
474
|
+
* removed.
|
|
475
|
+
*
|
|
476
|
+
* A choice's form value is `{ __case, ...fields }` ({@link CASE_KEY}); its wire
|
|
477
|
+
* encoding is the active case's fields inline, with no discriminator — the case
|
|
478
|
+
* is recovered from the field shape when the data is seeded back in
|
|
479
|
+
* ({@link resolveChoiceCase}). The walk is schema-driven: only positions the
|
|
480
|
+
* schema declares as choices are stripped, so a group child or map entry that
|
|
481
|
+
* happens to be named `__case` passes through untouched. Values at positions
|
|
482
|
+
* the schema does not describe pass through unchanged.
|
|
376
483
|
*/
|
|
377
|
-
function
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (child.kind !== 'nodeGroup')
|
|
387
|
-
continue;
|
|
388
|
-
if (child.presence && childInitial == null) {
|
|
389
|
-
group.removeControl(key);
|
|
484
|
+
function toWireValue(node, value) {
|
|
485
|
+
if (value === null || typeof value !== 'object')
|
|
486
|
+
return value;
|
|
487
|
+
if (isChoice(node)) {
|
|
488
|
+
const { [CASE_KEY]: active, ...rest } = value;
|
|
489
|
+
const fields = typeof active === 'string' && node.cases[active] ? caseFields(node.cases[active]) : {};
|
|
490
|
+
const out = {};
|
|
491
|
+
for (const key of Object.keys(rest)) {
|
|
492
|
+
out[key] = key in fields ? toWireValue(fields[key], rest[key]) : rest[key];
|
|
390
493
|
}
|
|
391
|
-
|
|
392
|
-
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
if (isNodeGroup(node)) {
|
|
497
|
+
const source = value;
|
|
498
|
+
const out = {};
|
|
499
|
+
for (const key of Object.keys(source)) {
|
|
500
|
+
out[key] = key in node.children ? toWireValue(node.children[key], source[key]) : source[key];
|
|
393
501
|
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
if (isNodeGroupList(node)) {
|
|
505
|
+
return Array.isArray(value) ? value.map((item) => toWireValue(node.type, item)) : value;
|
|
506
|
+
}
|
|
507
|
+
if (isMap(node)) {
|
|
508
|
+
const source = value;
|
|
509
|
+
const out = {};
|
|
510
|
+
for (const key of Object.keys(source))
|
|
511
|
+
out[key] = toWireValue(node.value, source[key]);
|
|
512
|
+
return out;
|
|
394
513
|
}
|
|
514
|
+
return value;
|
|
395
515
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Serialize a form built by {@link buildFormFromSchema} to its wire value:
|
|
518
|
+
* `form.getRawValue()` with every choice's {@link CASE_KEY} discriminator
|
|
519
|
+
* stripped (see {@link toWireValue}). The result is the inline encoding that
|
|
520
|
+
* `buildFormFromSchema` accepts back as `initial`, so serialize → rebuild
|
|
521
|
+
* round-trips the value.
|
|
522
|
+
*/
|
|
523
|
+
function serializeForm(schema, form) {
|
|
524
|
+
return toWireValue(schema, form.getRawValue());
|
|
400
525
|
}
|
|
401
526
|
/**
|
|
402
527
|
* Capture a schema literal with its exact type while checking it against
|
|
@@ -423,11 +548,11 @@ class LeafEnumRendererComponent {
|
|
|
423
548
|
remove;
|
|
424
549
|
editable = true;
|
|
425
550
|
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=\"{{
|
|
551
|
+
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
552
|
}
|
|
428
553
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafEnumRendererComponent, decorators: [{
|
|
429
554
|
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=\"{{
|
|
555
|
+
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
556
|
}], propDecorators: { leafEnum: [{
|
|
432
557
|
type: Input
|
|
433
558
|
}], initialValue: [{
|
|
@@ -446,20 +571,15 @@ class LeafRendererComponent {
|
|
|
446
571
|
elementRef;
|
|
447
572
|
leaf_;
|
|
448
573
|
control = new FormControl();
|
|
449
|
-
initialValue;
|
|
450
574
|
removable = false;
|
|
451
575
|
editable = true;
|
|
452
576
|
/** Focus the field once rendered — for fields the user just added (e.g. an enabled presence leaf). */
|
|
453
577
|
autofocus = false;
|
|
454
|
-
|
|
578
|
+
/** Emitted when the user removes this field (e.g. an optional presence leaf). */
|
|
579
|
+
remove = output();
|
|
455
580
|
constructor(elementRef) {
|
|
456
581
|
this.elementRef = elementRef;
|
|
457
582
|
}
|
|
458
|
-
ngOnInit() {
|
|
459
|
-
if (this.initialValue) {
|
|
460
|
-
this.control.patchValue(this.initialValue);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
583
|
ngAfterViewInit() {
|
|
464
584
|
if (this.autofocus) {
|
|
465
585
|
// Deferred out of the change-detection pass: focusing flips the form
|
|
@@ -504,7 +624,7 @@ class LeafRendererComponent {
|
|
|
504
624
|
return 'Invalid value';
|
|
505
625
|
}
|
|
506
626
|
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",
|
|
627
|
+
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
628
|
}
|
|
509
629
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, decorators: [{
|
|
510
630
|
type: Component,
|
|
@@ -518,22 +638,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
518
638
|
MatButtonModule,
|
|
519
639
|
MatTooltip,
|
|
520
640
|
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(
|
|
641
|
+
], 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
642
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { leaf_: [{
|
|
523
643
|
type: Input
|
|
524
644
|
}], control: [{
|
|
525
645
|
type: Input
|
|
526
|
-
}], initialValue: [{
|
|
527
|
-
type: Input
|
|
528
646
|
}], removable: [{
|
|
529
647
|
type: Input
|
|
530
648
|
}], editable: [{
|
|
531
649
|
type: Input
|
|
532
650
|
}], autofocus: [{
|
|
533
651
|
type: Input
|
|
534
|
-
}], remove: [{
|
|
535
|
-
type: Output
|
|
536
|
-
}] } });
|
|
652
|
+
}], remove: [{ type: i0.Output, args: ["remove"] }] } });
|
|
537
653
|
|
|
538
654
|
class NodeGroupListRendererComponent {
|
|
539
655
|
nodeGroupList;
|
|
@@ -597,7 +713,7 @@ class NodeGroupListRendererComponent {
|
|
|
597
713
|
}
|
|
598
714
|
}
|
|
599
715
|
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) }] });
|
|
716
|
+
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
717
|
}
|
|
602
718
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, decorators: [{
|
|
603
719
|
type: Component,
|
|
@@ -639,7 +755,7 @@ class AnonLeafRendererComponent {
|
|
|
639
755
|
this.remove.emit();
|
|
640
756
|
}
|
|
641
757
|
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 [
|
|
758
|
+
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
759
|
}
|
|
644
760
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AnonLeafRendererComponent, decorators: [{
|
|
645
761
|
type: Component,
|
|
@@ -651,7 +767,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
651
767
|
MatCheckboxModule,
|
|
652
768
|
MatInputModule,
|
|
653
769
|
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 [
|
|
770
|
+
], 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
771
|
}], propDecorators: { AnonLeaf: [{
|
|
656
772
|
type: Input
|
|
657
773
|
}], initialValue: [{
|
|
@@ -706,7 +822,7 @@ class LeafListRendererComponent {
|
|
|
706
822
|
this.formArray.push(new FormControl());
|
|
707
823
|
}
|
|
708
824
|
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:
|
|
825
|
+
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
826
|
}
|
|
711
827
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
|
|
712
828
|
type: Component,
|
|
@@ -715,7 +831,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
715
831
|
AnonLeafRendererComponent,
|
|
716
832
|
MatButtonModule,
|
|
717
833
|
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"] }]
|
|
834
|
+
], 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
835
|
}], propDecorators: { leaf_: [{
|
|
720
836
|
type: Input
|
|
721
837
|
}], initialValue: [{
|
|
@@ -733,37 +849,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
733
849
|
args: ['class.stacked']
|
|
734
850
|
}] } });
|
|
735
851
|
|
|
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
852
|
function asFormControl(control) {
|
|
768
853
|
return control;
|
|
769
854
|
}
|
|
@@ -787,8 +872,12 @@ class NodeMapRendererComponent {
|
|
|
787
872
|
editable = true;
|
|
788
873
|
/** Ordered view of entry keys, kept stable across renames (`addControl` appends). */
|
|
789
874
|
entryKeys = [];
|
|
790
|
-
|
|
791
|
-
|
|
875
|
+
ngOnChanges(changes) {
|
|
876
|
+
// Re-sync whenever the bound group changes: a host may rebind the renderer
|
|
877
|
+
// to another map (e.g. the tree editor swapping documents) while this
|
|
878
|
+
// component instance survives.
|
|
879
|
+
if (changes['formGroup'])
|
|
880
|
+
this.entryKeys = Object.keys(this.formGroup.controls);
|
|
792
881
|
}
|
|
793
882
|
/** The value schema as a leaf (used when `value.kind === 'leaf'`). */
|
|
794
883
|
get valueLeaf() {
|
|
@@ -830,7 +919,7 @@ class NodeMapRendererComponent {
|
|
|
830
919
|
asFormControl = asFormControl;
|
|
831
920
|
asFormGroup = asFormGroup;
|
|
832
921
|
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.
|
|
922
|
+
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
923
|
}
|
|
835
924
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, decorators: [{
|
|
836
925
|
type: Component,
|
|
@@ -843,7 +932,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
843
932
|
// node-map-renderer and dynamic-recursive-form import each other (a map value
|
|
844
933
|
// may be a group); forwardRef defers the reference to break the cycle.
|
|
845
934
|
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.
|
|
935
|
+
], 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
936
|
}], propDecorators: { nodeMap: [{
|
|
848
937
|
type: Input
|
|
849
938
|
}], formGroup: [{
|
|
@@ -871,12 +960,26 @@ class DynamicRecursiveFormComponent {
|
|
|
871
960
|
editable = model(false, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
872
961
|
/** Invoked with {@link index} to append a new sibling form to a parent list. */
|
|
873
962
|
addButtonCallback = input(null, ...(ngDevMode ? [{ debugName: "addButtonCallback" }] : []));
|
|
963
|
+
/**
|
|
964
|
+
* Key of a presence leaf whose field should grab focus when it renders — lets
|
|
965
|
+
* a host that added the control itself (e.g. the tree editor's optionals
|
|
966
|
+
* menu) hand focus to the new field, like the form's own add button does.
|
|
967
|
+
*/
|
|
968
|
+
focusLeaf = input(null, ...(ngDevMode ? [{ debugName: "focusLeaf" }] : []));
|
|
874
969
|
/** True when the schema is a root group (rendered flat, without a wrapping card). */
|
|
875
970
|
root = computed(() => this.schema().root ?? false, ...(ngDevMode ? [{ debugName: "root" }] : []));
|
|
876
971
|
ngOnInit() {
|
|
877
972
|
const initial = this.initialValue();
|
|
878
973
|
if (initial) {
|
|
879
|
-
this.formGroup()
|
|
974
|
+
const group = this.formGroup();
|
|
975
|
+
// Presence keys carried by the initial value need controls first:
|
|
976
|
+
// patchValue silently skips keys that have none, dropping the data.
|
|
977
|
+
for (const [key, child] of Object.entries(this.schema().children ?? {})) {
|
|
978
|
+
if ('presence' in child && child.presence && key in initial && !group.get(key)) {
|
|
979
|
+
group.addControl(key, buildControl(child, initial[key]));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
group.patchValue(initial);
|
|
880
983
|
}
|
|
881
984
|
}
|
|
882
985
|
get nodeGroupChildrenList() {
|
|
@@ -889,59 +992,44 @@ class DynamicRecursiveFormComponent {
|
|
|
889
992
|
emitRemoveEvent() {
|
|
890
993
|
this.remove.emit();
|
|
891
994
|
}
|
|
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
995
|
/** The key of the presence leaf the user just enabled; its field grabs focus when rendered. */
|
|
908
996
|
presenceFocusKey = null;
|
|
909
997
|
/**
|
|
910
|
-
* Add or remove a presence
|
|
911
|
-
* leaf
|
|
912
|
-
*
|
|
998
|
+
* Add or remove a presence node's control on this form — any presence kind:
|
|
999
|
+
* group, leaf, map, or choice. Removing it drops the key from `form.value`;
|
|
1000
|
+
* adding it builds the control fresh from its schema (nested presence
|
|
1001
|
+
* children start absent).
|
|
913
1002
|
*/
|
|
914
|
-
|
|
1003
|
+
toggleNodePresence(key, schema, present) {
|
|
915
1004
|
const group = this.formGroup();
|
|
916
1005
|
if (present) {
|
|
917
1006
|
if (!group.get(key)) {
|
|
918
1007
|
group.addControl(key, buildControl(schema));
|
|
919
|
-
this.presenceFocusKey = key;
|
|
920
1008
|
}
|
|
921
1009
|
}
|
|
922
1010
|
else if (group.get(key)) {
|
|
923
1011
|
group.removeControl(key);
|
|
924
|
-
if (this.presenceFocusKey === key)
|
|
925
|
-
this.presenceFocusKey = null;
|
|
926
1012
|
}
|
|
927
1013
|
}
|
|
928
1014
|
/**
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* map, or a choice group holding `__case`) from its schema.
|
|
1015
|
+
* {@link toggleNodePresence} for a presence leaf, additionally focusing the
|
|
1016
|
+
* rendered field when the toggle just created it.
|
|
932
1017
|
*/
|
|
933
|
-
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1018
|
+
toggleLeafPresence(key, schema, present) {
|
|
1019
|
+
const had = !!this.formGroup().get(key);
|
|
1020
|
+
this.toggleNodePresence(key, schema, present);
|
|
1021
|
+
if (present && !had) {
|
|
1022
|
+
this.presenceFocusKey = key;
|
|
1023
|
+
// Retire the request once the field has rendered and taken focus, so a
|
|
1024
|
+
// later re-creation of the same renderer cannot steal focus again.
|
|
1025
|
+
setTimeout(() => {
|
|
1026
|
+
if (this.presenceFocusKey === key)
|
|
1027
|
+
this.presenceFocusKey = null;
|
|
1028
|
+
});
|
|
942
1029
|
}
|
|
1030
|
+
if (!present && this.presenceFocusKey === key)
|
|
1031
|
+
this.presenceFocusKey = null;
|
|
943
1032
|
}
|
|
944
|
-
CASE_KEY = CASE_KEY;
|
|
945
1033
|
objectKeys(obj) {
|
|
946
1034
|
return Object.keys(obj);
|
|
947
1035
|
}
|
|
@@ -978,7 +1066,7 @@ class DynamicRecursiveFormComponent {
|
|
|
978
1066
|
asFormArray = asFormArray;
|
|
979
1067
|
asFormControl = asFormControl;
|
|
980
1068
|
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"] }] });
|
|
1069
|
+
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
1070
|
}
|
|
983
1071
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
|
|
984
1072
|
type: Component,
|
|
@@ -995,27 +1083,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
995
1083
|
MatIconModule,
|
|
996
1084
|
MatButtonModule,
|
|
997
1085
|
NgTemplateOutlet,
|
|
998
|
-
MatCardModule,
|
|
999
1086
|
MatCheckboxModule,
|
|
1000
1087
|
MatFormFieldModule,
|
|
1001
1088
|
MatSelectModule,
|
|
1002
1089
|
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 }] }] } });
|
|
1090
|
+
], 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"] }]
|
|
1091
|
+
}], 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
1092
|
|
|
1006
1093
|
/**
|
|
1007
1094
|
* 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
|
-
*
|
|
1095
|
+
* maps, choices) is a tree on the left, and selecting a node renders that
|
|
1096
|
+
* node's **entire subtree** on the right as a **flat list of sections** — the
|
|
1097
|
+
* node's own fields first, then every descendant's fields, each separated by a
|
|
1098
|
+
* breadcrumb heading (`Service / Deploy scope / …`) instead of nested panels.
|
|
1099
|
+
* Leaf fields render through {@link DynamicRecursiveFormComponent} with a
|
|
1100
|
+
* leaf-only schema slice; choice selectors, map rows, and add controls render
|
|
1101
|
+
* inline in their section. The tree adds row conveniences of its own: `+` on
|
|
1102
|
+
* list and map rows, a delete control on removable rows, and a
|
|
1103
|
+
* "+ Optional field" menu for absent presence children.
|
|
1104
|
+
*
|
|
1105
|
+
* The tree is **derived state**: any structural change to the form — made
|
|
1106
|
+
* through the tree rows or through the detail sections — triggers a rebuild (a
|
|
1107
|
+
* cheap shape signature over `valueChanges` detects it). Node ids are stable
|
|
1108
|
+
* paths, so expansion and selection survive rebuilds. Swapping the `schema` or
|
|
1109
|
+
* `formGroup` input rebinds the editor to the new pair, resetting expansion
|
|
1110
|
+
* and selection.
|
|
1013
1111
|
*
|
|
1014
1112
|
* 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.
|
|
1113
|
+
* detail panes — so the embedding client owns the surrounding chrome.
|
|
1019
1114
|
*/
|
|
1020
1115
|
class ConfigEditorComponent {
|
|
1021
1116
|
/** The form-description schema whose structure the tree renders. */
|
|
@@ -1026,31 +1121,63 @@ class ConfigEditorComponent {
|
|
|
1026
1121
|
editable = input(true, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
1027
1122
|
root;
|
|
1028
1123
|
selected = null;
|
|
1124
|
+
/** The selected subtree as a flat, breadcrumb-separated section list. */
|
|
1125
|
+
sections = [];
|
|
1126
|
+
/** Root-to-selection trail for the detail-pane breadcrumb, computed once per selection. */
|
|
1127
|
+
breadcrumb = [];
|
|
1029
1128
|
expanded = new Set();
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1129
|
+
shape = '';
|
|
1130
|
+
changes;
|
|
1131
|
+
host = inject(ElementRef);
|
|
1132
|
+
/** Stable per-instance ids for the shape signature, so replacing a control (setControl) reads as a structural change. */
|
|
1133
|
+
controlIds = new WeakMap();
|
|
1134
|
+
controlIdSeq = 0;
|
|
1135
|
+
constructor() {
|
|
1136
|
+
// Rebind whenever the schema or form inputs change (a host loading another
|
|
1137
|
+
// config document): derived state resets against the new pair. Wrapped in
|
|
1138
|
+
// untracked so only the two inputs re-trigger the effect.
|
|
1139
|
+
effect(() => {
|
|
1140
|
+
const schema = this.schema();
|
|
1141
|
+
const group = this.formGroup();
|
|
1142
|
+
untracked(() => this.attach(schema, group));
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
ngOnDestroy() {
|
|
1146
|
+
this.changes?.unsubscribe();
|
|
1147
|
+
}
|
|
1148
|
+
/** Bind the editor to a schema/form pair: fresh tree, root selection, and shape-sync subscription. */
|
|
1149
|
+
attach(schema, group) {
|
|
1150
|
+
this.changes?.unsubscribe();
|
|
1151
|
+
this.expanded.clear();
|
|
1152
|
+
this.root = this.buildTree(schema, group, schema.label ?? schema.name, '');
|
|
1153
|
+
this.shape = this.shapeOf(group);
|
|
1034
1154
|
this.expanded.add(this.root.id);
|
|
1035
1155
|
this.select(this.root);
|
|
1156
|
+
// The detail sections mutate the FormGroup directly (presence toggles,
|
|
1157
|
+
// list items, map entries, case switches); a shape change there must
|
|
1158
|
+
// reflect in the tree.
|
|
1159
|
+
this.changes = group.valueChanges.subscribe(() => this.syncShape());
|
|
1036
1160
|
}
|
|
1037
|
-
select(node) {
|
|
1161
|
+
select(node, reveal = true) {
|
|
1038
1162
|
this.selected = node;
|
|
1039
|
-
|
|
1040
|
-
|
|
1163
|
+
this.sections = this.buildSections(node, []);
|
|
1164
|
+
this.breadcrumb = this.pathTo(node);
|
|
1165
|
+
// Navigating retires any pending just-added-leaf focus request.
|
|
1166
|
+
this.focusSectionId = null;
|
|
1041
1167
|
this.focusLeafKey = null;
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1168
|
+
// Reveal the selection: expand every ancestor so the row is visible, and
|
|
1169
|
+
// the node itself as it opens on the right. Structural re-syncs pass
|
|
1170
|
+
// reveal=false — they must not re-expand what the user collapsed.
|
|
1171
|
+
if (reveal) {
|
|
1172
|
+
for (const crumb of this.breadcrumb) {
|
|
1173
|
+
if (crumb !== node || this.hasExpandableContent(crumb))
|
|
1174
|
+
this.expanded.add(crumb.id);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1049
1177
|
}
|
|
1050
1178
|
/**
|
|
1051
1179
|
* 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.
|
|
1180
|
+
* or an empty array if `target` is not in the current tree.
|
|
1054
1181
|
*/
|
|
1055
1182
|
pathTo(target) {
|
|
1056
1183
|
const walk = (node, trail) => {
|
|
@@ -1072,6 +1199,31 @@ class ConfigEditorComponent {
|
|
|
1072
1199
|
else
|
|
1073
1200
|
this.expanded.add(node.id);
|
|
1074
1201
|
}
|
|
1202
|
+
/** Keyboard access for tree rows: Enter/Space selects, ArrowRight expands, ArrowLeft collapses. */
|
|
1203
|
+
onRowKeydown(event, node) {
|
|
1204
|
+
// Keys pressed on the row's inner buttons keep their own meaning.
|
|
1205
|
+
if (event.target !== event.currentTarget)
|
|
1206
|
+
return;
|
|
1207
|
+
switch (event.key) {
|
|
1208
|
+
case 'Enter':
|
|
1209
|
+
case ' ':
|
|
1210
|
+
event.preventDefault();
|
|
1211
|
+
this.select(node);
|
|
1212
|
+
break;
|
|
1213
|
+
case 'ArrowRight':
|
|
1214
|
+
if (this.hasExpandableContent(node) && !this.expanded.has(node.id)) {
|
|
1215
|
+
event.preventDefault();
|
|
1216
|
+
this.expanded.add(node.id);
|
|
1217
|
+
}
|
|
1218
|
+
break;
|
|
1219
|
+
case 'ArrowLeft':
|
|
1220
|
+
if (this.expanded.has(node.id)) {
|
|
1221
|
+
event.preventDefault();
|
|
1222
|
+
this.expanded.delete(node.id);
|
|
1223
|
+
}
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1075
1227
|
/** Whether the row shows an expand twisty: it has child rows to reveal (children or an optionals menu row). */
|
|
1076
1228
|
hasExpandableContent(node) {
|
|
1077
1229
|
return node.children.length > 0 || (this.editable() && !!node.optionals?.length);
|
|
@@ -1084,73 +1236,70 @@ class ConfigEditorComponent {
|
|
|
1084
1236
|
hasError(node) {
|
|
1085
1237
|
return !!(node.group?.invalid || node.list?.array.invalid || node.map?.group.invalid);
|
|
1086
1238
|
}
|
|
1087
|
-
/** Append a new item to a list node's FormArray
|
|
1239
|
+
/** Append a new item to a list node's FormArray (up to `maxItems`), then select it. */
|
|
1088
1240
|
addItem(listNode) {
|
|
1089
1241
|
const list = listNode.list;
|
|
1090
1242
|
if (!list)
|
|
1091
1243
|
return;
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1244
|
+
if (list.maxItems != null && list.array.length >= list.maxItems)
|
|
1245
|
+
return;
|
|
1246
|
+
list.array.push(buildFormFromSchema(list.itemSchema));
|
|
1247
|
+
this.selectByPath(this.join(listNode.id, String(list.array.length - 1)));
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Remove a list item from its FormArray (down to `minItems`). List-item
|
|
1251
|
+
* identity is positional, so expansion state under the list is cleared —
|
|
1252
|
+
* otherwise it would silently migrate to the items that shift into the
|
|
1253
|
+
* removed indexes.
|
|
1254
|
+
*/
|
|
1102
1255
|
removeItem(listNode, item) {
|
|
1103
1256
|
if (!listNode.list || !item.removable)
|
|
1104
1257
|
return;
|
|
1105
1258
|
if (listNode.list.array.length <= listNode.list.minItems)
|
|
1106
1259
|
return;
|
|
1260
|
+
for (const id of [...this.expanded]) {
|
|
1261
|
+
if (id.startsWith(`${listNode.id}/`))
|
|
1262
|
+
this.expanded.delete(id);
|
|
1263
|
+
}
|
|
1107
1264
|
listNode.list.array.removeAt(item.removable.index);
|
|
1108
|
-
|
|
1109
|
-
this.
|
|
1110
|
-
this.reselectIfOrphaned(listNode);
|
|
1265
|
+
this.selectByPath(listNode.id);
|
|
1266
|
+
this.focusSelectedRow();
|
|
1111
1267
|
}
|
|
1112
|
-
/** The
|
|
1268
|
+
/** The just-added optional leaf (section path + key) whose field grabs focus when rendered. */
|
|
1269
|
+
focusSectionId = null;
|
|
1113
1270
|
focusLeafKey = null;
|
|
1114
|
-
/** Add an absent optional child from the menu: build its control
|
|
1271
|
+
/** Add an absent optional child from the menu: build its control and select the node it lands on. */
|
|
1115
1272
|
addOptional(node, entry) {
|
|
1116
1273
|
if (!node.group || node.group.get(entry.key))
|
|
1117
1274
|
return;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1275
|
+
node.group.addControl(entry.key, buildControl(entry.schema));
|
|
1276
|
+
// A leaf renders in the parent's detail pane; complex kinds become tree nodes.
|
|
1277
|
+
this.selectByPath(entry.schema.kind === 'leaf' ? node.id : this.join(node.id, entry.key));
|
|
1278
|
+
// Adding the last optional removes the menu row that held focus.
|
|
1279
|
+
if (entry.schema.kind !== 'leaf')
|
|
1280
|
+
this.focusSelectedRow();
|
|
1123
1281
|
if (entry.schema.kind === 'leaf') {
|
|
1124
|
-
|
|
1125
|
-
|
|
1282
|
+
// Set after selection (select() retires any pending request): the new
|
|
1283
|
+
// field should grab focus, like the form's own add button. Retired after
|
|
1284
|
+
// a tick so later re-renders of the section cannot steal focus again.
|
|
1285
|
+
this.focusSectionId = node.id;
|
|
1126
1286
|
this.focusLeafKey = entry.key;
|
|
1127
|
-
|
|
1287
|
+
setTimeout(() => {
|
|
1288
|
+
if (this.focusLeafKey === entry.key) {
|
|
1289
|
+
this.focusSectionId = null;
|
|
1290
|
+
this.focusLeafKey = null;
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1128
1293
|
}
|
|
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
1294
|
}
|
|
1137
1295
|
/** Remove a present optional child node, returning its entry to the parent's menu. */
|
|
1138
1296
|
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)
|
|
1297
|
+
const key = node.presenceRemovable?.key;
|
|
1298
|
+
if (!key || !parent.group)
|
|
1150
1299
|
return;
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
this.
|
|
1300
|
+
parent.group.removeControl(key);
|
|
1301
|
+
this.selectByPath(parent.id);
|
|
1302
|
+
this.focusSelectedRow();
|
|
1154
1303
|
}
|
|
1155
1304
|
/** The active case name of a choice node, or null when none is selected. */
|
|
1156
1305
|
activeCase(node) {
|
|
@@ -1160,25 +1309,22 @@ class ConfigEditorComponent {
|
|
|
1160
1309
|
activeCaseLabel(node) {
|
|
1161
1310
|
const c = node.choice;
|
|
1162
1311
|
const active = this.activeCase(node);
|
|
1163
|
-
return c && active ?
|
|
1312
|
+
return c && active ? (c.schema.caseLabels?.[active] ?? active) : null;
|
|
1164
1313
|
}
|
|
1165
1314
|
/** The display label for a case: the schema's `caseLabels` entry, else the case name. */
|
|
1166
1315
|
caseLabel(choice, caseName) {
|
|
1167
1316
|
return choice.caseLabels?.[caseName] ?? caseName;
|
|
1168
1317
|
}
|
|
1169
|
-
/** Switch a choice node's active case
|
|
1318
|
+
/** Switch a choice node's active case; the structural sync rebuilds the tree and sections. */
|
|
1170
1319
|
switchTreeCase(node, caseName) {
|
|
1171
1320
|
const c = node.choice;
|
|
1172
1321
|
if (!c)
|
|
1173
1322
|
return;
|
|
1174
1323
|
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);
|
|
1324
|
+
this.selectByPath(node.id);
|
|
1325
|
+
}
|
|
1326
|
+
objectKeys(obj) {
|
|
1327
|
+
return Object.keys(obj);
|
|
1182
1328
|
}
|
|
1183
1329
|
/** Append a new entry to a complex map node under a generated unique key, then select it. */
|
|
1184
1330
|
addTreeMapEntry(mapNode) {
|
|
@@ -1186,36 +1332,69 @@ class ConfigEditorComponent {
|
|
|
1186
1332
|
if (!m)
|
|
1187
1333
|
return;
|
|
1188
1334
|
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);
|
|
1335
|
+
if (key != null)
|
|
1336
|
+
this.selectByPath(this.join(mapNode.id, key));
|
|
1198
1337
|
}
|
|
1199
|
-
/** Remove a complex map entry (down to `minEntries`)
|
|
1338
|
+
/** Remove a complex map entry (down to `minEntries`). */
|
|
1200
1339
|
removeTreeMapEntry(mapNode, entryNode) {
|
|
1201
1340
|
const m = mapNode.map;
|
|
1202
1341
|
const e = entryNode.mapEntry;
|
|
1203
1342
|
if (!m || !e || !removeMapEntry(m.group, m.schema, e.key))
|
|
1204
1343
|
return;
|
|
1205
|
-
|
|
1206
|
-
this.
|
|
1344
|
+
this.selectByPath(mapNode.id);
|
|
1345
|
+
this.focusSelectedRow();
|
|
1207
1346
|
}
|
|
1208
|
-
/**
|
|
1347
|
+
/**
|
|
1348
|
+
* Commit a rename-on-blur of a map entry's key; on success the entry is
|
|
1349
|
+
* selected under its new path and its fresh key field regains focus (the
|
|
1350
|
+
* rename re-renders the section under a new id, destroying the input that
|
|
1351
|
+
* held focus).
|
|
1352
|
+
*/
|
|
1209
1353
|
renameTreeMapEntry(entryNode, rawKey) {
|
|
1210
1354
|
const e = entryNode.mapEntry;
|
|
1211
1355
|
if (!e)
|
|
1212
1356
|
return;
|
|
1213
1357
|
if (renameMapEntry(e.mapGroup, e.mapSchema, e.key, rawKey)) {
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1358
|
+
const parentPath = entryNode.id.slice(0, entryNode.id.lastIndexOf('/'));
|
|
1359
|
+
const newId = this.join(parentPath, rawKey.trim());
|
|
1360
|
+
// The entry's descendants keep their expansion under the new identity.
|
|
1361
|
+
for (const id of [...this.expanded]) {
|
|
1362
|
+
if (id === entryNode.id || id.startsWith(`${entryNode.id}/`)) {
|
|
1363
|
+
this.expanded.delete(id);
|
|
1364
|
+
this.expanded.add(newId + id.slice(entryNode.id.length));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
this.selectByPath(newId);
|
|
1368
|
+
// Deferred so the rebuilt section's key field exists before focusing.
|
|
1369
|
+
setTimeout(() => this.host.nativeElement.querySelector('.detail .key-field input')?.focus());
|
|
1217
1370
|
}
|
|
1218
1371
|
}
|
|
1372
|
+
/** Move keyboard focus to the selected tree row once the action's re-render settles. */
|
|
1373
|
+
focusSelectedRow() {
|
|
1374
|
+
setTimeout(() => this.host.nativeElement.querySelector('.tree-row.selected')?.focus());
|
|
1375
|
+
}
|
|
1376
|
+
/** A muted hint for a section whose node currently renders no content of its own. */
|
|
1377
|
+
emptySectionHint(s) {
|
|
1378
|
+
const n = s.node;
|
|
1379
|
+
if (n.list && !n.children.length)
|
|
1380
|
+
return `No ${n.list.itemLabel} items.`;
|
|
1381
|
+
if (n.map?.complex && !n.children.length)
|
|
1382
|
+
return 'No entries.';
|
|
1383
|
+
if (n.map && !n.map.complex && !Object.keys(n.map.group.controls).length && !this.editable()) {
|
|
1384
|
+
return 'No entries.';
|
|
1385
|
+
}
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
/** Whether a list node is at `maxItems` (the add control is hidden). */
|
|
1389
|
+
listAtMax(node) {
|
|
1390
|
+
const l = node?.list;
|
|
1391
|
+
return !!l && l.maxItems != null && l.array.length >= l.maxItems;
|
|
1392
|
+
}
|
|
1393
|
+
/** Whether a list node is at `minItems` (item remove controls are hidden). */
|
|
1394
|
+
listAtMin(node) {
|
|
1395
|
+
const l = node?.list;
|
|
1396
|
+
return !!l && l.array.length <= l.minItems;
|
|
1397
|
+
}
|
|
1219
1398
|
/** Whether a map node is at `maxEntries` (the add control is hidden). */
|
|
1220
1399
|
mapAtMax(node) {
|
|
1221
1400
|
const m = node?.map;
|
|
@@ -1226,84 +1405,154 @@ class ConfigEditorComponent {
|
|
|
1226
1405
|
const m = node?.map;
|
|
1227
1406
|
return !!m && m.schema.minEntries != null && Object.keys(m.group.controls).length <= m.schema.minEntries;
|
|
1228
1407
|
}
|
|
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
|
-
});
|
|
1408
|
+
// --- derived-tree maintenance ----------------------------------------------
|
|
1409
|
+
/** Rebuild the tree from the current form structure if its shape changed. */
|
|
1410
|
+
syncShape() {
|
|
1411
|
+
const shape = this.shapeOf(this.formGroup());
|
|
1412
|
+
if (shape === this.shape)
|
|
1413
|
+
return;
|
|
1414
|
+
this.rebuild();
|
|
1415
|
+
this.reconcileSelection();
|
|
1242
1416
|
}
|
|
1243
|
-
/**
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1417
|
+
/**
|
|
1418
|
+
* A cheap structural signature of a control tree: group keys (plus the active
|
|
1419
|
+
* `__case`), array lengths, leaf placeholders. Value edits don't change it;
|
|
1420
|
+
* added/removed/renamed controls and case switches do. Reading `__case` from
|
|
1421
|
+
* any group is safe because the name is reserved (the builder rejects it as a
|
|
1422
|
+
* case field name or map entry key), so only choice groups carry it.
|
|
1423
|
+
*/
|
|
1424
|
+
shapeOf(control) {
|
|
1425
|
+
if (control instanceof FormGroup) {
|
|
1426
|
+
const inner = Object.keys(control.controls)
|
|
1427
|
+
.sort()
|
|
1428
|
+
.map((k) => `${k}:${this.shapeOf(control.controls[k])}`)
|
|
1429
|
+
.join(',');
|
|
1430
|
+
const active = control.get(CASE_KEY)?.value;
|
|
1431
|
+
return `{#${this.uidOf(control)}${typeof active === 'string' ? `=${active};` : ''}${inner}}`;
|
|
1432
|
+
}
|
|
1433
|
+
if (control instanceof FormArray) {
|
|
1434
|
+
return `[#${this.uidOf(control)}${control.controls.map((c) => this.shapeOf(c)).join(',')}]`;
|
|
1435
|
+
}
|
|
1436
|
+
return '.';
|
|
1437
|
+
}
|
|
1438
|
+
/** The instance id a container contributes to the shape signature. */
|
|
1439
|
+
uidOf(control) {
|
|
1440
|
+
let id = this.controlIds.get(control);
|
|
1441
|
+
if (id == null) {
|
|
1442
|
+
id = ++this.controlIdSeq;
|
|
1443
|
+
this.controlIds.set(control, id);
|
|
1444
|
+
}
|
|
1445
|
+
return id;
|
|
1256
1446
|
}
|
|
1257
|
-
/**
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1447
|
+
/** Rebuild the whole tree from schema + form and refresh the shape signature. */
|
|
1448
|
+
rebuild() {
|
|
1449
|
+
const schema = this.schema();
|
|
1450
|
+
this.root = this.buildTree(schema, this.formGroup(), schema.label ?? schema.name, '');
|
|
1451
|
+
this.shape = this.shapeOf(this.formGroup());
|
|
1452
|
+
}
|
|
1453
|
+
/** Re-point `selected` at the rebuilt tree: same path, else the closest surviving ancestor. */
|
|
1454
|
+
reconcileSelection() {
|
|
1455
|
+
this.selectByPath(this.selected?.id ?? '', false);
|
|
1456
|
+
}
|
|
1457
|
+
/** The node at `path` in the current tree, or null. Walks by id-prefix segments. */
|
|
1458
|
+
byPath(path) {
|
|
1459
|
+
if (path === this.root.id)
|
|
1460
|
+
return this.root;
|
|
1461
|
+
const walk = (node) => {
|
|
1462
|
+
for (const child of node.children) {
|
|
1463
|
+
if (child.id === path)
|
|
1464
|
+
return child;
|
|
1465
|
+
if (path.startsWith(child.id + '/'))
|
|
1466
|
+
return walk(child);
|
|
1467
|
+
}
|
|
1468
|
+
return null;
|
|
1263
1469
|
};
|
|
1470
|
+
return walk(this.root);
|
|
1471
|
+
}
|
|
1472
|
+
/** Select the node at `path`, falling back through its ancestors to the root. */
|
|
1473
|
+
selectByPath(path, reveal = true) {
|
|
1474
|
+
let target = this.byPath(path);
|
|
1475
|
+
let trimmed = path;
|
|
1476
|
+
while (!target && trimmed.includes('/')) {
|
|
1477
|
+
trimmed = trimmed.slice(0, trimmed.lastIndexOf('/'));
|
|
1478
|
+
target = this.byPath(trimmed);
|
|
1479
|
+
}
|
|
1480
|
+
this.select(target ?? this.root, reveal);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Flatten a subtree into detail sections, pre-order: the node's own fields
|
|
1484
|
+
* first, then each child as its own breadcrumb-headed section. No nesting
|
|
1485
|
+
* chrome — the headings are the boundaries between children.
|
|
1486
|
+
*/
|
|
1487
|
+
buildSections(node, trail) {
|
|
1488
|
+
const here = [...trail, node];
|
|
1489
|
+
const list = [{ node, trail: here, ...this.sectionContent(node) }];
|
|
1490
|
+
for (const child of node.children)
|
|
1491
|
+
list.push(...this.buildSections(child, here));
|
|
1492
|
+
return list;
|
|
1493
|
+
}
|
|
1494
|
+
/** A section's own renderable fields: a leaf-only schema slice and the group it binds to. */
|
|
1495
|
+
sectionContent(node) {
|
|
1496
|
+
if (node.choice) {
|
|
1497
|
+
const active = this.activeCase(node);
|
|
1498
|
+
const body = active && node.choice.schema.cases[active] ? this.caseAsGroup(node.choice.schema, active) : null;
|
|
1499
|
+
return { schema: body ? this.leafOnly(body) : null, group: node.choice.group };
|
|
1500
|
+
}
|
|
1501
|
+
if (node.schema && node.group) {
|
|
1502
|
+
return { schema: this.leafOnly(node.schema), group: node.group };
|
|
1503
|
+
}
|
|
1504
|
+
return { schema: null, group: null };
|
|
1264
1505
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1506
|
+
/**
|
|
1507
|
+
* The node's own fields as a flattened schema: leaf and leafList children
|
|
1508
|
+
* only. Complex children are rendered as sections of their own, so the
|
|
1509
|
+
* embedded form never draws nested section chrome. Null when there are none.
|
|
1510
|
+
*/
|
|
1511
|
+
leafOnly(schema) {
|
|
1512
|
+
const children = {};
|
|
1513
|
+
for (const key of Object.keys(schema.children)) {
|
|
1514
|
+
const child = schema.children[key];
|
|
1515
|
+
if (child.kind === 'leaf' || child.kind === 'leafList')
|
|
1516
|
+
children[key] = child;
|
|
1517
|
+
}
|
|
1518
|
+
if (!Object.keys(children).length)
|
|
1519
|
+
return null;
|
|
1520
|
+
return { ...schema, root: false, children, appearance: { flatten: true } };
|
|
1521
|
+
}
|
|
1522
|
+
// --- tree construction -----------------------------------------------------
|
|
1523
|
+
buildTree(schema, group, label, path) {
|
|
1268
1524
|
const children = [];
|
|
1269
1525
|
const optionals = [];
|
|
1270
|
-
const
|
|
1271
|
-
keys.forEach((key, order) => {
|
|
1526
|
+
for (const key of Object.keys(schema.children)) {
|
|
1272
1527
|
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;
|
|
1528
|
+
if (child.kind === 'leaf' || child.kind === 'leafList') {
|
|
1529
|
+
// Leaves render in the detail pane; an absent presence leaf is offered by the menu.
|
|
1530
|
+
if (child.kind === 'leaf' && child.presence && !group.get(key)) {
|
|
1531
|
+
optionals.push({ key, schema: child, label: this.labelOf(child, key) });
|
|
1532
|
+
}
|
|
1533
|
+
continue;
|
|
1285
1534
|
}
|
|
1286
1535
|
const presence = child.kind !== 'nodeGroupList' && child.presence;
|
|
1287
|
-
if (presence && !
|
|
1288
|
-
optionals.push(
|
|
1289
|
-
|
|
1536
|
+
if (presence && !group.get(key)) {
|
|
1537
|
+
optionals.push({ key, schema: child, label: this.labelOf(child, key) });
|
|
1538
|
+
continue;
|
|
1290
1539
|
}
|
|
1291
|
-
const node = this.buildChildNode(child,
|
|
1540
|
+
const node = this.buildChildNode(child, group.get(key), this.labelOf(child, key), this.join(path, key));
|
|
1292
1541
|
if (!node)
|
|
1293
|
-
|
|
1542
|
+
continue;
|
|
1294
1543
|
if (presence)
|
|
1295
|
-
node.presenceRemovable = {
|
|
1544
|
+
node.presenceRemovable = { key };
|
|
1296
1545
|
children.push(node);
|
|
1297
|
-
}
|
|
1298
|
-
const node = { id:
|
|
1546
|
+
}
|
|
1547
|
+
const node = { id: path, label, children, group, schema };
|
|
1299
1548
|
if (optionals.length)
|
|
1300
1549
|
node.optionals = optionals;
|
|
1301
1550
|
return node;
|
|
1302
1551
|
}
|
|
1303
1552
|
/** 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) {
|
|
1553
|
+
buildChildNode(schema, control, label, path) {
|
|
1305
1554
|
if (schema.kind === 'nodeGroup') {
|
|
1306
|
-
return control instanceof FormGroup ? this.buildTree(schema, control, label) : null;
|
|
1555
|
+
return control instanceof FormGroup ? this.buildTree(schema, control, label, path) : null;
|
|
1307
1556
|
}
|
|
1308
1557
|
if (schema.kind === 'nodeGroupList') {
|
|
1309
1558
|
const array = control;
|
|
@@ -1314,20 +1563,18 @@ class ConfigEditorComponent {
|
|
|
1314
1563
|
.map((item, i) => {
|
|
1315
1564
|
// Just "#n": the item sits under its list node, so repeating the
|
|
1316
1565
|
// item name (e.g. "Interface #1") only echoes the parent.
|
|
1317
|
-
const node = this.buildTree(schema.type, item, `#${i + 1}
|
|
1318
|
-
node.removable = {
|
|
1566
|
+
const node = this.buildTree(schema.type, item, `#${i + 1}`, this.join(path, String(i)));
|
|
1567
|
+
node.removable = { index: i };
|
|
1319
1568
|
return node;
|
|
1320
1569
|
})
|
|
1321
1570
|
: [];
|
|
1322
1571
|
return {
|
|
1323
|
-
id:
|
|
1572
|
+
id: path,
|
|
1324
1573
|
label,
|
|
1325
1574
|
children: items,
|
|
1326
|
-
leaves: [],
|
|
1327
|
-
leafLists: [],
|
|
1328
1575
|
group: null,
|
|
1329
1576
|
list: array instanceof FormArray
|
|
1330
|
-
? { array, itemSchema: schema.type, itemLabel, minItems: schema.minItems ?? 0 }
|
|
1577
|
+
? { array, itemSchema: schema.type, itemLabel, minItems: schema.minItems ?? 0, maxItems: schema.maxItems }
|
|
1331
1578
|
: undefined,
|
|
1332
1579
|
};
|
|
1333
1580
|
}
|
|
@@ -1336,8 +1583,9 @@ class ConfigEditorComponent {
|
|
|
1336
1583
|
return null;
|
|
1337
1584
|
const active = control.get(CASE_KEY)?.value;
|
|
1338
1585
|
const node = active && schema.cases[active]
|
|
1339
|
-
? this.buildTree(this.caseAsGroup(schema, active), control, label)
|
|
1340
|
-
: { id:
|
|
1586
|
+
? this.buildTree(this.caseAsGroup(schema, active), control, label, path)
|
|
1587
|
+
: { id: path, label, children: [], group: control };
|
|
1588
|
+
node.schema = undefined;
|
|
1341
1589
|
node.choice = { schema, group: control };
|
|
1342
1590
|
return node;
|
|
1343
1591
|
}
|
|
@@ -1351,36 +1599,57 @@ class ConfigEditorComponent {
|
|
|
1351
1599
|
const entries = complex
|
|
1352
1600
|
? Object.keys(control.controls)
|
|
1353
1601
|
.map((key) => {
|
|
1354
|
-
|
|
1355
|
-
|
|
1602
|
+
// Index access, not .get(): entry keys are arbitrary runtime data
|
|
1603
|
+
// and .get() would split a key like '10.0.0.1' into a dotted path.
|
|
1604
|
+
const entryNode = this.buildChildNode(schema.value, control.controls[key], key, this.join(path, key));
|
|
1605
|
+
if (entryNode) {
|
|
1356
1606
|
entryNode.mapEntry = { mapGroup: control, mapSchema: schema, key };
|
|
1607
|
+
}
|
|
1357
1608
|
return entryNode;
|
|
1358
1609
|
})
|
|
1359
1610
|
.filter((n) => n !== null)
|
|
1360
1611
|
: [];
|
|
1361
1612
|
return {
|
|
1362
|
-
id:
|
|
1613
|
+
id: path,
|
|
1363
1614
|
label,
|
|
1364
1615
|
children: entries,
|
|
1365
|
-
leaves: [],
|
|
1366
|
-
leafLists: [],
|
|
1367
1616
|
group: null,
|
|
1368
1617
|
map: { schema, group: control, complex },
|
|
1369
1618
|
};
|
|
1370
1619
|
}
|
|
1371
1620
|
return null;
|
|
1372
1621
|
}
|
|
1622
|
+
/** Synthetic group over a case's normalized fields, so a case body builds like any group. */
|
|
1623
|
+
caseAsGroup(choice, caseName) {
|
|
1624
|
+
return {
|
|
1625
|
+
kind: 'nodeGroup',
|
|
1626
|
+
name: choice.name,
|
|
1627
|
+
children: choice.cases[caseName] ? caseFields(choice.cases[caseName]) : {},
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1373
1630
|
/** A node's display label: its schema `label`, else its record key. */
|
|
1374
1631
|
labelOf(node, key) {
|
|
1375
1632
|
return ('label' in node ? node.label : undefined) ?? key;
|
|
1376
1633
|
}
|
|
1634
|
+
/** Join a parent path and a segment; the root's path is the empty string. */
|
|
1635
|
+
join(parent, segment) {
|
|
1636
|
+
const seg = this.escapeSeg(segment);
|
|
1637
|
+
return parent ? `${parent}/${seg}` : seg;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Escape a path segment: `/` is the id separator, and map entry keys are
|
|
1641
|
+
* arbitrary runtime data that may contain it. Labels stay unescaped — only
|
|
1642
|
+
* node identities encode.
|
|
1643
|
+
*/
|
|
1644
|
+
escapeSeg(segment) {
|
|
1645
|
+
return segment.replace(/%/g, '%25').replace(/\//g, '%2F');
|
|
1646
|
+
}
|
|
1377
1647
|
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"] }] });
|
|
1648
|
+
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
1649
|
}
|
|
1380
1650
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, decorators: [{
|
|
1381
1651
|
type: Component,
|
|
1382
1652
|
args: [{ selector: 'nff-config-editor', standalone: true, imports: [
|
|
1383
|
-
ReactiveFormsModule,
|
|
1384
1653
|
NgTemplateOutlet,
|
|
1385
1654
|
MatIconModule,
|
|
1386
1655
|
MatButtonModule,
|
|
@@ -1389,11 +1658,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
1389
1658
|
MatInputModule,
|
|
1390
1659
|
MatSelectModule,
|
|
1391
1660
|
MatTooltip,
|
|
1392
|
-
|
|
1393
|
-
LeafListRendererComponent,
|
|
1661
|
+
DynamicRecursiveFormComponent,
|
|
1394
1662
|
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 }] }] } });
|
|
1663
|
+
], 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"] }]
|
|
1664
|
+
}], 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
1665
|
|
|
1398
1666
|
/*
|
|
1399
1667
|
* Public API Surface of ng-form-foundry
|
|
@@ -1403,5 +1671,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
1403
1671
|
* Generated bundle index. Do not edit.
|
|
1404
1672
|
*/
|
|
1405
1673
|
|
|
1406
|
-
export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, addMapEntry, buildControl, buildFormFromSchema, caseFields, defineSchema, removeMapEntry, renameMapEntry, resolveChoiceCase, switchChoiceCase };
|
|
1674
|
+
export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, addMapEntry, buildControl, buildFormFromSchema, caseFields, defineSchema, removeMapEntry, renameMapEntry, resolveChoiceCase, serializeForm, switchChoiceCase, toWireValue };
|
|
1407
1675
|
//# sourceMappingURL=ng-form-foundry.mjs.map
|