ng-form-foundry 0.3.3 → 0.3.4
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 +3 -1
- package/fesm2022/ng-form-foundry.mjs +200 -30
- package/fesm2022/ng-form-foundry.mjs.map +1 -1
- package/index.d.ts +77 -14
- package/package.json +1 -1
|
@@ -185,6 +185,24 @@ function buildLeafListControl(list, initial) {
|
|
|
185
185
|
const values = (Array.isArray(initial) ? initial : undefined) ?? list.default ?? [];
|
|
186
186
|
return new FormArray(values.map((v) => new FormControl(v)));
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Group error while the number of present (enabled) children is outside the
|
|
190
|
+
* group's `minPresent`/`maxPresent` range — JSON Schema `minProperties` /
|
|
191
|
+
* `maxProperties` on a closed object whose properties are presence-optional.
|
|
192
|
+
* Counted from the live controls, so presence toggles re-evaluate it.
|
|
193
|
+
*/
|
|
194
|
+
function presentChildrenValidator(group) {
|
|
195
|
+
return (control) => {
|
|
196
|
+
const actual = Object.keys(control.controls).length;
|
|
197
|
+
if (group.minPresent != null && actual < group.minPresent) {
|
|
198
|
+
return { minPresent: { required: group.minPresent, actual } };
|
|
199
|
+
}
|
|
200
|
+
if (group.maxPresent != null && actual > group.maxPresent) {
|
|
201
|
+
return { maxPresent: { allowed: group.maxPresent, actual } };
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
188
206
|
function buildNodeGroupControl(group, initial) {
|
|
189
207
|
const controls = {};
|
|
190
208
|
for (const key in group.children) {
|
|
@@ -200,7 +218,8 @@ function buildNodeGroupControl(group, initial) {
|
|
|
200
218
|
// parent record and prevents list builders from sizing to the real data.
|
|
201
219
|
controls[key] = buildControl(child, initial?.[key]);
|
|
202
220
|
}
|
|
203
|
-
|
|
221
|
+
const constrained = group.minPresent != null || group.maxPresent != null;
|
|
222
|
+
return new FormGroup(controls, constrained ? { validators: presentChildrenValidator(group) } : undefined);
|
|
204
223
|
}
|
|
205
224
|
function buildNodeGroupListControl(list, initial = null) {
|
|
206
225
|
// `initial` is the runtime data array — one group per element. With no data
|
|
@@ -672,7 +691,7 @@ class LeafRendererComponent {
|
|
|
672
691
|
return 'Invalid value';
|
|
673
692
|
}
|
|
674
693
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
675
|
-
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"] }] });
|
|
694
|
+
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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\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"] }] });
|
|
676
695
|
}
|
|
677
696
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, decorators: [{
|
|
678
697
|
type: Component,
|
|
@@ -686,7 +705,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
686
705
|
MatButtonModule,
|
|
687
706
|
MatTooltip,
|
|
688
707
|
LeafEnumRendererComponent,
|
|
689
|
-
], 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"] }]
|
|
708
|
+
], 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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\n"] }]
|
|
690
709
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { leaf_: [{
|
|
691
710
|
type: Input
|
|
692
711
|
}], control: [{
|
|
@@ -870,7 +889,7 @@ class LeafListRendererComponent {
|
|
|
870
889
|
this.formArray.push(new FormControl());
|
|
871
890
|
}
|
|
872
891
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
873
|
-
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"] }] });
|
|
892
|
+
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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\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"] }] });
|
|
874
893
|
}
|
|
875
894
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
|
|
876
895
|
type: Component,
|
|
@@ -879,7 +898,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
879
898
|
AnonLeafRendererComponent,
|
|
880
899
|
MatButtonModule,
|
|
881
900
|
MatPrefix,
|
|
882
|
-
], 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"] }]
|
|
901
|
+
], 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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\n"] }]
|
|
883
902
|
}], propDecorators: { leaf_: [{
|
|
884
903
|
type: Input
|
|
885
904
|
}], initialValue: [{
|
|
@@ -967,7 +986,7 @@ class NodeMapRendererComponent {
|
|
|
967
986
|
asFormControl = asFormControl;
|
|
968
987
|
asFormGroup = asFormGroup;
|
|
969
988
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
970
|
-
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"] }] });
|
|
989
|
+
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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\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"] }] });
|
|
971
990
|
}
|
|
972
991
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, decorators: [{
|
|
973
992
|
type: Component,
|
|
@@ -980,7 +999,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
980
999
|
// node-map-renderer and dynamic-recursive-form import each other (a map value
|
|
981
1000
|
// may be a group); forwardRef defers the reference to break the cycle.
|
|
982
1001
|
forwardRef(() => DynamicRecursiveFormComponent),
|
|
983
|
-
], 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"] }]
|
|
1002
|
+
], 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}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\n"] }]
|
|
984
1003
|
}], propDecorators: { nodeMap: [{
|
|
985
1004
|
type: Input
|
|
986
1005
|
}], formGroup: [{
|
|
@@ -1114,7 +1133,7 @@ class DynamicRecursiveFormComponent {
|
|
|
1114
1133
|
asFormArray = asFormArray;
|
|
1115
1134
|
asFormControl = asFormControl;
|
|
1116
1135
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1117
|
-
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"] }] });
|
|
1136
|
+
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}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\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"] }] });
|
|
1118
1137
|
}
|
|
1119
1138
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
|
|
1120
1139
|
type: Component,
|
|
@@ -1135,7 +1154,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
1135
1154
|
MatFormFieldModule,
|
|
1136
1155
|
MatSelectModule,
|
|
1137
1156
|
MatTooltip,
|
|
1138
|
-
], 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"] }]
|
|
1157
|
+
], 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}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}\n"] }]
|
|
1139
1158
|
}], 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 }] }] } });
|
|
1140
1159
|
|
|
1141
1160
|
/**
|
|
@@ -1165,8 +1184,12 @@ class ConfigEditorComponent {
|
|
|
1165
1184
|
schema = input.required(...(ngDevMode ? [{ debugName: "schema" }] : []));
|
|
1166
1185
|
/** The schema-built reactive group the editor binds to and mutates. */
|
|
1167
1186
|
formGroup = input.required(...(ngDevMode ? [{ debugName: "formGroup" }] : []));
|
|
1168
|
-
/**
|
|
1169
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Whether fields accept input and structural controls (add/remove/menus)
|
|
1189
|
+
* show. Two-way bindable: the root tree row carries a toggle, so the editor
|
|
1190
|
+
* can flip it itself and the host observes the change.
|
|
1191
|
+
*/
|
|
1192
|
+
editable = model(true, ...(ngDevMode ? [{ debugName: "editable" }] : []));
|
|
1170
1193
|
root;
|
|
1171
1194
|
selected = null;
|
|
1172
1195
|
/** The selected subtree as a flat, breadcrumb-separated section list. */
|
|
@@ -1208,7 +1231,10 @@ class ConfigEditorComponent {
|
|
|
1208
1231
|
}
|
|
1209
1232
|
select(node, reveal = true) {
|
|
1210
1233
|
this.selected = node;
|
|
1211
|
-
|
|
1234
|
+
// Sections that would render only their breadcrumb heading (a composite
|
|
1235
|
+
// node with no leaf fields and no chrome) are dropped: their children get
|
|
1236
|
+
// sections of their own, whose trails carry the full path anyway.
|
|
1237
|
+
this.sections = this.markContinuations(this.buildSections(node, []).filter((s, i) => i === 0 || this.sectionHasContent(s)));
|
|
1212
1238
|
this.breadcrumb = this.pathTo(node);
|
|
1213
1239
|
// Navigating retires any pending just-added-leaf focus request.
|
|
1214
1240
|
this.focusSectionId = null;
|
|
@@ -1284,15 +1310,23 @@ class ConfigEditorComponent {
|
|
|
1284
1310
|
hasError(node) {
|
|
1285
1311
|
return !!(node.group?.invalid || node.list?.array.invalid || node.map?.group.invalid);
|
|
1286
1312
|
}
|
|
1287
|
-
/**
|
|
1288
|
-
|
|
1313
|
+
/**
|
|
1314
|
+
* Append a new item to a list node's FormArray (up to `maxItems`). A tree-row
|
|
1315
|
+
* add selects the new item; a detail-pane add passes `keepSelection` so the
|
|
1316
|
+
* current (possibly ancestor) view stays put and the item appears as a new
|
|
1317
|
+
* section in it.
|
|
1318
|
+
*/
|
|
1319
|
+
addItem(listNode, keepSelection = false) {
|
|
1289
1320
|
const list = listNode.list;
|
|
1290
1321
|
if (!list)
|
|
1291
1322
|
return;
|
|
1292
1323
|
if (list.maxItems != null && list.array.length >= list.maxItems)
|
|
1293
1324
|
return;
|
|
1294
1325
|
list.array.push(buildFormFromSchema(list.itemSchema));
|
|
1295
|
-
|
|
1326
|
+
if (keepSelection)
|
|
1327
|
+
this.selectByPath(this.selected?.id ?? '', false);
|
|
1328
|
+
else
|
|
1329
|
+
this.selectByPath(this.join(listNode.id, String(list.array.length - 1)));
|
|
1296
1330
|
}
|
|
1297
1331
|
/**
|
|
1298
1332
|
* Remove a list item from its FormArray (down to `minItems`). List-item
|
|
@@ -1300,7 +1334,7 @@ class ConfigEditorComponent {
|
|
|
1300
1334
|
* otherwise it would silently migrate to the items that shift into the
|
|
1301
1335
|
* removed indexes.
|
|
1302
1336
|
*/
|
|
1303
|
-
removeItem(listNode, item) {
|
|
1337
|
+
removeItem(listNode, item, keepSelection = false) {
|
|
1304
1338
|
if (!listNode.list || !item.removable)
|
|
1305
1339
|
return;
|
|
1306
1340
|
if (listNode.list.array.length <= listNode.list.minItems)
|
|
@@ -1310,6 +1344,12 @@ class ConfigEditorComponent {
|
|
|
1310
1344
|
this.expanded.delete(id);
|
|
1311
1345
|
}
|
|
1312
1346
|
listNode.list.array.removeAt(item.removable.index);
|
|
1347
|
+
// A detail-pane remove keeps the (ancestor) selection; a tree-row remove
|
|
1348
|
+
// moves to the list and returns focus to its row.
|
|
1349
|
+
if (keepSelection) {
|
|
1350
|
+
this.selectByPath(this.selected?.id ?? '', false);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1313
1353
|
this.selectByPath(listNode.id);
|
|
1314
1354
|
this.focusSelectedRow();
|
|
1315
1355
|
}
|
|
@@ -1363,45 +1403,68 @@ class ConfigEditorComponent {
|
|
|
1363
1403
|
caseLabel(choice, caseName) {
|
|
1364
1404
|
return choice.caseLabels?.[caseName] ?? caseName;
|
|
1365
1405
|
}
|
|
1366
|
-
/**
|
|
1406
|
+
/**
|
|
1407
|
+
* Switch a choice node's active case from its detail selector; the
|
|
1408
|
+
* structural sync rebuilds the tree and sections. The tree selection stays
|
|
1409
|
+
* where it is: detail edits must never steal it, or a flattened ancestor
|
|
1410
|
+
* view would collapse to the choice node mid-edit. Selection moves only
|
|
1411
|
+
* through the tree itself.
|
|
1412
|
+
*/
|
|
1367
1413
|
switchTreeCase(node, caseName) {
|
|
1368
1414
|
const c = node.choice;
|
|
1369
1415
|
if (!c)
|
|
1370
1416
|
return;
|
|
1371
1417
|
switchChoiceCase(c.group, c.schema, caseName);
|
|
1372
|
-
this.selectByPath(node.id);
|
|
1418
|
+
this.selectByPath(this.selected?.id ?? node.id, false);
|
|
1373
1419
|
}
|
|
1374
1420
|
objectKeys(obj) {
|
|
1375
1421
|
return Object.keys(obj);
|
|
1376
1422
|
}
|
|
1377
|
-
/**
|
|
1378
|
-
|
|
1423
|
+
/**
|
|
1424
|
+
* Append a new entry to a complex map node under a generated unique key. A
|
|
1425
|
+
* tree-row add selects the new entry; a detail-pane add passes
|
|
1426
|
+
* `keepSelection` so the current view stays put (see {@link addItem}).
|
|
1427
|
+
*/
|
|
1428
|
+
addTreeMapEntry(mapNode, keepSelection = false) {
|
|
1379
1429
|
const m = mapNode.map;
|
|
1380
1430
|
if (!m)
|
|
1381
1431
|
return;
|
|
1382
1432
|
const key = addMapEntry(m.group, m.schema);
|
|
1383
|
-
if (key
|
|
1433
|
+
if (key == null)
|
|
1434
|
+
return;
|
|
1435
|
+
if (keepSelection)
|
|
1436
|
+
this.selectByPath(this.selected?.id ?? '', false);
|
|
1437
|
+
else
|
|
1384
1438
|
this.selectByPath(this.join(mapNode.id, key));
|
|
1385
1439
|
}
|
|
1386
1440
|
/** Remove a complex map entry (down to `minEntries`). */
|
|
1387
|
-
removeTreeMapEntry(mapNode, entryNode) {
|
|
1441
|
+
removeTreeMapEntry(mapNode, entryNode, keepSelection = false) {
|
|
1388
1442
|
const m = mapNode.map;
|
|
1389
1443
|
const e = entryNode.mapEntry;
|
|
1390
1444
|
if (!m || !e || !removeMapEntry(m.group, m.schema, e.key))
|
|
1391
1445
|
return;
|
|
1446
|
+
if (keepSelection) {
|
|
1447
|
+
this.selectByPath(this.selected?.id ?? '', false);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1392
1450
|
this.selectByPath(mapNode.id);
|
|
1393
1451
|
this.focusSelectedRow();
|
|
1394
1452
|
}
|
|
1395
1453
|
/**
|
|
1396
|
-
* Commit a rename-on-blur of a map entry's key; on success the
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1454
|
+
* Commit a rename-on-blur of a map entry's key; on success the selection is
|
|
1455
|
+
* remapped to the entry's new identity when it pointed at (or inside) the
|
|
1456
|
+
* entry — otherwise it stays put — and the fresh key field regains focus
|
|
1457
|
+
* (the rename re-renders the section under a new id, destroying the input
|
|
1458
|
+
* that held focus).
|
|
1400
1459
|
*/
|
|
1401
1460
|
renameTreeMapEntry(entryNode, rawKey) {
|
|
1402
1461
|
const e = entryNode.mapEntry;
|
|
1403
1462
|
if (!e)
|
|
1404
1463
|
return;
|
|
1464
|
+
// Captured before the mutation: the rename's structural sync reconciles
|
|
1465
|
+
// the selection against the stale id (falling back to an ancestor) before
|
|
1466
|
+
// this method can remap it.
|
|
1467
|
+
const selectedId = this.selected?.id ?? '';
|
|
1405
1468
|
if (renameMapEntry(e.mapGroup, e.mapSchema, e.key, rawKey)) {
|
|
1406
1469
|
const parentPath = entryNode.id.slice(0, entryNode.id.lastIndexOf('/'));
|
|
1407
1470
|
const newId = this.join(parentPath, rawKey.trim());
|
|
@@ -1412,7 +1475,12 @@ class ConfigEditorComponent {
|
|
|
1412
1475
|
this.expanded.add(newId + id.slice(entryNode.id.length));
|
|
1413
1476
|
}
|
|
1414
1477
|
}
|
|
1415
|
-
|
|
1478
|
+
// Selection is remapped, not moved: it follows the rename only when it
|
|
1479
|
+
// sat on the entry (or inside it); an ancestor view stays put.
|
|
1480
|
+
const followed = selectedId === entryNode.id || selectedId.startsWith(`${entryNode.id}/`)
|
|
1481
|
+
? newId + selectedId.slice(entryNode.id.length)
|
|
1482
|
+
: selectedId;
|
|
1483
|
+
this.selectByPath(followed, false);
|
|
1416
1484
|
// Deferred so the rebuilt section's key field exists before focusing.
|
|
1417
1485
|
setTimeout(() => this.host.nativeElement.querySelector('.detail .key-field input')?.focus());
|
|
1418
1486
|
}
|
|
@@ -1422,6 +1490,23 @@ class ConfigEditorComponent {
|
|
|
1422
1490
|
setTimeout(() => this.host.nativeElement.querySelector('.tree-row.selected')?.focus());
|
|
1423
1491
|
}
|
|
1424
1492
|
/** A muted hint for a section whose node currently renders no content of its own. */
|
|
1493
|
+
/**
|
|
1494
|
+
* Explains a group's `minPresent`/`maxPresent` violation next to the red
|
|
1495
|
+
* tree marker: the fix is enabling or removing optional fields, which is not
|
|
1496
|
+
* evident from any single field's own error.
|
|
1497
|
+
*/
|
|
1498
|
+
presentRangeHint(s) {
|
|
1499
|
+
const errors = s.node.group?.errors;
|
|
1500
|
+
const min = errors?.['minPresent'];
|
|
1501
|
+
if (min) {
|
|
1502
|
+
const noun = min.required === 1 ? 'field' : 'fields';
|
|
1503
|
+
return `At least ${min.required} ${noun} must be set (${min.actual} set) — add from the optional-fields menu.`;
|
|
1504
|
+
}
|
|
1505
|
+
const max = errors?.['maxPresent'];
|
|
1506
|
+
if (max)
|
|
1507
|
+
return `At most ${max.allowed} of these fields may be set (${max.actual} set).`;
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1425
1510
|
emptySectionHint(s) {
|
|
1426
1511
|
const n = s.node;
|
|
1427
1512
|
if (n.list && !n.children.length)
|
|
@@ -1537,8 +1622,93 @@ class ConfigEditorComponent {
|
|
|
1537
1622
|
const list = [{ node, trail: here, ...this.sectionContent(node) }];
|
|
1538
1623
|
for (const child of node.children)
|
|
1539
1624
|
list.push(...this.buildSections(child, here));
|
|
1625
|
+
// A list's / complex map's add control trails its items — appending
|
|
1626
|
+
// belongs after the last member, not under the container's heading.
|
|
1627
|
+
if (node.list || node.map?.complex) {
|
|
1628
|
+
list.push({ node, trail: here, schema: null, group: null, footer: true });
|
|
1629
|
+
}
|
|
1540
1630
|
return list;
|
|
1541
1631
|
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Whether a section renders anything beyond its heading: a leaf slice, the
|
|
1634
|
+
* chrome of its node kind (case selector, key field, add controls), or a
|
|
1635
|
+
* present-children error that needs explaining. Heading-only sections are
|
|
1636
|
+
* dropped from the flat list — a divider with nothing under it is clutter.
|
|
1637
|
+
*/
|
|
1638
|
+
sectionHasContent(s) {
|
|
1639
|
+
if (s.footer)
|
|
1640
|
+
return true;
|
|
1641
|
+
const n = s.node;
|
|
1642
|
+
// A non-empty list / complex map's leading section would hold only its
|
|
1643
|
+
// heading (the add control lives in the footer): drop it — the items'
|
|
1644
|
+
// trails carry the container in their breadcrumbs.
|
|
1645
|
+
return !!(s.schema ||
|
|
1646
|
+
n.choice ||
|
|
1647
|
+
n.mapEntry ||
|
|
1648
|
+
(n.map && !n.map.complex) ||
|
|
1649
|
+
this.emptySectionHint(s) ||
|
|
1650
|
+
this.presentRangeHint(s));
|
|
1651
|
+
}
|
|
1652
|
+
/** Every expandable node currently expanded — drives the root row's expand/collapse-all toggle. */
|
|
1653
|
+
allExpanded() {
|
|
1654
|
+
let all = true;
|
|
1655
|
+
const walk = (node) => {
|
|
1656
|
+
if (!all)
|
|
1657
|
+
return;
|
|
1658
|
+
if (this.hasExpandableContent(node) && !this.expanded.has(node.id))
|
|
1659
|
+
all = false;
|
|
1660
|
+
for (const child of node.children)
|
|
1661
|
+
walk(child);
|
|
1662
|
+
};
|
|
1663
|
+
walk(this.root);
|
|
1664
|
+
return all;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Expand every expandable node, or collapse back when all are open. The
|
|
1668
|
+
* collapse keeps the root open — a tree reduced to one row reads as empty.
|
|
1669
|
+
*/
|
|
1670
|
+
toggleExpandAll() {
|
|
1671
|
+
if (this.allExpanded()) {
|
|
1672
|
+
this.expanded.clear();
|
|
1673
|
+
this.expanded.add(this.root.id);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const walk = (node) => {
|
|
1677
|
+
if (this.hasExpandableContent(node))
|
|
1678
|
+
this.expanded.add(node.id);
|
|
1679
|
+
for (const child of node.children)
|
|
1680
|
+
walk(child);
|
|
1681
|
+
};
|
|
1682
|
+
walk(this.root);
|
|
1683
|
+
}
|
|
1684
|
+
/** The selected member's container (list / map), for the breadcrumb's remove control. */
|
|
1685
|
+
selectedMemberParent() {
|
|
1686
|
+
if (!this.selected || !(this.selected.removable || this.selected.mapEntry))
|
|
1687
|
+
return null;
|
|
1688
|
+
return this.breadcrumb.length > 1 ? (this.breadcrumb[this.breadcrumb.length - 2] ?? null) : null;
|
|
1689
|
+
}
|
|
1690
|
+
/** The section node's container (list / map), for member controls in its heading. */
|
|
1691
|
+
memberParent(s) {
|
|
1692
|
+
return s.trail.length > 1 ? (s.trail[s.trail.length - 2] ?? null) : null;
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Flag the sections that continue a run of siblings — a list item or map
|
|
1696
|
+
* entry whose preceding section is inside the same container. Their headings
|
|
1697
|
+
* draw no divider line, so a run of same-kind members reads as one list
|
|
1698
|
+
* rather than a stack of separated blocks.
|
|
1699
|
+
*/
|
|
1700
|
+
markContinuations(sections) {
|
|
1701
|
+
for (let i = 1; i < sections.length; i++) {
|
|
1702
|
+
const s = sections[i];
|
|
1703
|
+
if (s.footer || !(s.node.removable || s.node.mapEntry))
|
|
1704
|
+
continue;
|
|
1705
|
+
const containerId = s.node.id.slice(0, s.node.id.lastIndexOf('/'));
|
|
1706
|
+
const prev = sections[i - 1].node.id;
|
|
1707
|
+
if (prev === containerId || prev.startsWith(`${containerId}/`))
|
|
1708
|
+
s.continuation = true;
|
|
1709
|
+
}
|
|
1710
|
+
return sections;
|
|
1711
|
+
}
|
|
1542
1712
|
/** A section's own renderable fields: a leaf-only schema slice and the group it binds to. */
|
|
1543
1713
|
sectionContent(node) {
|
|
1544
1714
|
if (node.choice) {
|
|
@@ -1693,7 +1863,7 @@ class ConfigEditorComponent {
|
|
|
1693
1863
|
return segment.replace(/%/g, '%25').replace(/\//g, '%2F');
|
|
1694
1864
|
}
|
|
1695
1865
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1696
|
-
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"] }] });
|
|
1866
|
+
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 } }, outputs: { editable: "editableChange" }, 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 (node === root) {\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-pressed]=\"editable()\"\n (click)=\"editable.set(!editable()); $event.stopPropagation()\"\n >\n <mat-icon>{{ editable() ? 'edit_off' : 'edit' }}</mat-icon>\n </button>\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n [attr.aria-label]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n (click)=\"toggleExpandAll(); $event.stopPropagation()\"\n >\n <mat-icon>{{ allExpanded() ? 'unfold_less' : 'unfold_more' }}</mat-icon>\n </button>\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 <!-- A selected member (list item / map entry) is removable from its own\n view; the selection then moves to its container, like a tree-row\n remove \u2014 a deleted node cannot stay selected. -->\n @if (editable() && selected.removable && selectedMemberParent(); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + selected.label\"\n (click)=\"removeItem(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && selected.mapEntry && selectedMemberParent(); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove entry\"\n [attr.aria-label]=\"'Remove ' + selected.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\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 + (s.footer ? ':footer' : '')) {\n @if (s.trail.length > 1 && !s.footer) {\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\" [class.continuation]=\"s.continuation\" 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 @if (editable() && s.node.removable && memberParent(s); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + s.node.label\"\n (click)=\"removeItem(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && s.node.mapEntry && memberParent(s); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove entry\"\n [attr.aria-label]=\"'Remove ' + s.node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\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 (s.footer && editable() && s.node.map?.complex && !mapAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addTreeMapEntry(s.node, true)\"><mat-icon>add</mat-icon> Add entry</button>\n </div>\n }\n @if (s.footer && editable() && s.node.list && !listAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addItem(s.node, true)\">\n <mat-icon>add</mat-icon> Add {{ s.node.list.itemLabel }}\n </button>\n </div>\n }\n @if (!s.footer && emptySectionHint(s); as hint) {\n <p class=\"empty\">{{ hint }}</p>\n }\n @if (!s.footer && presentRangeHint(s); as hint) {\n <p class=\"section-error\" role=\"alert\">{{ 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;min-height:320px}.tree{flex:0 0 360px;min-width:360px;position:sticky;top:var(--nff-tree-sticky-top, 0px);align-self:flex-start;max-height:calc(100vh - var(--nff-tree-sticky-top, 0px));overflow:auto;padding:4px 16px 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}.root-toggle{color:var(--mat-sys-on-surface-variant)}.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;border-left:1px solid var(--mat-sys-outline-variant);padding-left:16px}.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;font-size:.95rem}.section-heading.continuation{margin-top:12px}.section-heading .crumb-current{font-weight:600}.section-remove{flex:0 0 auto;align-self:center}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.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}.section-error{color:var(--mat-sys-error);font-size:.85rem}.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"] }] });
|
|
1697
1867
|
}
|
|
1698
1868
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, decorators: [{
|
|
1699
1869
|
type: Component,
|
|
@@ -1708,8 +1878,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
1708
1878
|
MatTooltip,
|
|
1709
1879
|
DynamicRecursiveFormComponent,
|
|
1710
1880
|
NodeMapRendererComponent,
|
|
1711
|
-
], 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"] }]
|
|
1712
|
-
}], 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 }] }] } });
|
|
1881
|
+
], 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 (node === root) {\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-pressed]=\"editable()\"\n (click)=\"editable.set(!editable()); $event.stopPropagation()\"\n >\n <mat-icon>{{ editable() ? 'edit_off' : 'edit' }}</mat-icon>\n </button>\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n [attr.aria-label]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n (click)=\"toggleExpandAll(); $event.stopPropagation()\"\n >\n <mat-icon>{{ allExpanded() ? 'unfold_less' : 'unfold_more' }}</mat-icon>\n </button>\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 <!-- A selected member (list item / map entry) is removable from its own\n view; the selection then moves to its container, like a tree-row\n remove \u2014 a deleted node cannot stay selected. -->\n @if (editable() && selected.removable && selectedMemberParent(); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + selected.label\"\n (click)=\"removeItem(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && selected.mapEntry && selectedMemberParent(); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove entry\"\n [attr.aria-label]=\"'Remove ' + selected.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\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 + (s.footer ? ':footer' : '')) {\n @if (s.trail.length > 1 && !s.footer) {\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\" [class.continuation]=\"s.continuation\" 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 @if (editable() && s.node.removable && memberParent(s); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove\"\n [attr.aria-label]=\"'Remove ' + s.node.label\"\n (click)=\"removeItem(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && s.node.mapEntry && memberParent(s); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n matTooltip=\"Remove entry\"\n [attr.aria-label]=\"'Remove ' + s.node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\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 (s.footer && editable() && s.node.map?.complex && !mapAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addTreeMapEntry(s.node, true)\"><mat-icon>add</mat-icon> Add entry</button>\n </div>\n }\n @if (s.footer && editable() && s.node.list && !listAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton (click)=\"addItem(s.node, true)\">\n <mat-icon>add</mat-icon> Add {{ s.node.list.itemLabel }}\n </button>\n </div>\n }\n @if (!s.footer && emptySectionHint(s); as hint) {\n <p class=\"empty\">{{ hint }}</p>\n }\n @if (!s.footer && presentRangeHint(s); as hint) {\n <p class=\"section-error\" role=\"alert\">{{ 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;min-height:320px}.tree{flex:0 0 360px;min-width:360px;position:sticky;top:var(--nff-tree-sticky-top, 0px);align-self:flex-start;max-height:calc(100vh - var(--nff-tree-sticky-top, 0px));overflow:auto;padding:4px 16px 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}.root-toggle{color:var(--mat-sys-on-surface-variant)}.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;border-left:1px solid var(--mat-sys-outline-variant);padding-left:16px}.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;font-size:.95rem}.section-heading.continuation{margin-top:12px}.section-heading .crumb-current{font-weight:600}.section-remove{flex:0 0 auto;align-self:center}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.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}.section-error{color:var(--mat-sys-error);font-size:.85rem}.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"] }]
|
|
1882
|
+
}], 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 }] }, { type: i0.Output, args: ["editableChange"] }] } });
|
|
1713
1883
|
|
|
1714
1884
|
/*
|
|
1715
1885
|
* Public API Surface of ng-form-foundry
|