@ngstarter-ui/components 21.0.39 → 21.0.40
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/ai/component-registry.json +143 -1
- package/fesm2022/ngstarter-ui-components-form-builder.mjs +1695 -0
- package/fesm2022/ngstarter-ui-components-form-builder.mjs.map +1 -0
- package/fesm2022/ngstarter-ui-components-toolbar.mjs +2 -4
- package/fesm2022/ngstarter-ui-components-toolbar.mjs.map +1 -1
- package/fesm2022/ngstarter-ui-components-tree.mjs +146 -14
- package/fesm2022/ngstarter-ui-components-tree.mjs.map +1 -1
- package/package.json +5 -1
- package/types/ngstarter-ui-components-form-builder.d.ts +317 -0
- package/types/ngstarter-ui-components-tree.d.ts +38 -4
|
@@ -0,0 +1,1695 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, input, signal, computed, viewChild, ViewContainerRef, effect, ChangeDetectionStrategy, Component, inject, model, output, ElementRef } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/forms';
|
|
4
|
+
import { Validators, ReactiveFormsModule, FormControl, FormGroup, FormsModule } from '@angular/forms';
|
|
5
|
+
import { moveItemInArray, transferArrayItem, CdkDrag, CdkDragHandle, CdkDragPlaceholder, CdkDragPreview, CdkDropList, CdkDropListGroup } from '@angular/cdk/drag-drop';
|
|
6
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
7
|
+
import { Button } from '@ngstarter-ui/components/button';
|
|
8
|
+
import { Card, CardAside, CardContent, CardHeader } from '@ngstarter-ui/components/card';
|
|
9
|
+
import { ConfirmManager } from '@ngstarter-ui/components/confirm';
|
|
10
|
+
import { Dialog, DialogActions, DialogClose, DialogContent, DialogTitle } from '@ngstarter-ui/components/dialog';
|
|
11
|
+
import { FormField, Hint, IconButtonSuffix, Label } from '@ngstarter-ui/components/form-field';
|
|
12
|
+
import { Icon } from '@ngstarter-ui/components/icon';
|
|
13
|
+
import { Input } from '@ngstarter-ui/components/input';
|
|
14
|
+
import { Panel, PanelAside, PanelContent, PanelHeader, PanelSidebar } from '@ngstarter-ui/components/panel';
|
|
15
|
+
import { ScrollbarArea } from '@ngstarter-ui/components/scrollbar-area';
|
|
16
|
+
import { Tab, TabGroup } from '@ngstarter-ui/components/tabs';
|
|
17
|
+
import { Tree, TreeDragPlaceholder, TreeNode, TreeNodeDef, TreeNodePadding } from '@ngstarter-ui/components/tree';
|
|
18
|
+
import { Datepicker, DatepickerInput, DatepickerToggle, provideNativeDateAdapter } from '@ngstarter-ui/components/datepicker';
|
|
19
|
+
import { Select } from '@ngstarter-ui/components/select';
|
|
20
|
+
import { Option } from '@ngstarter-ui/components/option';
|
|
21
|
+
import { Checkbox } from '@ngstarter-ui/components/checkbox';
|
|
22
|
+
import { SlideToggle } from '@ngstarter-ui/components/slide-toggle';
|
|
23
|
+
|
|
24
|
+
const FORM_BUILDER_FIELDS = new InjectionToken('FORM_BUILDER_FIELDS');
|
|
25
|
+
const FORM_BUILDER_SETTINGS = new InjectionToken('FORM_BUILDER_SETTINGS');
|
|
26
|
+
function formBuilderField(definition) {
|
|
27
|
+
return definition;
|
|
28
|
+
}
|
|
29
|
+
function formBuilderSettings(definition) {
|
|
30
|
+
return definition;
|
|
31
|
+
}
|
|
32
|
+
function provideFormBuilder(config = {}) {
|
|
33
|
+
return makeEnvironmentProviders([
|
|
34
|
+
...(config.fields ?? []).map(field => ({
|
|
35
|
+
provide: FORM_BUILDER_FIELDS,
|
|
36
|
+
useValue: field,
|
|
37
|
+
multi: true
|
|
38
|
+
})),
|
|
39
|
+
...(config.settings ?? []).map(settings => ({
|
|
40
|
+
provide: FORM_BUILDER_SETTINGS,
|
|
41
|
+
useValue: settings,
|
|
42
|
+
multi: true
|
|
43
|
+
}))
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
const DEFAULT_FORM_BUILDER_FIELDS = [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
label: 'Text',
|
|
50
|
+
group: 'Basic',
|
|
51
|
+
icon: 'fluent:text-font-24-regular',
|
|
52
|
+
defaults: {
|
|
53
|
+
label: 'Text field',
|
|
54
|
+
placeholder: 'Enter text'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'number',
|
|
59
|
+
label: 'Number',
|
|
60
|
+
group: 'Basic',
|
|
61
|
+
icon: 'fluent:number-symbol-24-regular',
|
|
62
|
+
defaults: {
|
|
63
|
+
label: 'Number',
|
|
64
|
+
placeholder: '0'
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'email',
|
|
69
|
+
label: 'Email',
|
|
70
|
+
group: 'Basic',
|
|
71
|
+
icon: 'fluent:mail-24-regular',
|
|
72
|
+
defaults: {
|
|
73
|
+
label: 'Email',
|
|
74
|
+
placeholder: 'name@example.com'
|
|
75
|
+
},
|
|
76
|
+
validators: field => field.required ? [Validators.required, Validators.email] : [Validators.email]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'textarea',
|
|
80
|
+
label: 'Textarea',
|
|
81
|
+
group: 'Basic',
|
|
82
|
+
icon: 'fluent:text-description-24-regular',
|
|
83
|
+
defaults: {
|
|
84
|
+
label: 'Description',
|
|
85
|
+
placeholder: 'Enter description',
|
|
86
|
+
width: 12
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'grid',
|
|
91
|
+
label: 'Grid',
|
|
92
|
+
group: 'Layout',
|
|
93
|
+
icon: 'fluent:grid-24-regular',
|
|
94
|
+
description: 'Container for nested fields.',
|
|
95
|
+
defaults: {
|
|
96
|
+
label: 'Grid',
|
|
97
|
+
width: 12,
|
|
98
|
+
children: []
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'select',
|
|
103
|
+
label: 'Select',
|
|
104
|
+
group: 'Choices',
|
|
105
|
+
icon: 'fluent:multiselect-ltr-24-regular',
|
|
106
|
+
defaults: {
|
|
107
|
+
label: 'Select',
|
|
108
|
+
placeholder: 'Choose an option',
|
|
109
|
+
options: [
|
|
110
|
+
{ label: 'Option 1', value: 'option_1' },
|
|
111
|
+
{ label: 'Option 2', value: 'option_2' }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'checkbox',
|
|
117
|
+
label: 'Checkbox',
|
|
118
|
+
group: 'Choices',
|
|
119
|
+
icon: 'fluent:checkbox-checked-24-regular',
|
|
120
|
+
defaults: {
|
|
121
|
+
label: 'Checkbox',
|
|
122
|
+
defaultValue: false
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'toggle',
|
|
127
|
+
label: 'Toggle',
|
|
128
|
+
group: 'Choices',
|
|
129
|
+
icon: 'fluent:toggle-right-24-regular',
|
|
130
|
+
defaults: {
|
|
131
|
+
label: 'Toggle',
|
|
132
|
+
defaultValue: false
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'date',
|
|
137
|
+
label: 'Date',
|
|
138
|
+
group: 'Date and time',
|
|
139
|
+
icon: 'fluent:calendar-ltr-24-regular',
|
|
140
|
+
defaults: {
|
|
141
|
+
label: 'Date',
|
|
142
|
+
placeholder: 'Select date'
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'currency',
|
|
147
|
+
label: 'Currency',
|
|
148
|
+
group: 'Finance',
|
|
149
|
+
icon: 'fluent:money-24-regular',
|
|
150
|
+
defaults: {
|
|
151
|
+
label: 'Amount',
|
|
152
|
+
placeholder: '0.00'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
function validatorsFromRules(rules = [], field) {
|
|
157
|
+
const validators = [];
|
|
158
|
+
if (field?.required || rules.some(rule => rule.type === 'required')) {
|
|
159
|
+
validators.push(Validators.required);
|
|
160
|
+
}
|
|
161
|
+
for (const rule of rules) {
|
|
162
|
+
if (rule.type === 'email') {
|
|
163
|
+
validators.push(Validators.email);
|
|
164
|
+
}
|
|
165
|
+
else if (rule.type === 'minLength') {
|
|
166
|
+
validators.push(Validators.minLength(Number(rule.value)));
|
|
167
|
+
}
|
|
168
|
+
else if (rule.type === 'maxLength') {
|
|
169
|
+
validators.push(Validators.maxLength(Number(rule.value)));
|
|
170
|
+
}
|
|
171
|
+
else if (rule.type === 'min') {
|
|
172
|
+
validators.push(Validators.min(Number(rule.value)));
|
|
173
|
+
}
|
|
174
|
+
else if (rule.type === 'max') {
|
|
175
|
+
validators.push(Validators.max(Number(rule.value)));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return validators;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
class FormBuilderFieldHost {
|
|
182
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
183
|
+
control = input.required(...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
|
|
184
|
+
definitions = input([], ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
185
|
+
readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
186
|
+
editableCanvas = input(false, ...(ngDevMode ? [{ debugName: "editableCanvas" }] : /* istanbul ignore next */ []));
|
|
187
|
+
customLoaded = signal(false, ...(ngDevMode ? [{ debugName: "customLoaded" }] : /* istanbul ignore next */ []));
|
|
188
|
+
textInputType = computed(() => {
|
|
189
|
+
const type = this.field().type;
|
|
190
|
+
return type === 'number' || type === 'email' ? type : 'text';
|
|
191
|
+
}, ...(ngDevMode ? [{ debugName: "textInputType" }] : /* istanbul ignore next */ []));
|
|
192
|
+
anchor = viewChild.required('anchor', { read: ViewContainerRef });
|
|
193
|
+
constructor() {
|
|
194
|
+
effect(async () => {
|
|
195
|
+
const field = this.field();
|
|
196
|
+
const control = this.control();
|
|
197
|
+
const definitions = this.definitions();
|
|
198
|
+
const viewContainer = this.anchor();
|
|
199
|
+
const renderer = definitions.find(definition => definition.type === field.type)?.renderer;
|
|
200
|
+
viewContainer.clear();
|
|
201
|
+
this.customLoaded.set(false);
|
|
202
|
+
if (!renderer) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const componentType = await renderer();
|
|
206
|
+
const componentRef = viewContainer.createComponent(componentType);
|
|
207
|
+
componentRef.setInput('field', field);
|
|
208
|
+
componentRef.setInput('control', control);
|
|
209
|
+
componentRef.setInput('readonly', this.readonly());
|
|
210
|
+
this.customLoaded.set(true);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilderFieldHost, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
214
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: FormBuilderFieldHost, isStandalone: true, selector: "ngs-form-builder-field-host", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: true, transformFunction: null }, definitions: { classPropertyName: "definitions", publicName: "definitions", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, editableCanvas: { classPropertyName: "editableCanvas", publicName: "editableCanvas", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.is-custom": "customLoaded()", "class.is-width-1": "!editableCanvas() && field().width === 1", "class.is-width-2": "!editableCanvas() && field().width === 2", "class.is-width-3": "!editableCanvas() && field().width === 3", "class.is-width-4": "!editableCanvas() && field().width === 4", "class.is-width-5": "!editableCanvas() && field().width === 5", "class.is-width-6": "!editableCanvas() && field().width === 6", "class.is-width-7": "!editableCanvas() && field().width === 7", "class.is-width-8": "!editableCanvas() && field().width === 8", "class.is-width-9": "!editableCanvas() && field().width === 9", "class.is-width-10": "!editableCanvas() && field().width === 10", "class.is-width-11": "!editableCanvas() && field().width === 11", "class.is-width-12": "!editableCanvas() && (field().width ?? 12) === 12" }, classAttribute: "ngs-form-builder-field-host" }, providers: [
|
|
215
|
+
provideNativeDateAdapter()
|
|
216
|
+
], viewQueries: [{ propertyName: "anchor", first: true, predicate: ["anchor"], descendants: true, read: ViewContainerRef, isSignal: true }], exportAs: ["ngsFormBuilderFieldHost"], ngImport: i0, template: "<ng-container #anchor/>\n\n@if (!customLoaded()) {\n @switch (field().type) {\n @case ('textarea') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <textarea ngsInput\n rows=\"3\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\"></textarea>\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @case ('select') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <ngs-select [formControl]=\"control()\" [placeholder]=\"field().placeholder || ''\">\n @for (option of field().options ?? []; track option.value) {\n <ngs-option [value]=\"option.value\">{{ option.label }}</ngs-option>\n }\n </ngs-select>\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @case ('checkbox') {\n <div class=\"ngs-form-builder-check-field\">\n <ngs-checkbox [formControl]=\"control()\" [disabled]=\"readonly() || (field().disabled ?? false)\">\n {{ field().label }}\n </ngs-checkbox>\n @if (field().hint) {\n <span>{{ field().hint }}</span>\n }\n </div>\n }\n\n @case ('toggle') {\n <div class=\"ngs-form-builder-check-field\">\n <ngs-slide-toggle [formControl]=\"control()\" [disabled]=\"readonly() || (field().disabled ?? false)\">\n {{ field().label }}\n </ngs-slide-toggle>\n @if (field().hint) {\n <span>{{ field().hint }}</span>\n }\n </div>\n }\n\n @case ('date') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <input ngsInput\n [ngsDatepicker]=\"picker\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\">\n <ngs-datepicker-toggle ngsIconButtonSuffix [for]=\"picker\"/>\n <ngs-datepicker #picker/>\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @case ('currency') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <input ngsInput\n type=\"number\"\n inputmode=\"decimal\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\">\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @default {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <input ngsInput\n [type]=\"textInputType()\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\">\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n }\n}\n", styles: [":host{display:block;min-width:0}:host.is-width-1{grid-column:span 1}:host.is-width-2{grid-column:span 2}:host.is-width-3{grid-column:span 3}:host.is-width-4{grid-column:span 4}:host.is-width-5{grid-column:span 5}:host.is-width-6{grid-column:span 6}:host.is-width-7{grid-column:span 7}:host.is-width-8{grid-column:span 8}:host.is-width-9{grid-column:span 9}:host.is-width-10{grid-column:span 10}:host.is-width-11{grid-column:span 11}:host.is-width-12{grid-column:span 12}:host .ngs-form-builder-check-field{display:flex;min-height:calc(var(--spacing, .25rem) * 11);flex-direction:column;justify-content:center;gap:calc(var(--spacing, .25rem) * 1)}:host .ngs-form-builder-check-field span{color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-xs)}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: Datepicker, selector: "ngs-datepicker", inputs: ["startAt", "calendarHeaderComponent", "quickPresets", "showQuickPresets"], exportAs: ["ngsDatepicker"] }, { kind: "directive", type: DatepickerInput, selector: "input[ngsDatepicker]", inputs: ["ngsDatepicker"], exportAs: ["ngsDatepickerInput"] }, { kind: "component", type: DatepickerToggle, selector: "ngs-datepicker-toggle", inputs: ["for"], exportAs: ["ngsDatepickerToggle"] }, { kind: "component", type: FormField, selector: "ngs-form-field", inputs: ["subscriptHiddenIfEmpty", "sameHeightAsButton"], exportAs: ["ngsFormField"] }, { kind: "component", type: Hint, selector: "ngs-hint", inputs: ["align"], exportAs: ["ngsHint"] }, { kind: "directive", type: IconButtonSuffix, selector: "[ngsIconButtonSuffix]" }, { kind: "component", type: Label, selector: "ngs-label" }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "component", type: Select, selector: "ngs-select", inputs: ["id", "placeholder", "disabled", "required", "multiple", "hideCheckIcon", "clearable", "aria-label", "tabIndex", "aria-describedby", "value"], outputs: ["selectionChange", "opened", "closed", "valueChange"], exportAs: ["ngsSelect"] }, { kind: "component", type: Option, selector: "ngs-option", inputs: ["value", "data", "disabled", "selected"], outputs: ["onSelectionChange"], exportAs: ["ngsOption"] }, { kind: "component", type: Checkbox, selector: "ngs-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: ["checkedChange", "disabledChange", "indeterminateChange", "change"], exportAs: ["ngsCheckbox"] }, { kind: "component", type: SlideToggle, selector: "ngs-slide-toggle", inputs: ["id", "name", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "disabled", "disableRipple", "tabIndex", "hideIcon", "color", "checked"], outputs: ["disabledChange", "checkedChange", "change", "toggleChange"], exportAs: ["ngsSlideToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
217
|
+
}
|
|
218
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilderFieldHost, decorators: [{
|
|
219
|
+
type: Component,
|
|
220
|
+
args: [{ selector: 'ngs-form-builder-field-host', exportAs: 'ngsFormBuilderFieldHost', imports: [
|
|
221
|
+
ReactiveFormsModule,
|
|
222
|
+
Datepicker,
|
|
223
|
+
DatepickerInput,
|
|
224
|
+
DatepickerToggle,
|
|
225
|
+
FormField,
|
|
226
|
+
Hint,
|
|
227
|
+
IconButtonSuffix,
|
|
228
|
+
Label,
|
|
229
|
+
Input,
|
|
230
|
+
Select,
|
|
231
|
+
Option,
|
|
232
|
+
Checkbox,
|
|
233
|
+
SlideToggle
|
|
234
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
235
|
+
provideNativeDateAdapter()
|
|
236
|
+
], host: {
|
|
237
|
+
'class': 'ngs-form-builder-field-host',
|
|
238
|
+
'[class.is-custom]': 'customLoaded()',
|
|
239
|
+
'[class.is-width-1]': '!editableCanvas() && field().width === 1',
|
|
240
|
+
'[class.is-width-2]': '!editableCanvas() && field().width === 2',
|
|
241
|
+
'[class.is-width-3]': '!editableCanvas() && field().width === 3',
|
|
242
|
+
'[class.is-width-4]': '!editableCanvas() && field().width === 4',
|
|
243
|
+
'[class.is-width-5]': '!editableCanvas() && field().width === 5',
|
|
244
|
+
'[class.is-width-6]': '!editableCanvas() && field().width === 6',
|
|
245
|
+
'[class.is-width-7]': '!editableCanvas() && field().width === 7',
|
|
246
|
+
'[class.is-width-8]': '!editableCanvas() && field().width === 8',
|
|
247
|
+
'[class.is-width-9]': '!editableCanvas() && field().width === 9',
|
|
248
|
+
'[class.is-width-10]': '!editableCanvas() && field().width === 10',
|
|
249
|
+
'[class.is-width-11]': '!editableCanvas() && field().width === 11',
|
|
250
|
+
'[class.is-width-12]': '!editableCanvas() && (field().width ?? 12) === 12'
|
|
251
|
+
}, template: "<ng-container #anchor/>\n\n@if (!customLoaded()) {\n @switch (field().type) {\n @case ('textarea') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <textarea ngsInput\n rows=\"3\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\"></textarea>\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @case ('select') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <ngs-select [formControl]=\"control()\" [placeholder]=\"field().placeholder || ''\">\n @for (option of field().options ?? []; track option.value) {\n <ngs-option [value]=\"option.value\">{{ option.label }}</ngs-option>\n }\n </ngs-select>\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @case ('checkbox') {\n <div class=\"ngs-form-builder-check-field\">\n <ngs-checkbox [formControl]=\"control()\" [disabled]=\"readonly() || (field().disabled ?? false)\">\n {{ field().label }}\n </ngs-checkbox>\n @if (field().hint) {\n <span>{{ field().hint }}</span>\n }\n </div>\n }\n\n @case ('toggle') {\n <div class=\"ngs-form-builder-check-field\">\n <ngs-slide-toggle [formControl]=\"control()\" [disabled]=\"readonly() || (field().disabled ?? false)\">\n {{ field().label }}\n </ngs-slide-toggle>\n @if (field().hint) {\n <span>{{ field().hint }}</span>\n }\n </div>\n }\n\n @case ('date') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <input ngsInput\n [ngsDatepicker]=\"picker\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\">\n <ngs-datepicker-toggle ngsIconButtonSuffix [for]=\"picker\"/>\n <ngs-datepicker #picker/>\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @case ('currency') {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <input ngsInput\n type=\"number\"\n inputmode=\"decimal\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\">\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n\n @default {\n <ngs-form-field>\n <ngs-label>{{ field().label }}</ngs-label>\n <input ngsInput\n [type]=\"textInputType()\"\n [formControl]=\"control()\"\n [placeholder]=\"field().placeholder || ''\"\n [readonly]=\"readonly() || field().readonly\">\n @if (field().hint) {\n <ngs-hint>{{ field().hint }}</ngs-hint>\n }\n </ngs-form-field>\n }\n }\n}\n", styles: [":host{display:block;min-width:0}:host.is-width-1{grid-column:span 1}:host.is-width-2{grid-column:span 2}:host.is-width-3{grid-column:span 3}:host.is-width-4{grid-column:span 4}:host.is-width-5{grid-column:span 5}:host.is-width-6{grid-column:span 6}:host.is-width-7{grid-column:span 7}:host.is-width-8{grid-column:span 8}:host.is-width-9{grid-column:span 9}:host.is-width-10{grid-column:span 10}:host.is-width-11{grid-column:span 11}:host.is-width-12{grid-column:span 12}:host .ngs-form-builder-check-field{display:flex;min-height:calc(var(--spacing, .25rem) * 11);flex-direction:column;justify-content:center;gap:calc(var(--spacing, .25rem) * 1)}:host .ngs-form-builder-check-field span{color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-xs)}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
252
|
+
}], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: true }] }], definitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "definitions", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], editableCanvas: [{ type: i0.Input, args: [{ isSignal: true, alias: "editableCanvas", required: false }] }], anchor: [{ type: i0.ViewChild, args: ['anchor', { ...{ read: ViewContainerRef }, isSignal: true }] }] } });
|
|
253
|
+
|
|
254
|
+
class FormBuilderRenderer {
|
|
255
|
+
providedFields = inject(FORM_BUILDER_FIELDS, { optional: true }) ?? [];
|
|
256
|
+
schema = input.required(...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
|
|
257
|
+
readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
258
|
+
showSubmit = input(true, ...(ngDevMode ? [{ debugName: "showSubmit" }] : /* istanbul ignore next */ []));
|
|
259
|
+
submitLabel = input('Submit', ...(ngDevMode ? [{ debugName: "submitLabel" }] : /* istanbul ignore next */ []));
|
|
260
|
+
value = model({}, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
261
|
+
formSubmit = output();
|
|
262
|
+
formReady = output();
|
|
263
|
+
definitions = computed(() => [
|
|
264
|
+
...DEFAULT_FORM_BUILDER_FIELDS,
|
|
265
|
+
...this.providedFields
|
|
266
|
+
], ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
267
|
+
visibleCanvasItems = computed(() => this.resolveCanvasItems(this.schema())
|
|
268
|
+
.map(item => {
|
|
269
|
+
if (item.field) {
|
|
270
|
+
return {
|
|
271
|
+
...item,
|
|
272
|
+
field: this.visibleFields([item.field])[0]
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
...item,
|
|
277
|
+
section: {
|
|
278
|
+
...item.section,
|
|
279
|
+
fields: this.visibleFields(item.section.fields)
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
})
|
|
283
|
+
.filter(item => !!item.field || !!item.section?.fields.length), ...(ngDevMode ? [{ debugName: "visibleCanvasItems" }] : /* istanbul ignore next */ []));
|
|
284
|
+
formGroup = computed(() => this.createFormGroup(), ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
|
|
285
|
+
constructor() {
|
|
286
|
+
effect(onCleanup => {
|
|
287
|
+
const form = this.formGroup();
|
|
288
|
+
this.formReady.emit(form);
|
|
289
|
+
const subscription = form.valueChanges.subscribe(() => {
|
|
290
|
+
this.value.set(form.getRawValue());
|
|
291
|
+
});
|
|
292
|
+
onCleanup(() => subscription.unsubscribe());
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
getControl(field) {
|
|
296
|
+
return this.formGroup().get(field.name);
|
|
297
|
+
}
|
|
298
|
+
isContainerField(field) {
|
|
299
|
+
return field.type === 'grid';
|
|
300
|
+
}
|
|
301
|
+
visibleChildren(field) {
|
|
302
|
+
return this.visibleFields(field.children ?? []);
|
|
303
|
+
}
|
|
304
|
+
submit() {
|
|
305
|
+
const form = this.formGroup();
|
|
306
|
+
if (form.invalid) {
|
|
307
|
+
form.markAllAsTouched();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.formSubmit.emit(form.getRawValue());
|
|
311
|
+
}
|
|
312
|
+
createFormGroup() {
|
|
313
|
+
const controls = {};
|
|
314
|
+
const value = this.value();
|
|
315
|
+
const fields = [
|
|
316
|
+
...flattenFields(this.schema().fields ?? []),
|
|
317
|
+
...this.schema().sections.flatMap(section => flattenFields(section.fields))
|
|
318
|
+
];
|
|
319
|
+
for (const field of fields) {
|
|
320
|
+
if (this.isContainerField(field)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const definition = this.definitions().find(item => item.type === field.type);
|
|
324
|
+
const validators = definition?.validators?.(field) ?? validatorsFromRules(field.validation, field);
|
|
325
|
+
const control = new FormControl({
|
|
326
|
+
value: value[field.name] ?? field.defaultValue ?? null,
|
|
327
|
+
disabled: field.disabled || this.readonly()
|
|
328
|
+
}, validators);
|
|
329
|
+
controls[field.name] = control;
|
|
330
|
+
}
|
|
331
|
+
return new FormGroup(controls);
|
|
332
|
+
}
|
|
333
|
+
visibleFields(fields) {
|
|
334
|
+
return fields
|
|
335
|
+
.filter(field => field.visibility?.form !== false)
|
|
336
|
+
.map(field => ({
|
|
337
|
+
...field,
|
|
338
|
+
children: field.children ? this.visibleFields(field.children) : undefined
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
resolveCanvasItems(schema) {
|
|
342
|
+
const fieldsById = new Map((schema.fields ?? []).map(field => [field.id, field]));
|
|
343
|
+
const sectionsById = new Map(schema.sections.map(section => [section.id, section]));
|
|
344
|
+
const items = [];
|
|
345
|
+
for (const item of normalizedLayout(schema)) {
|
|
346
|
+
if (item.kind === 'field') {
|
|
347
|
+
const field = fieldsById.get(item.id);
|
|
348
|
+
if (field) {
|
|
349
|
+
items.push({ ...item, field });
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const section = sectionsById.get(item.id);
|
|
354
|
+
if (section) {
|
|
355
|
+
items.push({ ...item, section });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return items;
|
|
359
|
+
}
|
|
360
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilderRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
361
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: FormBuilderRenderer, isStandalone: true, selector: "ngs-form-builder-renderer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, showSubmit: { classPropertyName: "showSubmit", publicName: "showSubmit", isSignal: true, isRequired: false, transformFunction: null }, submitLabel: { classPropertyName: "submitLabel", publicName: "submitLabel", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", formSubmit: "formSubmit", formReady: "formReady" }, host: { classAttribute: "ngs-form-builder-renderer" }, exportAs: ["ngsFormBuilderRenderer"], ngImport: i0, template: "<ng-template #renderFields let-fields>\n <div class=\"ngs-form-builder-renderer-grid\">\n @for (field of fields; track field.id) {\n @if (isContainerField(field)) {\n <section class=\"ngs-form-builder-renderer-grid-field\">\n <header>\n <h4>{{ field.label }}</h4>\n @if (field.hint) {\n <p>{{ field.hint }}</p>\n }\n </header>\n\n <ng-container\n [ngTemplateOutlet]=\"renderFields\"\n [ngTemplateOutletContext]=\"{ $implicit: visibleChildren(field) }\"/>\n </section>\n } @else {\n <ngs-form-builder-field-host\n [field]=\"field\"\n [control]=\"getControl(field)\"\n [definitions]=\"definitions()\"\n [readonly]=\"readonly()\"/>\n }\n }\n </div>\n</ng-template>\n\n<form [formGroup]=\"formGroup()\" (ngSubmit)=\"submit()\">\n <div class=\"flex flex-col gap-6\">\n @for (item of visibleCanvasItems(); track item.kind + ':' + item.id) {\n @if (item.field) {\n <ng-container\n [ngTemplateOutlet]=\"renderFields\"\n [ngTemplateOutletContext]=\"{ $implicit: [item.field] }\"/>\n } @else if (item.section; as section) {\n <section class=\"ngs-form-builder-renderer-section\">\n @if (section.title || section.description) {\n <header>\n @if (section.title) {\n <h3>{{ section.title }}</h3>\n }\n @if (section.description) {\n <p>{{ section.description }}</p>\n }\n </header>\n }\n\n <ng-container\n [ngTemplateOutlet]=\"renderFields\"\n [ngTemplateOutletContext]=\"{ $implicit: section.fields }\"/>\n </section>\n }\n }\n\n @if (showSubmit() && !readonly()) {\n <div class=\"flex justify-end\">\n <button ngsButton=\"filled\" type=\"submit\">{{ submitLabel() }}</button>\n </div>\n }\n </div>\n</form>\n", styles: [":host{display:block}:host .ngs-form-builder-renderer-section{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-renderer-section header{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 1)}:host .ngs-form-builder-renderer-section header h3{color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-lg);font-weight:600}:host .ngs-form-builder-renderer-section header p{color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-sm)}:host .ngs-form-builder-renderer-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-renderer-grid-field{display:flex;grid-column:1/-1;flex-direction:column;gap:calc(var(--spacing, .25rem) * 4);border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-lg);padding:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-renderer-grid-field>header h4{color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-base);font-weight:600}:host .ngs-form-builder-renderer-grid-field>header p{color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-sm)}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: FormBuilderFieldHost, selector: "ngs-form-builder-field-host", inputs: ["field", "control", "definitions", "readonly", "editableCanvas"], exportAs: ["ngsFormBuilderFieldHost"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
362
|
+
}
|
|
363
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilderRenderer, decorators: [{
|
|
364
|
+
type: Component,
|
|
365
|
+
args: [{ selector: 'ngs-form-builder-renderer', exportAs: 'ngsFormBuilderRenderer', imports: [
|
|
366
|
+
NgTemplateOutlet,
|
|
367
|
+
ReactiveFormsModule,
|
|
368
|
+
Button,
|
|
369
|
+
FormBuilderFieldHost
|
|
370
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
371
|
+
'class': 'ngs-form-builder-renderer'
|
|
372
|
+
}, template: "<ng-template #renderFields let-fields>\n <div class=\"ngs-form-builder-renderer-grid\">\n @for (field of fields; track field.id) {\n @if (isContainerField(field)) {\n <section class=\"ngs-form-builder-renderer-grid-field\">\n <header>\n <h4>{{ field.label }}</h4>\n @if (field.hint) {\n <p>{{ field.hint }}</p>\n }\n </header>\n\n <ng-container\n [ngTemplateOutlet]=\"renderFields\"\n [ngTemplateOutletContext]=\"{ $implicit: visibleChildren(field) }\"/>\n </section>\n } @else {\n <ngs-form-builder-field-host\n [field]=\"field\"\n [control]=\"getControl(field)\"\n [definitions]=\"definitions()\"\n [readonly]=\"readonly()\"/>\n }\n }\n </div>\n</ng-template>\n\n<form [formGroup]=\"formGroup()\" (ngSubmit)=\"submit()\">\n <div class=\"flex flex-col gap-6\">\n @for (item of visibleCanvasItems(); track item.kind + ':' + item.id) {\n @if (item.field) {\n <ng-container\n [ngTemplateOutlet]=\"renderFields\"\n [ngTemplateOutletContext]=\"{ $implicit: [item.field] }\"/>\n } @else if (item.section; as section) {\n <section class=\"ngs-form-builder-renderer-section\">\n @if (section.title || section.description) {\n <header>\n @if (section.title) {\n <h3>{{ section.title }}</h3>\n }\n @if (section.description) {\n <p>{{ section.description }}</p>\n }\n </header>\n }\n\n <ng-container\n [ngTemplateOutlet]=\"renderFields\"\n [ngTemplateOutletContext]=\"{ $implicit: section.fields }\"/>\n </section>\n }\n }\n\n @if (showSubmit() && !readonly()) {\n <div class=\"flex justify-end\">\n <button ngsButton=\"filled\" type=\"submit\">{{ submitLabel() }}</button>\n </div>\n }\n </div>\n</form>\n", styles: [":host{display:block}:host .ngs-form-builder-renderer-section{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-renderer-section header{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 1)}:host .ngs-form-builder-renderer-section header h3{color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-lg);font-weight:600}:host .ngs-form-builder-renderer-section header p{color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-sm)}:host .ngs-form-builder-renderer-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-renderer-grid-field{display:flex;grid-column:1/-1;flex-direction:column;gap:calc(var(--spacing, .25rem) * 4);border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-lg);padding:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-renderer-grid-field>header h4{color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-base);font-weight:600}:host .ngs-form-builder-renderer-grid-field>header p{color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-sm)}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
373
|
+
}], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], showSubmit: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSubmit", required: false }] }], submitLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitLabel", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], formReady: [{ type: i0.Output, args: ["formReady"] }] } });
|
|
374
|
+
function flattenFields(fields) {
|
|
375
|
+
return fields.flatMap(field => [field, ...flattenFields(field.children ?? [])]);
|
|
376
|
+
}
|
|
377
|
+
function normalizedLayout(schema) {
|
|
378
|
+
const used = new Set();
|
|
379
|
+
const layout = [];
|
|
380
|
+
const fieldsById = new Map((schema.fields ?? []).map(field => [field.id, field]));
|
|
381
|
+
const sectionsById = new Map(schema.sections.map(section => [section.id, section]));
|
|
382
|
+
for (const item of schema.layout ?? []) {
|
|
383
|
+
const key = `${item.kind}:${item.id}`;
|
|
384
|
+
if (used.has(key)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if ((item.kind === 'field' && fieldsById.has(item.id)) || (item.kind === 'section' && sectionsById.has(item.id))) {
|
|
388
|
+
used.add(key);
|
|
389
|
+
layout.push(item);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const field of schema.fields ?? []) {
|
|
393
|
+
const key = `field:${field.id}`;
|
|
394
|
+
if (!used.has(key)) {
|
|
395
|
+
used.add(key);
|
|
396
|
+
layout.push({ kind: 'field', id: field.id });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const section of schema.sections) {
|
|
400
|
+
const key = `section:${section.id}`;
|
|
401
|
+
if (!used.has(key)) {
|
|
402
|
+
used.add(key);
|
|
403
|
+
layout.push({ kind: 'section', id: section.id });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return layout;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
class FormBuilderSettingsHost {
|
|
410
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
411
|
+
schema = input.required(...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
|
|
412
|
+
definitions = input([], ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
413
|
+
settingsDefinitions = input([], ...(ngDevMode ? [{ debugName: "settingsDefinitions" }] : /* istanbul ignore next */ []));
|
|
414
|
+
update = input.required(...(ngDevMode ? [{ debugName: "update" }] : /* istanbul ignore next */ []));
|
|
415
|
+
customLoaded = signal(false, ...(ngDevMode ? [{ debugName: "customLoaded" }] : /* istanbul ignore next */ []));
|
|
416
|
+
anchor = viewChild.required('anchor', { read: ViewContainerRef });
|
|
417
|
+
constructor() {
|
|
418
|
+
effect(async () => {
|
|
419
|
+
const field = this.field();
|
|
420
|
+
const definitions = this.definitions();
|
|
421
|
+
const settingDefinitions = this.settingsDefinitions();
|
|
422
|
+
const viewContainer = this.anchor();
|
|
423
|
+
const fieldDefinition = definitions.find(definition => definition.type === field.type);
|
|
424
|
+
const customSettings = fieldDefinition?.settings ??
|
|
425
|
+
settingDefinitions.find(definition => definition.fieldType === field.type)?.component;
|
|
426
|
+
viewContainer.clear();
|
|
427
|
+
this.customLoaded.set(false);
|
|
428
|
+
if (!customSettings) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const componentType = await customSettings();
|
|
432
|
+
const componentRef = viewContainer.createComponent(componentType);
|
|
433
|
+
componentRef.setInput('field', field);
|
|
434
|
+
componentRef.setInput('schema', this.schema());
|
|
435
|
+
componentRef.setInput('update', this.update());
|
|
436
|
+
this.customLoaded.set(true);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilderSettingsHost, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
440
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.4", type: FormBuilderSettingsHost, isStandalone: true, selector: "ngs-form-builder-settings-host", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, definitions: { classPropertyName: "definitions", publicName: "definitions", isSignal: true, isRequired: false, transformFunction: null }, settingsDefinitions: { classPropertyName: "settingsDefinitions", publicName: "settingsDefinitions", isSignal: true, isRequired: false, transformFunction: null }, update: { classPropertyName: "update", publicName: "update", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.is-empty": "!customLoaded()" }, classAttribute: "ngs-form-builder-settings-host" }, viewQueries: [{ propertyName: "anchor", first: true, predicate: ["anchor"], descendants: true, read: ViewContainerRef, isSignal: true }], exportAs: ["ngsFormBuilderSettingsHost"], ngImport: i0, template: "<ng-container #anchor/>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
441
|
+
}
|
|
442
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilderSettingsHost, decorators: [{
|
|
443
|
+
type: Component,
|
|
444
|
+
args: [{ selector: 'ngs-form-builder-settings-host', exportAs: 'ngsFormBuilderSettingsHost', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
445
|
+
'class': 'ngs-form-builder-settings-host',
|
|
446
|
+
'[class.is-empty]': '!customLoaded()'
|
|
447
|
+
}, template: "<ng-container #anchor/>\n" }]
|
|
448
|
+
}], ctorParameters: () => [], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], definitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "definitions", required: false }] }], settingsDefinitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "settingsDefinitions", required: false }] }], update: [{ type: i0.Input, args: [{ isSignal: true, alias: "update", required: true }] }], anchor: [{ type: i0.ViewChild, args: ['anchor', { ...{ read: ViewContainerRef }, isSignal: true }] }] } });
|
|
449
|
+
|
|
450
|
+
class BasicFormBuilderFieldSettings {
|
|
451
|
+
field = input.required(...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
452
|
+
update = input.required(...(ngDevMode ? [{ debugName: "update" }] : /* istanbul ignore next */ []));
|
|
453
|
+
fieldWidthOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
454
|
+
hasOptions = computed(() => ['select', 'radio', 'checkbox-list'].includes(this.field().type), ...(ngDevMode ? [{ debugName: "hasOptions" }] : /* istanbul ignore next */ []));
|
|
455
|
+
optionsText = computed(() => (this.field().options ?? [])
|
|
456
|
+
.map(option => `${option.label}:${option.value}`)
|
|
457
|
+
.join('\n'), ...(ngDevMode ? [{ debugName: "optionsText" }] : /* istanbul ignore next */ []));
|
|
458
|
+
patch(changes) {
|
|
459
|
+
this.update()(changes);
|
|
460
|
+
}
|
|
461
|
+
patchOptions(value) {
|
|
462
|
+
const options = value
|
|
463
|
+
.split('\n')
|
|
464
|
+
.map(line => line.trim())
|
|
465
|
+
.filter(Boolean)
|
|
466
|
+
.map((line, index) => {
|
|
467
|
+
const [label, optionValue] = line.split(':');
|
|
468
|
+
return {
|
|
469
|
+
label: label?.trim() || `Option ${index + 1}`,
|
|
470
|
+
value: (optionValue ?? label ?? `option_${index + 1}`).trim()
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
this.patch({ options });
|
|
474
|
+
}
|
|
475
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BasicFormBuilderFieldSettings, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
476
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: BasicFormBuilderFieldSettings, isStandalone: true, selector: "ngs-basic-form-builder-field-settings", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, update: { classPropertyName: "update", publicName: "update", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "ngs-basic-form-builder-field-settings" }, exportAs: ["ngsBasicFormBuilderFieldSettings"], ngImport: i0, template: "<div class=\"flex flex-col gap-4\">\n <ngs-form-field>\n <ngs-label>Label</ngs-label>\n <input ngsInput\n [ngModel]=\"field().label\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ label: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Field ID</ngs-label>\n <input ngsInput\n [ngModel]=\"field().name\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ name: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Placeholder</ngs-label>\n <input ngsInput\n [ngModel]=\"field().placeholder || ''\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ placeholder: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Hint</ngs-label>\n <input ngsInput\n [ngModel]=\"field().hint || ''\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ hint: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Width</ngs-label>\n <ngs-select [ngModel]=\"field().width ?? 12\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ width: $event })\">\n @for (width of fieldWidthOptions; track width) {\n <ngs-option [value]=\"width\">{{ width }}/12</ngs-option>\n }\n </ngs-select>\n </ngs-form-field>\n\n <div class=\"flex flex-col gap-3 py-1\">\n <ngs-slide-toggle [ngModel]=\"field().required ?? false\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ required: $event })\">\n Required field\n </ngs-slide-toggle>\n\n <ngs-slide-toggle [ngModel]=\"field().readonly ?? false\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ readonly: $event })\">\n Readonly\n </ngs-slide-toggle>\n\n <ngs-slide-toggle [ngModel]=\"field().disabled ?? false\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ disabled: $event })\">\n Disabled\n </ngs-slide-toggle>\n </div>\n\n @if (hasOptions()) {\n <ngs-form-field>\n <ngs-label>Options</ngs-label>\n <textarea ngsInput\n rows=\"4\"\n [ngModel]=\"optionsText()\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patchOptions($event)\"></textarea>\n <ngs-hint>One option per line. Use Label:value to set explicit values.</ngs-hint>\n </ngs-form-field>\n }\n</div>\n", styles: [":host{display:block}:host .settings-section{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 3);border-top:1px solid var(--ngs-color-outline-variant);padding-top:calc(var(--spacing, .25rem) * 4)}:host .settings-section h4{color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-sm);font-weight:600}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: FormField, selector: "ngs-form-field", inputs: ["subscriptHiddenIfEmpty", "sameHeightAsButton"], exportAs: ["ngsFormField"] }, { kind: "component", type: Hint, selector: "ngs-hint", inputs: ["align"], exportAs: ["ngsHint"] }, { kind: "component", type: Label, selector: "ngs-label" }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "component", type: Select, selector: "ngs-select", inputs: ["id", "placeholder", "disabled", "required", "multiple", "hideCheckIcon", "clearable", "aria-label", "tabIndex", "aria-describedby", "value"], outputs: ["selectionChange", "opened", "closed", "valueChange"], exportAs: ["ngsSelect"] }, { kind: "component", type: Option, selector: "ngs-option", inputs: ["value", "data", "disabled", "selected"], outputs: ["onSelectionChange"], exportAs: ["ngsOption"] }, { kind: "component", type: SlideToggle, selector: "ngs-slide-toggle", inputs: ["id", "name", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "disabled", "disableRipple", "tabIndex", "hideIcon", "color", "checked"], outputs: ["disabledChange", "checkedChange", "change", "toggleChange"], exportAs: ["ngsSlideToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
477
|
+
}
|
|
478
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BasicFormBuilderFieldSettings, decorators: [{
|
|
479
|
+
type: Component,
|
|
480
|
+
args: [{ selector: 'ngs-basic-form-builder-field-settings', exportAs: 'ngsBasicFormBuilderFieldSettings', imports: [
|
|
481
|
+
FormsModule,
|
|
482
|
+
FormField,
|
|
483
|
+
Hint,
|
|
484
|
+
Label,
|
|
485
|
+
Input,
|
|
486
|
+
Select,
|
|
487
|
+
Option,
|
|
488
|
+
SlideToggle
|
|
489
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
490
|
+
'class': 'ngs-basic-form-builder-field-settings'
|
|
491
|
+
}, template: "<div class=\"flex flex-col gap-4\">\n <ngs-form-field>\n <ngs-label>Label</ngs-label>\n <input ngsInput\n [ngModel]=\"field().label\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ label: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Field ID</ngs-label>\n <input ngsInput\n [ngModel]=\"field().name\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ name: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Placeholder</ngs-label>\n <input ngsInput\n [ngModel]=\"field().placeholder || ''\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ placeholder: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Hint</ngs-label>\n <input ngsInput\n [ngModel]=\"field().hint || ''\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ hint: $event })\">\n </ngs-form-field>\n\n <ngs-form-field>\n <ngs-label>Width</ngs-label>\n <ngs-select [ngModel]=\"field().width ?? 12\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ width: $event })\">\n @for (width of fieldWidthOptions; track width) {\n <ngs-option [value]=\"width\">{{ width }}/12</ngs-option>\n }\n </ngs-select>\n </ngs-form-field>\n\n <div class=\"flex flex-col gap-3 py-1\">\n <ngs-slide-toggle [ngModel]=\"field().required ?? false\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ required: $event })\">\n Required field\n </ngs-slide-toggle>\n\n <ngs-slide-toggle [ngModel]=\"field().readonly ?? false\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ readonly: $event })\">\n Readonly\n </ngs-slide-toggle>\n\n <ngs-slide-toggle [ngModel]=\"field().disabled ?? false\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patch({ disabled: $event })\">\n Disabled\n </ngs-slide-toggle>\n </div>\n\n @if (hasOptions()) {\n <ngs-form-field>\n <ngs-label>Options</ngs-label>\n <textarea ngsInput\n rows=\"4\"\n [ngModel]=\"optionsText()\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"patchOptions($event)\"></textarea>\n <ngs-hint>One option per line. Use Label:value to set explicit values.</ngs-hint>\n </ngs-form-field>\n }\n</div>\n", styles: [":host{display:block}:host .settings-section{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 3);border-top:1px solid var(--ngs-color-outline-variant);padding-top:calc(var(--spacing, .25rem) * 4)}:host .settings-section h4{color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-sm);font-weight:600}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
492
|
+
}], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], update: [{ type: i0.Input, args: [{ isSignal: true, alias: "update", required: true }] }] } });
|
|
493
|
+
|
|
494
|
+
const ROOT_DROP_LIST_ID = 'ngs-form-builder-root-fields';
|
|
495
|
+
const PALETTE_DRAG_TYPE = 'application/x-ngstarter-form-builder-field';
|
|
496
|
+
const PALETTE_DRAG_ITEM = 'application/x-ngstarter-form-builder-item';
|
|
497
|
+
class FormBuilder {
|
|
498
|
+
dialog = inject(Dialog);
|
|
499
|
+
confirmManager = inject(ConfirmManager);
|
|
500
|
+
elementRef = inject(ElementRef);
|
|
501
|
+
providedFields = inject(FORM_BUILDER_FIELDS, { optional: true }) ?? [];
|
|
502
|
+
providedSettings = inject(FORM_BUILDER_SETTINGS, { optional: true }) ?? [];
|
|
503
|
+
previewControls = new Map();
|
|
504
|
+
actualFieldsTree = viewChild('actualFieldsTree', ...(ngDevMode ? [{ debugName: "actualFieldsTree" }] : /* istanbul ignore next */ []));
|
|
505
|
+
suppressPaletteClick = false;
|
|
506
|
+
schema = model(createDefaultFormBuilderSchema(), ...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
|
|
507
|
+
paletteTitle = input('Fields', ...(ngDevMode ? [{ debugName: "paletteTitle" }] : /* istanbul ignore next */ []));
|
|
508
|
+
inspectorTitle = input('Field properties', ...(ngDevMode ? [{ debugName: "inspectorTitle" }] : /* istanbul ignore next */ []));
|
|
509
|
+
fieldSelected = output();
|
|
510
|
+
fieldAdded = output();
|
|
511
|
+
fieldRemoved = output();
|
|
512
|
+
search = signal('', ...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
513
|
+
selectedFieldId = signal(null, ...(ngDevMode ? [{ debugName: "selectedFieldId" }] : /* istanbul ignore next */ []));
|
|
514
|
+
nativeDragItem = signal(null, ...(ngDevMode ? [{ debugName: "nativeDragItem" }] : /* istanbul ignore next */ []));
|
|
515
|
+
nativeDragFieldDefinition = computed(() => {
|
|
516
|
+
const item = this.nativeDragItem();
|
|
517
|
+
return item?.kind === 'field' ? item.definition : null;
|
|
518
|
+
}, ...(ngDevMode ? [{ debugName: "nativeDragFieldDefinition" }] : /* istanbul ignore next */ []));
|
|
519
|
+
nativeDragSection = computed(() => this.nativeDragItem()?.kind === 'section', ...(ngDevMode ? [{ debugName: "nativeDragSection" }] : /* istanbul ignore next */ []));
|
|
520
|
+
nativeDropTarget = signal(null, ...(ngDevMode ? [{ debugName: "nativeDropTarget" }] : /* istanbul ignore next */ []));
|
|
521
|
+
dragCollapsedSectionIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "dragCollapsedSectionIds" }] : /* istanbul ignore next */ []));
|
|
522
|
+
expandedFieldTreeNodeIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedFieldTreeNodeIds" }] : /* istanbul ignore next */ []));
|
|
523
|
+
definitions = computed(() => [
|
|
524
|
+
...DEFAULT_FORM_BUILDER_FIELDS,
|
|
525
|
+
...this.providedFields
|
|
526
|
+
], ...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
527
|
+
settingsDefinitions = computed(() => this.providedSettings, ...(ngDevMode ? [{ debugName: "settingsDefinitions" }] : /* istanbul ignore next */ []));
|
|
528
|
+
fieldContainerDropListIds = computed(() => this.collectFieldContainerDropListIds(this.schema()), ...(ngDevMode ? [{ debugName: "fieldContainerDropListIds" }] : /* istanbul ignore next */ []));
|
|
529
|
+
canvasItems = computed(() => this.resolveCanvasItems(this.schema()), ...(ngDevMode ? [{ debugName: "canvasItems" }] : /* istanbul ignore next */ []));
|
|
530
|
+
layoutDefinitions = computed(() => {
|
|
531
|
+
const query = this.search().trim().toLowerCase();
|
|
532
|
+
return this.definitions().filter(definition => {
|
|
533
|
+
if ((definition.group || 'Other') !== 'Layout') {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
return this.matchesPaletteQuery(definition, query);
|
|
537
|
+
});
|
|
538
|
+
}, ...(ngDevMode ? [{ debugName: "layoutDefinitions" }] : /* istanbul ignore next */ []));
|
|
539
|
+
paletteGroups = computed(() => {
|
|
540
|
+
const query = this.search().trim().toLowerCase();
|
|
541
|
+
const groups = new Map();
|
|
542
|
+
for (const definition of this.definitions()) {
|
|
543
|
+
const group = definition.group || 'Other';
|
|
544
|
+
if (group === 'Layout' || !this.matchesPaletteQuery(definition, query)) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
groups.set(group, [...(groups.get(group) ?? []), definition]);
|
|
548
|
+
}
|
|
549
|
+
return Array.from(groups, ([name, fields]) => ({ name, fields }));
|
|
550
|
+
}, ...(ngDevMode ? [{ debugName: "paletteGroups" }] : /* istanbul ignore next */ []));
|
|
551
|
+
selectedField = computed(() => {
|
|
552
|
+
const selectedId = this.selectedFieldId();
|
|
553
|
+
return selectedId ? this.findFieldLocation(this.schema(), selectedId)?.field ?? null : null;
|
|
554
|
+
}, ...(ngDevMode ? [{ debugName: "selectedField" }] : /* istanbul ignore next */ []));
|
|
555
|
+
selectedSection = computed(() => {
|
|
556
|
+
const selectedId = this.selectedFieldId();
|
|
557
|
+
return selectedId ? this.findFieldLocation(this.schema(), selectedId)?.section ?? null : null;
|
|
558
|
+
}, ...(ngDevMode ? [{ debugName: "selectedSection" }] : /* istanbul ignore next */ []));
|
|
559
|
+
fieldTree = computed(() => {
|
|
560
|
+
return this.resolveCanvasItems(this.schema()).map(item => {
|
|
561
|
+
if (item.field) {
|
|
562
|
+
return this.createFieldTreeNode(item.field);
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
id: item.section.id,
|
|
566
|
+
label: item.section.title,
|
|
567
|
+
type: 'section',
|
|
568
|
+
icon: 'fluent:folder-24-regular',
|
|
569
|
+
kind: 'section',
|
|
570
|
+
section: item.section,
|
|
571
|
+
children: item.section.fields.map(field => this.createFieldTreeNode(field, item.section))
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
}, ...(ngDevMode ? [{ debugName: "fieldTree" }] : /* istanbul ignore next */ []));
|
|
575
|
+
fieldTreeChildrenAccessor = (node) => node.children ?? [];
|
|
576
|
+
hasFieldTreeChildren = (_, node) => !!node.children?.length;
|
|
577
|
+
trackFieldTreeNode = (_, node) => node.id;
|
|
578
|
+
fieldTreeDraggablePredicate = (_node) => true;
|
|
579
|
+
fieldTreeDropPredicate = (source, target, position) => this.canDropFieldTreeNode(source, target, position);
|
|
580
|
+
updateSelectedField = (changes) => {
|
|
581
|
+
this.patchSelectedField(changes);
|
|
582
|
+
};
|
|
583
|
+
fieldDropListEnterPredicate = (drag) => {
|
|
584
|
+
const data = drag.data;
|
|
585
|
+
return !(isFormBuilderLayoutItem(data) && data.kind === 'section');
|
|
586
|
+
};
|
|
587
|
+
paletteDragStarted(event, definition) {
|
|
588
|
+
this.suppressPaletteClick = true;
|
|
589
|
+
this.nativeDragItem.set({ kind: 'field', definition });
|
|
590
|
+
event.dataTransfer?.setData(PALETTE_DRAG_TYPE, definition.type);
|
|
591
|
+
event.dataTransfer?.setData(PALETTE_DRAG_ITEM, 'field');
|
|
592
|
+
if (event.dataTransfer) {
|
|
593
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
594
|
+
this.setPaletteDragImage(event);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
sectionPaletteDragStarted(event) {
|
|
598
|
+
this.suppressPaletteClick = true;
|
|
599
|
+
this.nativeDragItem.set({ kind: 'section' });
|
|
600
|
+
event.dataTransfer?.setData(PALETTE_DRAG_ITEM, 'section');
|
|
601
|
+
if (event.dataTransfer) {
|
|
602
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
603
|
+
this.setPaletteDragImage(event);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
paletteDragEnded() {
|
|
607
|
+
this.nativeDragItem.set(null);
|
|
608
|
+
this.nativeDropTarget.set(null);
|
|
609
|
+
window.setTimeout(() => {
|
|
610
|
+
this.suppressPaletteClick = false;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
paletteClicked(definition) {
|
|
614
|
+
if (this.suppressPaletteClick) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.addField(definition);
|
|
618
|
+
}
|
|
619
|
+
nativeCanvasDragOver(event) {
|
|
620
|
+
this.nativeDragOver(event, ROOT_DROP_LIST_ID, '.ngs-form-builder-field:not(.ngs-form-builder-native-ghost-field), .ngs-form-builder-section:not(.ngs-form-builder-native-ghost-section)');
|
|
621
|
+
}
|
|
622
|
+
nativeFieldDragOver(event, containerId) {
|
|
623
|
+
if (this.nativeDragItem()?.kind === 'section') {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
event.stopPropagation();
|
|
627
|
+
this.nativeDragOver(event, containerId, '.ngs-form-builder-field:not(.ngs-form-builder-native-ghost-field)');
|
|
628
|
+
}
|
|
629
|
+
nativeSectionDragOver(event, section) {
|
|
630
|
+
if (this.nativeDragItem()?.kind === 'section') {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
event.stopPropagation();
|
|
634
|
+
this.nativeDragOverAtIndex(event, this.sectionDropListId(section), section.fields.length);
|
|
635
|
+
}
|
|
636
|
+
nativeSectionDrop(event, section) {
|
|
637
|
+
if (this.nativeDragItem()?.kind === 'section') {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
event.stopPropagation();
|
|
641
|
+
this.nativeFieldDrop(event, this.sectionDropListId(section));
|
|
642
|
+
}
|
|
643
|
+
nativeContainerFieldDragOver(event, field) {
|
|
644
|
+
if (!this.isContainerField(field)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (this.nativeDragItem()?.kind === 'section') {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
event.stopPropagation();
|
|
651
|
+
this.nativeDragOverAtIndex(event, this.fieldDropListId(field), field.children?.length ?? 0);
|
|
652
|
+
}
|
|
653
|
+
nativeContainerFieldDragLeave(event, field) {
|
|
654
|
+
if (!this.isContainerField(field)) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
this.nativeDragLeave(event, this.fieldDropListId(field));
|
|
658
|
+
}
|
|
659
|
+
nativeContainerFieldDrop(event, field) {
|
|
660
|
+
if (!this.isContainerField(field)) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (this.nativeDragItem()?.kind === 'section') {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
event.stopPropagation();
|
|
667
|
+
this.nativeFieldDrop(event, this.fieldDropListId(field));
|
|
668
|
+
}
|
|
669
|
+
nativeDragLeave(event, containerId) {
|
|
670
|
+
const target = event.currentTarget;
|
|
671
|
+
const relatedTarget = event.relatedTarget;
|
|
672
|
+
if (target instanceof HTMLElement &&
|
|
673
|
+
relatedTarget instanceof Node &&
|
|
674
|
+
target.contains(relatedTarget)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (this.nativeDropTarget()?.containerId === containerId) {
|
|
678
|
+
this.nativeDropTarget.set(null);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
nativeFieldDrop(event, containerId) {
|
|
682
|
+
const item = this.resolveNativeDragItem(event);
|
|
683
|
+
if (!item) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
event.preventDefault();
|
|
687
|
+
event.stopPropagation();
|
|
688
|
+
const target = this.nativeDropTarget();
|
|
689
|
+
const index = target?.containerId === containerId ? target.index : 0;
|
|
690
|
+
if (item.kind === 'section') {
|
|
691
|
+
if (containerId !== ROOT_DROP_LIST_ID) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.insertNativeSection(index);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
this.insertNativeField(item.definition, containerId, index);
|
|
698
|
+
}
|
|
699
|
+
this.nativeDragItem.set(null);
|
|
700
|
+
this.nativeDropTarget.set(null);
|
|
701
|
+
}
|
|
702
|
+
isNativeDropTarget(containerId, index) {
|
|
703
|
+
const target = this.nativeDropTarget();
|
|
704
|
+
return target?.containerId === containerId && target.index === index;
|
|
705
|
+
}
|
|
706
|
+
addField(definition, sectionId) {
|
|
707
|
+
const schema = cloneSchema(this.schema());
|
|
708
|
+
const fields = sectionId
|
|
709
|
+
? schema.sections.find(item => item.id === sectionId)?.fields
|
|
710
|
+
: schema.fields;
|
|
711
|
+
if (!fields) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const field = this.createField(definition, schema);
|
|
715
|
+
const section = sectionId ? schema.sections.find(item => item.id === sectionId) : undefined;
|
|
716
|
+
fields.push(field);
|
|
717
|
+
if (!sectionId) {
|
|
718
|
+
schema.layout = this.normalizedLayout(schema);
|
|
719
|
+
}
|
|
720
|
+
this.schema.set(schema);
|
|
721
|
+
this.selectField(field, section);
|
|
722
|
+
this.fieldAdded.emit({ field, section });
|
|
723
|
+
}
|
|
724
|
+
addSection() {
|
|
725
|
+
const schema = cloneSchema(this.schema());
|
|
726
|
+
schema.sections.push({
|
|
727
|
+
id: uniqueId('section'),
|
|
728
|
+
title: `Section ${schema.sections.length + 1}`,
|
|
729
|
+
fields: []
|
|
730
|
+
});
|
|
731
|
+
schema.layout = this.normalizedLayout(schema);
|
|
732
|
+
this.schema.set(schema);
|
|
733
|
+
}
|
|
734
|
+
openPreview(template) {
|
|
735
|
+
this.dialog.open(template, {
|
|
736
|
+
width: '760px',
|
|
737
|
+
maxWidth: 'calc(100vw - 48px)',
|
|
738
|
+
maxHeight: 'calc(100vh - 48px)',
|
|
739
|
+
ariaLabel: 'Form preview'
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
removeSection(section) {
|
|
743
|
+
const schema = cloneSchema(this.schema());
|
|
744
|
+
schema.sections = schema.sections.filter(item => item.id !== section.id);
|
|
745
|
+
schema.layout = this.normalizedLayout(schema).filter(item => !(item.kind === 'section' && item.id === section.id));
|
|
746
|
+
if (schema.sections.length === 0) {
|
|
747
|
+
const nextSection = {
|
|
748
|
+
id: uniqueId('section'),
|
|
749
|
+
title: 'Section',
|
|
750
|
+
fields: []
|
|
751
|
+
};
|
|
752
|
+
schema.sections.push(nextSection);
|
|
753
|
+
schema.layout = this.normalizedLayout(schema);
|
|
754
|
+
}
|
|
755
|
+
if (section.fields.some(field => field.id === this.selectedFieldId() || containsField(field, this.selectedFieldId()))) {
|
|
756
|
+
this.selectedFieldId.set(null);
|
|
757
|
+
}
|
|
758
|
+
this.schema.set(schema);
|
|
759
|
+
}
|
|
760
|
+
updateSection(section, changes) {
|
|
761
|
+
const schema = cloneSchema(this.schema());
|
|
762
|
+
const nextSection = schema.sections.find(item => item.id === section.id);
|
|
763
|
+
if (!nextSection) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
Object.assign(nextSection, changes);
|
|
767
|
+
this.schema.set(schema);
|
|
768
|
+
}
|
|
769
|
+
sectionDragStarted(section) {
|
|
770
|
+
if (section.collapsed) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.dragCollapsedSectionIds.update(ids => new Set(ids).add(section.id));
|
|
774
|
+
}
|
|
775
|
+
sectionDragEnded(section) {
|
|
776
|
+
if (!this.dragCollapsedSectionIds().has(section.id)) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this.dragCollapsedSectionIds.update(ids => {
|
|
780
|
+
const next = new Set(ids);
|
|
781
|
+
next.delete(section.id);
|
|
782
|
+
return next;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
isSectionCollapsed(section) {
|
|
786
|
+
return section.collapsed === true || this.dragCollapsedSectionIds().has(section.id);
|
|
787
|
+
}
|
|
788
|
+
dropField(event) {
|
|
789
|
+
const schema = cloneSchema(this.schema());
|
|
790
|
+
const targetContainer = this.findContainerLocation(schema, event.container.id);
|
|
791
|
+
if (!targetContainer) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (event.previousContainer.id === 'ngs-form-builder-palette') {
|
|
795
|
+
const definition = event.item.data;
|
|
796
|
+
const field = this.createField(definition, schema);
|
|
797
|
+
targetContainer.fields.splice(event.currentIndex, 0, field);
|
|
798
|
+
this.schema.set(schema);
|
|
799
|
+
this.selectField(field, targetContainer.section);
|
|
800
|
+
this.fieldAdded.emit({ field, section: targetContainer.section });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (event.previousContainer.id === ROOT_DROP_LIST_ID) {
|
|
804
|
+
const layout = this.normalizedLayout(schema);
|
|
805
|
+
const movingItem = this.resolveCanvasDragItem(event.item.data, layout);
|
|
806
|
+
if (!movingItem || movingItem.kind !== 'field') {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const sourceIndex = (schema.fields ?? []).findIndex(field => field.id === movingItem.id);
|
|
810
|
+
const movingField = (schema.fields ?? [])[sourceIndex];
|
|
811
|
+
const layoutIndex = layout.findIndex(item => item.kind === 'field' && item.id === movingItem.id);
|
|
812
|
+
if (!movingField) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (targetContainer.owner &&
|
|
816
|
+
(targetContainer.owner.id === movingField.id || containsField(movingField, targetContainer.owner.id))) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
schema.fields?.splice(sourceIndex, 1);
|
|
820
|
+
if (layoutIndex > -1) {
|
|
821
|
+
layout.splice(layoutIndex, 1);
|
|
822
|
+
}
|
|
823
|
+
targetContainer.fields.splice(event.currentIndex, 0, movingField);
|
|
824
|
+
schema.layout = layout;
|
|
825
|
+
this.schema.set(schema);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const sourceContainer = this.findContainerLocation(schema, event.previousContainer.id);
|
|
829
|
+
if (!sourceContainer) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const movingField = sourceContainer.fields[event.previousIndex];
|
|
833
|
+
if (movingField &&
|
|
834
|
+
targetContainer.owner &&
|
|
835
|
+
(targetContainer.owner.id === movingField.id || containsField(movingField, targetContainer.owner.id))) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (sourceContainer.id === targetContainer.id) {
|
|
839
|
+
moveItemInArray(targetContainer.fields, event.previousIndex, event.currentIndex);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
transferArrayItem(sourceContainer.fields, targetContainer.fields, event.previousIndex, event.currentIndex);
|
|
843
|
+
}
|
|
844
|
+
this.schema.set(schema);
|
|
845
|
+
}
|
|
846
|
+
dropCanvasItem(event) {
|
|
847
|
+
const schema = cloneSchema(this.schema());
|
|
848
|
+
const layout = this.normalizedLayout(schema);
|
|
849
|
+
if (event.previousContainer.id === 'ngs-form-builder-palette') {
|
|
850
|
+
const definition = event.item.data;
|
|
851
|
+
const field = this.createField(definition, schema);
|
|
852
|
+
schema.fields ??= [];
|
|
853
|
+
schema.fields.push(field);
|
|
854
|
+
layout.splice(event.currentIndex, 0, { kind: 'field', id: field.id });
|
|
855
|
+
schema.layout = layout;
|
|
856
|
+
this.schema.set(schema);
|
|
857
|
+
this.selectField(field);
|
|
858
|
+
this.fieldAdded.emit({ field });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (event.previousContainer.id === ROOT_DROP_LIST_ID) {
|
|
862
|
+
const movingItem = this.resolveCanvasDragItem(event.item.data, layout);
|
|
863
|
+
const previousIndex = movingItem
|
|
864
|
+
? layout.findIndex(item => item.kind === movingItem.kind && item.id === movingItem.id)
|
|
865
|
+
: event.previousIndex;
|
|
866
|
+
if (previousIndex < 0) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
moveItemInArray(layout, previousIndex, event.currentIndex);
|
|
870
|
+
schema.layout = layout;
|
|
871
|
+
this.schema.set(schema);
|
|
872
|
+
this.clearSectionDragCollapse();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const sourceContainer = this.findContainerLocation(schema, event.previousContainer.id);
|
|
876
|
+
if (!sourceContainer) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const movingField = sourceContainer.fields[event.previousIndex];
|
|
880
|
+
if (!movingField) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
sourceContainer.fields.splice(event.previousIndex, 1);
|
|
884
|
+
schema.fields ??= [];
|
|
885
|
+
schema.fields.push(movingField);
|
|
886
|
+
layout.splice(event.currentIndex, 0, { kind: 'field', id: movingField.id });
|
|
887
|
+
schema.layout = layout;
|
|
888
|
+
this.schema.set(schema);
|
|
889
|
+
this.clearSectionDragCollapse();
|
|
890
|
+
}
|
|
891
|
+
selectField(field, section = this.selectedSection()) {
|
|
892
|
+
this.selectedFieldId.set(field.id);
|
|
893
|
+
this.fieldSelected.emit({ field, section: section ?? undefined });
|
|
894
|
+
}
|
|
895
|
+
selectFieldTreeNode(node) {
|
|
896
|
+
if (node.field) {
|
|
897
|
+
if (node.section?.collapsed) {
|
|
898
|
+
this.updateSection(node.section, { collapsed: false });
|
|
899
|
+
}
|
|
900
|
+
this.selectField(node.field, node.section);
|
|
901
|
+
this.scrollCanvasFieldIntoView(node.field.id);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
toggleFieldTreeNode(tree, node, event) {
|
|
905
|
+
event.stopPropagation();
|
|
906
|
+
if (!node.children?.length) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (tree.isExpanded(node)) {
|
|
910
|
+
tree.collapse(node);
|
|
911
|
+
this.expandedFieldTreeNodeIds.update(ids => {
|
|
912
|
+
const next = new Set(ids);
|
|
913
|
+
next.delete(node.id);
|
|
914
|
+
return next;
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
tree.expand(node);
|
|
919
|
+
this.expandedFieldTreeNodeIds.update(ids => new Set(ids).add(node.id));
|
|
920
|
+
}
|
|
921
|
+
isFieldTreeNodeExpanded(tree, node) {
|
|
922
|
+
return tree.isExpanded(node) || this.expandedFieldTreeNodeIds().has(node.id);
|
|
923
|
+
}
|
|
924
|
+
fieldTreePlaceholderIcon(source) {
|
|
925
|
+
return source.kind === 'section' ? 'fluent:folder-24-regular' : source.icon;
|
|
926
|
+
}
|
|
927
|
+
fieldTreeNodeDropped(event) {
|
|
928
|
+
if (event.source.kind === 'section') {
|
|
929
|
+
this.dropFieldTreeSection(event);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (event.source.kind !== 'field' ||
|
|
933
|
+
event.source.id === event.target.id ||
|
|
934
|
+
(event.target.kind === 'field' && event.position === 'inside' && !this.isContainerField(event.target.field))) {
|
|
935
|
+
this.resetFieldTreeAfterRejectedDrop();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const schema = cloneSchema(this.schema());
|
|
939
|
+
const sourceLocation = this.findFieldLocation(schema, event.source.id);
|
|
940
|
+
if (!sourceLocation) {
|
|
941
|
+
this.resetFieldTreeAfterRejectedDrop();
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (event.target.kind === 'field' &&
|
|
945
|
+
(sourceLocation.field.id === event.target.id || containsField(sourceLocation.field, event.target.id))) {
|
|
946
|
+
this.resetFieldTreeAfterRejectedDrop();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const movingField = sourceLocation.field;
|
|
950
|
+
this.detachFieldFromLocation(schema, sourceLocation);
|
|
951
|
+
if (event.target.kind === 'section') {
|
|
952
|
+
const inserted = event.position === 'inside'
|
|
953
|
+
? this.insertFieldIntoTreeSection(schema, movingField, event.target.id)
|
|
954
|
+
: this.insertFieldAroundTreeSection(schema, movingField, event.target.id, event.position);
|
|
955
|
+
if (inserted) {
|
|
956
|
+
this.schema.set(schema);
|
|
957
|
+
this.restoreFieldTreeExpansion();
|
|
958
|
+
this.selectField(movingField, event.position === 'inside' ? inserted.section : undefined);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (event.target.kind === 'field') {
|
|
963
|
+
const target = this.resolveTreeFieldInsertTarget(schema, event.target.id, event.position);
|
|
964
|
+
if (target) {
|
|
965
|
+
target.fields.splice(target.index, 0, movingField);
|
|
966
|
+
if (target.fields === (schema.fields ?? [])) {
|
|
967
|
+
this.insertRootFieldIntoLayout(schema, movingField, event.target.id, event.position);
|
|
968
|
+
}
|
|
969
|
+
this.schema.set(schema);
|
|
970
|
+
this.restoreFieldTreeExpansion();
|
|
971
|
+
this.selectField(movingField, target.section);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
this.resetFieldTreeAfterRejectedDrop();
|
|
976
|
+
}
|
|
977
|
+
confirmRemoveField(field) {
|
|
978
|
+
const confirmRef = this.confirmManager.open({
|
|
979
|
+
title: 'Delete field',
|
|
980
|
+
description: `Delete "${field.label}" from this form? This action cannot be undone.`
|
|
981
|
+
});
|
|
982
|
+
confirmRef.confirmed.subscribe(() => {
|
|
983
|
+
this.removeField(field);
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
removeField(field) {
|
|
987
|
+
const schema = cloneSchema(this.schema());
|
|
988
|
+
const location = this.findFieldLocation(schema, field.id);
|
|
989
|
+
if (!location) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
location.siblings.splice(location.index, 1);
|
|
993
|
+
schema.layout = this.normalizedLayout(schema).filter(item => !(item.kind === 'field' && item.id === field.id));
|
|
994
|
+
this.deletePreviewControls(field);
|
|
995
|
+
if (this.selectedFieldId() === field.id || containsField(field, this.selectedFieldId())) {
|
|
996
|
+
this.selectedFieldId.set(null);
|
|
997
|
+
}
|
|
998
|
+
this.schema.set(schema);
|
|
999
|
+
this.fieldRemoved.emit({ field, section: location.section });
|
|
1000
|
+
}
|
|
1001
|
+
sectionDropListId(section) {
|
|
1002
|
+
return `ngs-form-builder-section-${section.id}`;
|
|
1003
|
+
}
|
|
1004
|
+
fieldDropListId(field) {
|
|
1005
|
+
return `ngs-form-builder-field-${field.id}`;
|
|
1006
|
+
}
|
|
1007
|
+
rootDropListId() {
|
|
1008
|
+
return ROOT_DROP_LIST_ID;
|
|
1009
|
+
}
|
|
1010
|
+
previewControl(field) {
|
|
1011
|
+
const existing = this.previewControls.get(field.id);
|
|
1012
|
+
if (existing) {
|
|
1013
|
+
return existing;
|
|
1014
|
+
}
|
|
1015
|
+
const control = new FormControl({
|
|
1016
|
+
value: field.defaultValue ?? null,
|
|
1017
|
+
disabled: true
|
|
1018
|
+
});
|
|
1019
|
+
this.previewControls.set(field.id, control);
|
|
1020
|
+
return control;
|
|
1021
|
+
}
|
|
1022
|
+
patchSelectedField(changes) {
|
|
1023
|
+
const selectedId = this.selectedFieldId();
|
|
1024
|
+
if (!selectedId) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const schema = cloneSchema(this.schema());
|
|
1028
|
+
const location = this.findFieldLocation(schema, selectedId);
|
|
1029
|
+
if (!location) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const nextField = {
|
|
1033
|
+
...location.field,
|
|
1034
|
+
...changes
|
|
1035
|
+
};
|
|
1036
|
+
if (changes.type === 'grid' && !nextField.children) {
|
|
1037
|
+
nextField.children = [];
|
|
1038
|
+
}
|
|
1039
|
+
location.siblings[location.index] = nextField;
|
|
1040
|
+
this.schema.set(schema);
|
|
1041
|
+
}
|
|
1042
|
+
isContainerField(field) {
|
|
1043
|
+
return field.type === 'grid';
|
|
1044
|
+
}
|
|
1045
|
+
fieldIcon(field) {
|
|
1046
|
+
return this.definitions().find(definition => definition.type === field.type)?.icon || 'fluent:form-24-regular';
|
|
1047
|
+
}
|
|
1048
|
+
definitionWidth(definition) {
|
|
1049
|
+
return definition.defaults?.width ?? this.defaultWidth(definition.type);
|
|
1050
|
+
}
|
|
1051
|
+
matchesPaletteQuery(definition, query) {
|
|
1052
|
+
return !query ||
|
|
1053
|
+
definition.label.toLowerCase().includes(query) ||
|
|
1054
|
+
definition.type.toLowerCase().includes(query) ||
|
|
1055
|
+
(definition.description?.toLowerCase().includes(query) ?? false);
|
|
1056
|
+
}
|
|
1057
|
+
nativeDragOver(event, containerId, itemSelector) {
|
|
1058
|
+
const item = this.nativeDragItem();
|
|
1059
|
+
if (!item || (item.kind === 'section' && containerId !== ROOT_DROP_LIST_ID)) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
event.preventDefault();
|
|
1063
|
+
if (event.dataTransfer) {
|
|
1064
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
1065
|
+
}
|
|
1066
|
+
this.nativeDropTarget.set({
|
|
1067
|
+
containerId,
|
|
1068
|
+
index: this.resolveNativeDropIndex(event, itemSelector)
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
nativeDragOverAtIndex(event, containerId, index) {
|
|
1072
|
+
const item = this.nativeDragItem();
|
|
1073
|
+
if (!item || item.kind === 'section') {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
event.preventDefault();
|
|
1077
|
+
if (event.dataTransfer) {
|
|
1078
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
1079
|
+
}
|
|
1080
|
+
this.nativeDropTarget.set({ containerId, index });
|
|
1081
|
+
}
|
|
1082
|
+
insertNativeField(definition, containerId, index) {
|
|
1083
|
+
const schema = cloneSchema(this.schema());
|
|
1084
|
+
const field = this.createField(definition, schema);
|
|
1085
|
+
if (containerId === ROOT_DROP_LIST_ID) {
|
|
1086
|
+
const layout = this.normalizedLayout(schema);
|
|
1087
|
+
schema.fields ??= [];
|
|
1088
|
+
schema.fields.push(field);
|
|
1089
|
+
layout.splice(clampIndex(index, layout.length), 0, { kind: 'field', id: field.id });
|
|
1090
|
+
schema.layout = layout;
|
|
1091
|
+
this.schema.set(schema);
|
|
1092
|
+
this.selectField(field);
|
|
1093
|
+
this.fieldAdded.emit({ field });
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const targetContainer = this.findContainerLocation(schema, containerId);
|
|
1097
|
+
if (!targetContainer) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
targetContainer.fields.splice(clampIndex(index, targetContainer.fields.length), 0, field);
|
|
1101
|
+
this.schema.set(schema);
|
|
1102
|
+
this.selectField(field, targetContainer.section);
|
|
1103
|
+
this.fieldAdded.emit({ field, section: targetContainer.section });
|
|
1104
|
+
}
|
|
1105
|
+
insertNativeSection(index) {
|
|
1106
|
+
const schema = cloneSchema(this.schema());
|
|
1107
|
+
const section = {
|
|
1108
|
+
id: uniqueId('section'),
|
|
1109
|
+
title: `Section ${schema.sections.length + 1}`,
|
|
1110
|
+
fields: []
|
|
1111
|
+
};
|
|
1112
|
+
const layout = this.normalizedLayout(schema);
|
|
1113
|
+
schema.sections.push(section);
|
|
1114
|
+
layout.splice(clampIndex(index, layout.length), 0, { kind: 'section', id: section.id });
|
|
1115
|
+
schema.layout = layout;
|
|
1116
|
+
this.schema.set(schema);
|
|
1117
|
+
}
|
|
1118
|
+
resolveNativeDragItem(event) {
|
|
1119
|
+
const currentItem = this.nativeDragItem();
|
|
1120
|
+
if (currentItem) {
|
|
1121
|
+
return currentItem;
|
|
1122
|
+
}
|
|
1123
|
+
if (event.dataTransfer?.getData(PALETTE_DRAG_ITEM) === 'section') {
|
|
1124
|
+
return { kind: 'section' };
|
|
1125
|
+
}
|
|
1126
|
+
const type = event.dataTransfer?.getData(PALETTE_DRAG_TYPE);
|
|
1127
|
+
const definition = type ? this.definitions().find(item => item.type === type) ?? null : null;
|
|
1128
|
+
return definition ? { kind: 'field', definition } : null;
|
|
1129
|
+
}
|
|
1130
|
+
resolveNativeDropIndex(event, itemSelector) {
|
|
1131
|
+
const container = event.currentTarget;
|
|
1132
|
+
if (!(container instanceof HTMLElement)) {
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
1135
|
+
const items = Array.from(container.children).filter((child) => {
|
|
1136
|
+
return child instanceof HTMLElement && child.matches(itemSelector);
|
|
1137
|
+
});
|
|
1138
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1139
|
+
const rect = items[index].getBoundingClientRect();
|
|
1140
|
+
const centerY = rect.top + rect.height / 2;
|
|
1141
|
+
const centerX = rect.left + rect.width / 2;
|
|
1142
|
+
const sameRow = event.clientY >= rect.top && event.clientY <= rect.bottom;
|
|
1143
|
+
if (event.clientY < centerY || (sameRow && event.clientX < centerX)) {
|
|
1144
|
+
return index;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return items.length;
|
|
1148
|
+
}
|
|
1149
|
+
setPaletteDragImage(event) {
|
|
1150
|
+
const source = event.currentTarget;
|
|
1151
|
+
if (!(source instanceof HTMLElement) || !event.dataTransfer) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const clone = source.cloneNode(true);
|
|
1155
|
+
if (!(clone instanceof HTMLElement)) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
clone.classList.add('ngs-form-builder-native-drag-image');
|
|
1159
|
+
source.parentElement?.appendChild(clone);
|
|
1160
|
+
const rect = source.getBoundingClientRect();
|
|
1161
|
+
event.dataTransfer.setDragImage(clone, Math.min(event.offsetX, rect.width), Math.min(event.offsetY, rect.height));
|
|
1162
|
+
window.setTimeout(() => clone.remove());
|
|
1163
|
+
}
|
|
1164
|
+
createField(definition, schema) {
|
|
1165
|
+
const baseLabel = definition.defaults?.label || definition.label;
|
|
1166
|
+
const name = uniqueFieldName(toFieldName(baseLabel), schema);
|
|
1167
|
+
return {
|
|
1168
|
+
id: uniqueId('field'),
|
|
1169
|
+
name,
|
|
1170
|
+
type: definition.type,
|
|
1171
|
+
label: baseLabel,
|
|
1172
|
+
width: definition.defaults?.width ?? this.defaultWidth(definition.type),
|
|
1173
|
+
visibility: {
|
|
1174
|
+
form: true,
|
|
1175
|
+
email: true,
|
|
1176
|
+
pdf: true
|
|
1177
|
+
},
|
|
1178
|
+
...definition.defaults
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
defaultWidth(type) {
|
|
1182
|
+
return 12;
|
|
1183
|
+
}
|
|
1184
|
+
resolveCanvasItems(schema) {
|
|
1185
|
+
const fieldsById = new Map((schema.fields ?? []).map(field => [field.id, field]));
|
|
1186
|
+
const sectionsById = new Map(schema.sections.map(section => [section.id, section]));
|
|
1187
|
+
const items = [];
|
|
1188
|
+
for (const item of this.normalizedLayout(schema)) {
|
|
1189
|
+
if (item.kind === 'field') {
|
|
1190
|
+
const field = fieldsById.get(item.id);
|
|
1191
|
+
if (field) {
|
|
1192
|
+
items.push({ ...item, field });
|
|
1193
|
+
}
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const section = sectionsById.get(item.id);
|
|
1197
|
+
if (section) {
|
|
1198
|
+
items.push({ ...item, section });
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return items;
|
|
1202
|
+
}
|
|
1203
|
+
normalizedLayout(schema) {
|
|
1204
|
+
const used = new Set();
|
|
1205
|
+
const layout = [];
|
|
1206
|
+
const fieldsById = new Map((schema.fields ?? []).map(field => [field.id, field]));
|
|
1207
|
+
const sectionsById = new Map(schema.sections.map(section => [section.id, section]));
|
|
1208
|
+
for (const item of schema.layout ?? []) {
|
|
1209
|
+
const key = `${item.kind}:${item.id}`;
|
|
1210
|
+
if (used.has(key)) {
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if ((item.kind === 'field' && fieldsById.has(item.id)) || (item.kind === 'section' && sectionsById.has(item.id))) {
|
|
1214
|
+
used.add(key);
|
|
1215
|
+
layout.push(item);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
for (const field of schema.fields ?? []) {
|
|
1219
|
+
const key = `field:${field.id}`;
|
|
1220
|
+
if (!used.has(key)) {
|
|
1221
|
+
used.add(key);
|
|
1222
|
+
layout.push({ kind: 'field', id: field.id });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
for (const section of schema.sections) {
|
|
1226
|
+
const key = `section:${section.id}`;
|
|
1227
|
+
if (!used.has(key)) {
|
|
1228
|
+
used.add(key);
|
|
1229
|
+
layout.push({ kind: 'section', id: section.id });
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return layout;
|
|
1233
|
+
}
|
|
1234
|
+
collectFieldContainerDropListIds(schema) {
|
|
1235
|
+
return [
|
|
1236
|
+
...(schema.fields ?? []).flatMap(field => this.collectFieldDropListIds(field)),
|
|
1237
|
+
...schema.sections.flatMap(section => [
|
|
1238
|
+
this.sectionDropListId(section),
|
|
1239
|
+
...section.fields.flatMap(field => this.collectFieldDropListIds(field))
|
|
1240
|
+
])
|
|
1241
|
+
];
|
|
1242
|
+
}
|
|
1243
|
+
collectFieldDropListIds(field) {
|
|
1244
|
+
return [
|
|
1245
|
+
...(this.isContainerField(field) ? [this.fieldDropListId(field)] : []),
|
|
1246
|
+
...(field.children ?? []).flatMap(child => this.collectFieldDropListIds(child))
|
|
1247
|
+
];
|
|
1248
|
+
}
|
|
1249
|
+
resolveCanvasDragItem(data, layout) {
|
|
1250
|
+
if (isFormBuilderLayoutItem(data)) {
|
|
1251
|
+
return data;
|
|
1252
|
+
}
|
|
1253
|
+
if (isFormBuilderField(data)) {
|
|
1254
|
+
return layout.find(item => item.kind === 'field' && item.id === data.id) ?? null;
|
|
1255
|
+
}
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
detachFieldFromLocation(schema, location) {
|
|
1259
|
+
location.siblings.splice(location.index, 1);
|
|
1260
|
+
if (location.siblings === (schema.fields ?? [])) {
|
|
1261
|
+
schema.layout = this.normalizedLayout(schema).filter(item => !(item.kind === 'field' && item.id === location.field.id));
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
insertFieldIntoTreeSection(schema, field, sectionId) {
|
|
1265
|
+
const section = schema.sections.find(item => item.id === sectionId);
|
|
1266
|
+
if (!section) {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
section.fields.push(field);
|
|
1270
|
+
return {
|
|
1271
|
+
fields: section.fields,
|
|
1272
|
+
index: section.fields.length - 1,
|
|
1273
|
+
section
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
insertFieldAroundTreeSection(schema, field, sectionId, position) {
|
|
1277
|
+
if (position === 'inside') {
|
|
1278
|
+
return this.insertFieldIntoTreeSection(schema, field, sectionId);
|
|
1279
|
+
}
|
|
1280
|
+
const section = schema.sections.find(item => item.id === sectionId);
|
|
1281
|
+
if (!section) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
schema.fields ??= [];
|
|
1285
|
+
if (!schema.fields.some(item => item.id === field.id)) {
|
|
1286
|
+
schema.fields.push(field);
|
|
1287
|
+
}
|
|
1288
|
+
const layout = this.normalizedLayout(schema).filter(item => !(item.kind === 'field' && item.id === field.id));
|
|
1289
|
+
const sectionIndex = layout.findIndex(item => item.kind === 'section' && item.id === sectionId);
|
|
1290
|
+
layout.splice(sectionIndex === -1 ? layout.length : sectionIndex + (position === 'after' ? 1 : 0), 0, {
|
|
1291
|
+
kind: 'field',
|
|
1292
|
+
id: field.id
|
|
1293
|
+
});
|
|
1294
|
+
schema.layout = layout;
|
|
1295
|
+
return {
|
|
1296
|
+
fields: schema.fields,
|
|
1297
|
+
index: schema.fields.findIndex(item => item.id === field.id)
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
resolveTreeFieldInsertTarget(schema, targetFieldId, position) {
|
|
1301
|
+
const targetLocation = this.findFieldLocation(schema, targetFieldId);
|
|
1302
|
+
if (!targetLocation) {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
if (position === 'inside' && this.isContainerField(targetLocation.field)) {
|
|
1306
|
+
targetLocation.field.children ??= [];
|
|
1307
|
+
return {
|
|
1308
|
+
fields: targetLocation.field.children,
|
|
1309
|
+
index: targetLocation.field.children.length,
|
|
1310
|
+
section: targetLocation.section
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const insertIndex = position === 'before' ? targetLocation.index : targetLocation.index + 1;
|
|
1314
|
+
return {
|
|
1315
|
+
fields: targetLocation.siblings,
|
|
1316
|
+
index: insertIndex,
|
|
1317
|
+
section: targetLocation.section
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
insertRootFieldIntoLayout(schema, field, targetFieldId, position) {
|
|
1321
|
+
schema.fields ??= [];
|
|
1322
|
+
if (!schema.fields.some(item => item.id === field.id)) {
|
|
1323
|
+
schema.fields.push(field);
|
|
1324
|
+
}
|
|
1325
|
+
const layout = this.normalizedLayout(schema).filter(item => !(item.kind === 'field' && item.id === field.id));
|
|
1326
|
+
const targetIndex = layout.findIndex(item => item.kind === 'field' && item.id === targetFieldId);
|
|
1327
|
+
layout.splice(targetIndex === -1 ? layout.length : targetIndex + (position === 'before' ? 0 : 1), 0, {
|
|
1328
|
+
kind: 'field',
|
|
1329
|
+
id: field.id
|
|
1330
|
+
});
|
|
1331
|
+
schema.layout = layout;
|
|
1332
|
+
}
|
|
1333
|
+
clearSectionDragCollapse() {
|
|
1334
|
+
if (this.dragCollapsedSectionIds().size) {
|
|
1335
|
+
this.dragCollapsedSectionIds.set(new Set());
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
resetFieldTreeAfterRejectedDrop() {
|
|
1339
|
+
this.schema.set(cloneSchema(this.schema()));
|
|
1340
|
+
this.restoreFieldTreeExpansion();
|
|
1341
|
+
}
|
|
1342
|
+
canDropFieldTreeNode(source, target, position) {
|
|
1343
|
+
if (source.id === target.id) {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
if (source.kind === 'section') {
|
|
1347
|
+
return position !== 'inside' && (target.kind === 'section' || this.isRootFieldTreeNode(target));
|
|
1348
|
+
}
|
|
1349
|
+
if (target.kind === 'field' && position === 'inside') {
|
|
1350
|
+
return !!target.field && this.isContainerField(target.field);
|
|
1351
|
+
}
|
|
1352
|
+
return true;
|
|
1353
|
+
}
|
|
1354
|
+
dropFieldTreeSection(event) {
|
|
1355
|
+
if (!this.canDropFieldTreeNode(event.source, event.target, event.position)) {
|
|
1356
|
+
this.resetFieldTreeAfterRejectedDrop();
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const schema = cloneSchema(this.schema());
|
|
1360
|
+
const layout = this.normalizedLayout(schema);
|
|
1361
|
+
const sourceIndex = layout.findIndex(item => item.kind === 'section' && item.id === event.source.id);
|
|
1362
|
+
const targetKind = event.target.kind === 'section' ? 'section' : 'field';
|
|
1363
|
+
const targetIndex = layout.findIndex(item => item.kind === targetKind && item.id === event.target.id);
|
|
1364
|
+
if (sourceIndex < 0 || targetIndex < 0) {
|
|
1365
|
+
this.resetFieldTreeAfterRejectedDrop();
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const [sectionItem] = layout.splice(sourceIndex, 1);
|
|
1369
|
+
const adjustedTargetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex;
|
|
1370
|
+
layout.splice(adjustedTargetIndex + (event.position === 'after' ? 1 : 0), 0, sectionItem);
|
|
1371
|
+
schema.layout = layout;
|
|
1372
|
+
this.schema.set(schema);
|
|
1373
|
+
this.restoreFieldTreeExpansion();
|
|
1374
|
+
}
|
|
1375
|
+
isRootFieldTreeNode(node) {
|
|
1376
|
+
if (node.kind !== 'field') {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return this.resolveCanvasItems(this.schema()).some(item => item.kind === 'field' && item.id === node.id);
|
|
1380
|
+
}
|
|
1381
|
+
restoreFieldTreeExpansion() {
|
|
1382
|
+
const expandedIds = this.expandedFieldTreeNodeIds();
|
|
1383
|
+
if (!expandedIds.size) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
this.afterNextPaint(() => {
|
|
1387
|
+
this.afterNextPaint(() => {
|
|
1388
|
+
const tree = this.actualFieldsTree();
|
|
1389
|
+
if (!tree) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
for (const node of this.flattenFieldTree(this.fieldTree())) {
|
|
1393
|
+
if (node.children?.length && expandedIds.has(node.id)) {
|
|
1394
|
+
tree.expand(node);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
flattenFieldTree(nodes) {
|
|
1401
|
+
return nodes.flatMap(node => [node, ...this.flattenFieldTree(node.children ?? [])]);
|
|
1402
|
+
}
|
|
1403
|
+
scrollCanvasFieldIntoView(fieldId) {
|
|
1404
|
+
this.afterNextPaint(() => {
|
|
1405
|
+
this.afterNextPaint(() => {
|
|
1406
|
+
const target = this.findCanvasFieldElement(fieldId);
|
|
1407
|
+
if (!target) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const scrollableContent = target
|
|
1411
|
+
.closest('ngs-scrollbar-area')
|
|
1412
|
+
?.querySelector('.scrollable-content');
|
|
1413
|
+
if (!scrollableContent) {
|
|
1414
|
+
target.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
this.scrollElementFullyIntoView(target, scrollableContent);
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
findCanvasFieldElement(fieldId) {
|
|
1422
|
+
const fields = this.elementRef.nativeElement.querySelectorAll('.ngs-form-builder-canvas [data-form-builder-field-id]');
|
|
1423
|
+
return Array.from(fields).find(field => field.dataset['formBuilderFieldId'] === fieldId) ?? null;
|
|
1424
|
+
}
|
|
1425
|
+
scrollElementFullyIntoView(target, scrollableContent) {
|
|
1426
|
+
const padding = 16;
|
|
1427
|
+
const containerRect = scrollableContent.getBoundingClientRect();
|
|
1428
|
+
const targetRect = target.getBoundingClientRect();
|
|
1429
|
+
const availableHeight = Math.max(0, containerRect.height - padding * 2);
|
|
1430
|
+
const availableWidth = Math.max(0, containerRect.width - padding * 2);
|
|
1431
|
+
let top = scrollableContent.scrollTop;
|
|
1432
|
+
let left = scrollableContent.scrollLeft;
|
|
1433
|
+
if (targetRect.height > availableHeight || targetRect.top < containerRect.top + padding) {
|
|
1434
|
+
top += targetRect.top - containerRect.top - padding;
|
|
1435
|
+
}
|
|
1436
|
+
else if (targetRect.bottom > containerRect.bottom - padding) {
|
|
1437
|
+
top += targetRect.bottom - containerRect.bottom + padding;
|
|
1438
|
+
}
|
|
1439
|
+
if (targetRect.width > availableWidth || targetRect.left < containerRect.left + padding) {
|
|
1440
|
+
left += targetRect.left - containerRect.left - padding;
|
|
1441
|
+
}
|
|
1442
|
+
else if (targetRect.right > containerRect.right - padding) {
|
|
1443
|
+
left += targetRect.right - containerRect.right + padding;
|
|
1444
|
+
}
|
|
1445
|
+
scrollableContent.scrollTo({
|
|
1446
|
+
top: Math.max(0, top),
|
|
1447
|
+
left: Math.max(0, left),
|
|
1448
|
+
behavior: 'smooth'
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
afterNextPaint(callback) {
|
|
1452
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
1453
|
+
requestAnimationFrame(callback);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
window.setTimeout(callback);
|
|
1457
|
+
}
|
|
1458
|
+
createFieldTreeNode(field, section) {
|
|
1459
|
+
const definition = this.definitions().find(item => item.type === field.type);
|
|
1460
|
+
return {
|
|
1461
|
+
id: field.id,
|
|
1462
|
+
label: field.label,
|
|
1463
|
+
name: field.name,
|
|
1464
|
+
type: field.type,
|
|
1465
|
+
icon: definition?.icon || 'fluent:form-24-regular',
|
|
1466
|
+
kind: 'field',
|
|
1467
|
+
field,
|
|
1468
|
+
section,
|
|
1469
|
+
children: field.children?.map(child => this.createFieldTreeNode(child, section))
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
findContainerLocation(schema, containerId) {
|
|
1473
|
+
if (containerId === ROOT_DROP_LIST_ID) {
|
|
1474
|
+
return {
|
|
1475
|
+
id: containerId,
|
|
1476
|
+
fields: schema.fields ?? []
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const rootNested = this.findFieldContainerLocation(schema.fields ?? [], undefined, containerId);
|
|
1480
|
+
if (rootNested) {
|
|
1481
|
+
return rootNested;
|
|
1482
|
+
}
|
|
1483
|
+
for (const section of schema.sections) {
|
|
1484
|
+
if (this.sectionDropListId(section) === containerId) {
|
|
1485
|
+
return {
|
|
1486
|
+
id: containerId,
|
|
1487
|
+
fields: section.fields,
|
|
1488
|
+
section
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
const nested = this.findFieldContainerLocation(section.fields, section, containerId);
|
|
1492
|
+
if (nested) {
|
|
1493
|
+
return nested;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
findFieldContainerLocation(fields, section, containerId) {
|
|
1499
|
+
for (const field of fields) {
|
|
1500
|
+
if (this.isContainerField(field) && this.fieldDropListId(field) === containerId) {
|
|
1501
|
+
field.children ??= [];
|
|
1502
|
+
return {
|
|
1503
|
+
id: containerId,
|
|
1504
|
+
fields: field.children,
|
|
1505
|
+
section,
|
|
1506
|
+
owner: field
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
const nested = this.findFieldContainerLocation(field.children ?? [], section, containerId);
|
|
1510
|
+
if (nested) {
|
|
1511
|
+
return nested;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
findFieldLocation(schema, fieldId) {
|
|
1517
|
+
const rootLocation = findFieldLocationInFields(schema.fields ?? [], fieldId);
|
|
1518
|
+
if (rootLocation) {
|
|
1519
|
+
return rootLocation;
|
|
1520
|
+
}
|
|
1521
|
+
for (const section of schema.sections) {
|
|
1522
|
+
const location = findFieldLocationInFields(section.fields, fieldId, section);
|
|
1523
|
+
if (location) {
|
|
1524
|
+
return location;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
deletePreviewControls(field) {
|
|
1530
|
+
this.previewControls.delete(field.id);
|
|
1531
|
+
for (const child of field.children ?? []) {
|
|
1532
|
+
this.deletePreviewControls(child);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1536
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: FormBuilder, isStandalone: true, selector: "ngs-form-builder", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, paletteTitle: { classPropertyName: "paletteTitle", publicName: "paletteTitle", isSignal: true, isRequired: false, transformFunction: null }, inspectorTitle: { classPropertyName: "inspectorTitle", publicName: "inspectorTitle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { schema: "schemaChange", fieldSelected: "fieldSelected", fieldAdded: "fieldAdded", fieldRemoved: "fieldRemoved" }, host: { classAttribute: "ngs-form-builder" }, viewQueries: [{ propertyName: "actualFieldsTree", first: true, predicate: ["actualFieldsTree"], descendants: true, isSignal: true }], exportAs: ["ngsFormBuilder"], ngImport: i0, template: "<ng-template #fieldCard let-field let-section=\"section\" let-dragData=\"dragData\">\n <div\n class=\"ngs-form-builder-field\"\n cdkDrag\n [cdkDragData]=\"dragData || field\"\n [class.is-width-1]=\"(field.width ?? 12) === 1\"\n [class.is-width-2]=\"field.width === 2\"\n [class.is-width-3]=\"field.width === 3\"\n [class.is-width-4]=\"field.width === 4\"\n [class.is-width-5]=\"field.width === 5\"\n [class.is-width-6]=\"field.width === 6\"\n [class.is-width-7]=\"field.width === 7\"\n [class.is-width-8]=\"field.width === 8\"\n [class.is-width-9]=\"field.width === 9\"\n [class.is-width-10]=\"field.width === 10\"\n [class.is-width-11]=\"field.width === 11\"\n [class.is-width-12]=\"(field.width ?? 12) === 12\"\n [class.is-container]=\"isContainerField(field)\"\n [class.is-selected]=\"selectedFieldId() === field.id\"\n [attr.data-form-builder-field-id]=\"field.id\"\n (dragover)=\"nativeContainerFieldDragOver($event, field)\"\n (dragleave)=\"nativeContainerFieldDragLeave($event, field)\"\n (drop)=\"nativeContainerFieldDrop($event, field)\"\n (click)=\"$event.stopPropagation(); selectField(field, section)\">\n <div class=\"ngs-form-builder-field-header\">\n <div class=\"ngs-form-builder-field-label\">\n <ngs-icon [name]=\"fieldIcon(field)\"/>\n <span>{{ field.label }}</span>\n </div>\n\n <button ngsIconButton type=\"button\" class=\"ngs-form-builder-field-handle\" cdkDragHandle aria-label=\"Move field\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </button>\n </div>\n\n @if (isContainerField(field)) {\n <div class=\"ngs-form-builder-grid-field\">\n <ng-container\n [ngTemplateOutlet]=\"fieldList\"\n [ngTemplateOutletContext]=\"{\n $implicit: field.children ?? [],\n containerId: fieldDropListId(field),\n section\n }\"/>\n </div>\n } @else {\n <ngs-form-builder-field-host\n [field]=\"field\"\n [control]=\"previewControl(field)\"\n [definitions]=\"definitions()\"\n [readonly]=\"true\"\n [editableCanvas]=\"true\"/>\n }\n\n <ng-template cdkDragPlaceholder>\n <div\n class=\"ngs-form-builder-field ngs-form-builder-field-line-placeholder\"\n [class.is-width-1]=\"(field.width ?? 12) === 1\"\n [class.is-width-2]=\"field.width === 2\"\n [class.is-width-3]=\"field.width === 3\"\n [class.is-width-4]=\"field.width === 4\"\n [class.is-width-5]=\"field.width === 5\"\n [class.is-width-6]=\"field.width === 6\"\n [class.is-width-7]=\"field.width === 7\"\n [class.is-width-8]=\"field.width === 8\"\n [class.is-width-9]=\"field.width === 9\"\n [class.is-width-10]=\"field.width === 10\"\n [class.is-width-11]=\"field.width === 11\"\n [class.is-width-12]=\"(field.width ?? 12) === 12\">\n </div>\n </ng-template>\n\n <ng-template cdkDragPreview>\n <div\n class=\"ngs-form-builder-field ngs-form-builder-field-drag-preview\"\n [class.is-width-1]=\"(field.width ?? 12) === 1\"\n [class.is-width-2]=\"field.width === 2\"\n [class.is-width-3]=\"field.width === 3\"\n [class.is-width-4]=\"field.width === 4\"\n [class.is-width-5]=\"field.width === 5\"\n [class.is-width-6]=\"field.width === 6\"\n [class.is-width-7]=\"field.width === 7\"\n [class.is-width-8]=\"field.width === 8\"\n [class.is-width-9]=\"field.width === 9\"\n [class.is-width-10]=\"field.width === 10\"\n [class.is-width-11]=\"field.width === 11\"\n [class.is-width-12]=\"(field.width ?? 12) === 12\">\n <div class=\"ngs-form-builder-field-header\">\n <div class=\"ngs-form-builder-field-label\">\n <ngs-icon [name]=\"fieldIcon(field)\"/>\n <span>{{ field.label }}</span>\n </div>\n <span class=\"ngs-form-builder-drag-icon\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </span>\n </div>\n <div class=\"ngs-form-builder-ghost-control\"></div>\n </div>\n </ng-template>\n </div>\n</ng-template>\n\n<ng-template #nativeFieldGhost>\n @if (nativeDragFieldDefinition(); as definition) {\n <div\n class=\"ngs-form-builder-field ngs-form-builder-field-placeholder ngs-form-builder-native-ghost-field\"\n [class.is-width-1]=\"definitionWidth(definition) === 1\"\n [class.is-width-2]=\"definitionWidth(definition) === 2\"\n [class.is-width-3]=\"definitionWidth(definition) === 3\"\n [class.is-width-4]=\"definitionWidth(definition) === 4\"\n [class.is-width-5]=\"definitionWidth(definition) === 5\"\n [class.is-width-6]=\"definitionWidth(definition) === 6\"\n [class.is-width-7]=\"definitionWidth(definition) === 7\"\n [class.is-width-8]=\"definitionWidth(definition) === 8\"\n [class.is-width-9]=\"definitionWidth(definition) === 9\"\n [class.is-width-10]=\"definitionWidth(definition) === 10\"\n [class.is-width-11]=\"definitionWidth(definition) === 11\"\n [class.is-width-12]=\"definitionWidth(definition) === 12\">\n <div class=\"ngs-form-builder-field-header\">\n <div class=\"ngs-form-builder-field-label\">\n <ngs-icon [name]=\"definition.icon || 'fluent:form-24-regular'\"/>\n <span>{{ definition.defaults?.label || definition.label }}</span>\n </div>\n <span class=\"ngs-form-builder-drag-icon\">\n <ngs-icon name=\"fluent:arrow-move-24-regular\"/>\n </span>\n </div>\n <div class=\"ngs-form-builder-ghost-control\"></div>\n </div>\n }\n</ng-template>\n\n<ng-template #nativeSectionGhost>\n <ngs-card class=\"ngs-form-builder-section ngs-form-builder-native-ghost-section\">\n <ngs-card-header>\n <div class=\"ngs-form-builder-section-heading\">\n <span class=\"ngs-form-builder-drag-icon\">\n <ngs-icon name=\"fluent:folder-add-24-regular\"/>\n </span>\n <span class=\"ngs-form-builder-section-title\">Section</span>\n </div>\n </ngs-card-header>\n </ngs-card>\n</ng-template>\n\n<ng-template #fieldList let-fields let-containerId=\"containerId\" let-section=\"section\">\n <div\n cdkDropList\n class=\"ngs-form-builder-drop-list ngs-form-builder-canvas-drop-list\"\n [id]=\"containerId\"\n [cdkDropListData]=\"fields\"\n cdkDropListOrientation=\"mixed\"\n [cdkDropListConnectedTo]=\"fieldContainerDropListIds()\"\n [cdkDropListEnterPredicate]=\"fieldDropListEnterPredicate\"\n (dragover)=\"nativeFieldDragOver($event, containerId)\"\n (dragleave)=\"nativeDragLeave($event, containerId)\"\n (drop)=\"nativeFieldDrop($event, containerId)\"\n (cdkDropListDropped)=\"dropField($event)\">\n @for (field of fields; track field.id; let fieldIndex = $index) {\n @if (isNativeDropTarget(containerId, fieldIndex)) {\n <ng-container [ngTemplateOutlet]=\"nativeFieldGhost\"/>\n }\n <ng-container\n [ngTemplateOutlet]=\"fieldCard\"\n [ngTemplateOutletContext]=\"{\n $implicit: field,\n section\n }\"/>\n }\n\n @if (isNativeDropTarget(containerId, fields.length)) {\n <ng-container [ngTemplateOutlet]=\"nativeFieldGhost\"/>\n }\n </div>\n</ng-template>\n\n<ngs-panel class=\"ngs-form-builder-shell\" cdkDropListGroup>\n <ngs-panel-header autoHeight class=\"flex items-center justify-between gap-4 border-b border-b-subtle px-5 py-3\">\n <div class=\"min-w-0\">\n <p class=\"text-sm text-secondary\">Form builder</p>\n <h2 class=\"truncate text-lg font-semibold\">{{ schema().title || 'Untitled form' }}</h2>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <button ngsButton=\"outlined\" type=\"button\" (click)=\"openPreview(previewDialog)\">\n <ngs-icon name=\"fluent:eye-24-regular\"/>\n Preview\n </button>\n\n </div>\n </ngs-panel-header>\n\n <ngs-panel-sidebar class=\"ngs-form-builder-palette w-80 border-e border-e-subtle\">\n <ngs-scrollbar-area [absolute]=\"true\">\n <div class=\"flex flex-col gap-4 p-4\">\n <h3 class=\"text-base font-semibold\">{{ paletteTitle() }}</h3>\n\n <ngs-tab-group animationDuration=\"0ms\" ngs-stretch-tabs=\"false\">\n <ngs-tab label=\"Placeholders\">\n <div class=\"flex flex-col gap-4 pt-4\">\n <ngs-form-field>\n <ngs-label>Search placeholder</ngs-label>\n <input ngsInput\n [ngModel]=\"search()\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"search.set($event)\">\n </ngs-form-field>\n\n <div class=\"flex flex-col gap-5\">\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-xs font-semibold uppercase tracking-normal text-secondary\">Layout</h4>\n <button\n type=\"button\"\n class=\"ngs-form-builder-palette-item\"\n draggable=\"true\"\n (dragstart)=\"sectionPaletteDragStarted($event)\"\n (dragend)=\"paletteDragEnded()\">\n <span class=\"ngs-form-builder-palette-item-content\">\n <span class=\"ngs-form-builder-palette-item-label\">\n <ngs-icon name=\"fluent:folder-add-24-regular\"/>\n <span>Section</span>\n </span>\n </span>\n </button>\n\n @for (definition of layoutDefinitions(); track definition.type) {\n <button\n type=\"button\"\n class=\"ngs-form-builder-palette-item\"\n draggable=\"true\"\n (dragstart)=\"paletteDragStarted($event, definition)\"\n (dragend)=\"paletteDragEnded()\"\n (click)=\"paletteClicked(definition)\">\n <span class=\"ngs-form-builder-palette-item-content\">\n <span class=\"ngs-form-builder-palette-item-label\">\n <ngs-icon [name]=\"definition.icon || 'fluent:form-24-regular'\"/>\n <span>{{ definition.label }}</span>\n </span>\n </span>\n </button>\n }\n </section>\n\n @for (group of paletteGroups(); track group.name) {\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-xs font-semibold uppercase tracking-normal text-secondary\">{{ group.name }}</h4>\n <div class=\"flex flex-col gap-2\">\n @for (definition of group.fields; track definition.type) {\n <button\n type=\"button\"\n class=\"ngs-form-builder-palette-item\"\n draggable=\"true\"\n (dragstart)=\"paletteDragStarted($event, definition)\"\n (dragend)=\"paletteDragEnded()\"\n (click)=\"paletteClicked(definition)\">\n <span class=\"ngs-form-builder-palette-item-content\">\n <span class=\"ngs-form-builder-palette-item-label\">\n <ngs-icon [name]=\"definition.icon || 'fluent:form-24-regular'\"/>\n <span>{{ definition.label }}</span>\n </span>\n </span>\n </button>\n }\n </div>\n </section>\n }\n </div>\n </div>\n </ngs-tab>\n\n <ngs-tab label=\"Actual fields\">\n <div class=\"pt-4\">\n <ngs-tree\n #actualFieldsTree=\"ngsTree\"\n [dataSource]=\"fieldTree()\"\n [childrenAccessor]=\"fieldTreeChildrenAccessor\"\n [trackBy]=\"trackFieldTreeNode\"\n [draggablePredicate]=\"fieldTreeDraggablePredicate\"\n [dropPredicate]=\"fieldTreeDropPredicate\"\n [reorderOnDrop]=\"false\"\n (nodeDrop)=\"fieldTreeNodeDropped($event)\"\n draggable>\n <ng-template ngsTreeDragPlaceholder let-source=\"source\">\n <div class=\"ngs-form-builder-tree-drag-placeholder\">\n <ngs-icon [name]=\"fieldTreePlaceholderIcon(source)\"/>\n <span>{{ source.label }}</span>\n </div>\n </ng-template>\n\n <ngs-tree-node *ngsTreeNodeDef=\"let node\" ngsTreeNodePadding [value]=\"node.id\">\n <button\n ngsButton=\"text\"\n type=\"button\"\n class=\"ngs-form-builder-tree-item\"\n [class.is-selected]=\"selectedFieldId() === node.id\"\n (click)=\"selectFieldTreeNode(node)\">\n <ngs-icon [name]=\"node.icon\"/>\n <span>{{ node.label }}</span>\n @if (node.name) {\n <small>{{ node.name }}</small>\n }\n </button>\n </ngs-tree-node>\n\n <ngs-tree-node *ngsTreeNodeDef=\"let node; when: hasFieldTreeChildren\"\n ngsTreeNodePadding\n [value]=\"node.id\"\n [cdkTreeNodeTypeaheadLabel]=\"node.label\">\n <button\n ngsIconButton\n class=\"ngs-form-builder-tree-toggle\"\n [attr.aria-label]=\"'Toggle ' + node.label\"\n (click)=\"toggleFieldTreeNode(actualFieldsTree, node, $event)\">\n <ngs-icon [name]=\"isFieldTreeNodeExpanded(actualFieldsTree, node) ? 'fluent:chevron-down-24-regular' : 'fluent:chevron-right-24-regular'\" class=\"size-4\"/>\n </button>\n <button\n ngsButton=\"text\"\n type=\"button\"\n class=\"ngs-form-builder-tree-item\"\n [class.is-selected]=\"selectedFieldId() === node.id\"\n (click)=\"selectFieldTreeNode(node)\">\n <ngs-icon [name]=\"node.icon\"/>\n <span>{{ node.label }}</span>\n @if (node.name) {\n <small>{{ node.name }}</small>\n }\n </button>\n </ngs-tree-node>\n </ngs-tree>\n </div>\n </ngs-tab>\n </ngs-tab-group>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-sidebar>\n\n <ngs-panel-content>\n <ngs-scrollbar-area [absolute]=\"true\">\n <div class=\"ngs-form-builder-canvas flex flex-col gap-4 p-5\">\n <div\n cdkDropList\n class=\"ngs-form-builder-canvas-list\"\n [id]=\"rootDropListId()\"\n [cdkDropListData]=\"canvasItems()\"\n [cdkDropListConnectedTo]=\"fieldContainerDropListIds()\"\n (dragover)=\"nativeCanvasDragOver($event)\"\n (dragleave)=\"nativeDragLeave($event, rootDropListId())\"\n (drop)=\"nativeFieldDrop($event, rootDropListId())\"\n (cdkDropListDropped)=\"dropCanvasItem($event)\">\n @for (item of canvasItems(); track item.kind + ':' + item.id; let itemIndex = $index) {\n @if (isNativeDropTarget(rootDropListId(), itemIndex)) {\n <ng-container [ngTemplateOutlet]=\"nativeDragSection() ? nativeSectionGhost : nativeFieldGhost\"/>\n }\n @if (item.field; as field) {\n <ng-container\n [ngTemplateOutlet]=\"fieldCard\"\n [ngTemplateOutletContext]=\"{\n $implicit: field,\n dragData: item\n }\"/>\n } @else if (item.section; as section) {\n <ngs-card class=\"ngs-form-builder-section\"\n cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragData]=\"item\"\n (cdkDragStarted)=\"sectionDragStarted(section)\"\n (cdkDragEnded)=\"sectionDragEnded(section)\"\n (dragover)=\"nativeSectionDragOver($event, section)\"\n (dragleave)=\"nativeDragLeave($event, sectionDropListId(section))\"\n (drop)=\"nativeSectionDrop($event, section)\">\n <ngs-card-header>\n <div class=\"ngs-form-builder-section-heading\">\n <button ngsIconButton type=\"button\" class=\"ngs-form-builder-section-handle\" cdkDragHandle aria-label=\"Move section\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </button>\n <input ngsInput\n class=\"ngs-form-builder-section-title\"\n [ngModel]=\"section.title\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"updateSection(section, { title: $event })\"\n aria-label=\"Section title\">\n </div>\n @if (section.description) {\n <p class=\"truncate text-sm text-secondary\">{{ section.description }}</p>\n }\n\n <ngs-card-aside>\n <div class=\"ngs-form-builder-section-actions\">\n <button ngsIconButton type=\"button\" aria-label=\"Collapse section\" (click)=\"updateSection(section, { collapsed: !section.collapsed })\">\n <ngs-icon [name]=\"isSectionCollapsed(section) ? 'fluent:chevron-down-24-regular' : 'fluent:chevron-up-24-regular'\"/>\n </button>\n <button ngsIconButton type=\"button\" aria-label=\"Delete section\" (click)=\"removeSection(section)\">\n <ngs-icon name=\"fluent:delete-24-regular\"/>\n </button>\n </div>\n </ngs-card-aside>\n </ngs-card-header>\n\n @if (!isSectionCollapsed(section)) {\n <ngs-card-content>\n <ng-container\n [ngTemplateOutlet]=\"fieldList\"\n [ngTemplateOutletContext]=\"{\n $implicit: section.fields,\n containerId: sectionDropListId(section),\n section\n }\"/>\n </ngs-card-content>\n } @else if (isNativeDropTarget(sectionDropListId(section), section.fields.length)) {\n <ngs-card-content>\n <div class=\"ngs-form-builder-drop-list ngs-form-builder-canvas-drop-list\">\n <ng-container [ngTemplateOutlet]=\"nativeFieldGhost\"/>\n </div>\n </ngs-card-content>\n }\n\n <ng-template cdkDragPlaceholder>\n <div class=\"ngs-form-builder-section-line-placeholder\"></div>\n </ng-template>\n\n <ng-template cdkDragPreview>\n <ngs-card class=\"ngs-form-builder-section ngs-form-builder-section-drag-preview\">\n <ngs-card-header>\n <div class=\"ngs-form-builder-section-heading\">\n <span class=\"ngs-form-builder-section-handle\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </span>\n <span class=\"ngs-form-builder-section-preview-title\">{{ section.title }}</span>\n </div>\n <ngs-card-aside>\n <ngs-icon name=\"fluent:chevron-down-24-regular\" class=\"size-5\"/>\n </ngs-card-aside>\n </ngs-card-header>\n </ngs-card>\n </ng-template>\n </ngs-card>\n }\n }\n\n @if (isNativeDropTarget(rootDropListId(), canvasItems().length)) {\n <ng-container [ngTemplateOutlet]=\"nativeDragSection() ? nativeSectionGhost : nativeFieldGhost\"/>\n }\n\n </div>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n\n <ngs-panel-aside class=\"ngs-form-builder-inspector w-88 border-s border-s-subtle\">\n <ngs-scrollbar-area [absolute]=\"true\">\n <div class=\"flex flex-col gap-4 p-5\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <h3 class=\"text-base font-semibold\">{{ inspectorTitle() }}</h3>\n @if (selectedField()) {\n <p class=\"truncate text-sm text-secondary\">{{ selectedField()?.label }}</p>\n }\n </div>\n <button ngsIconButton type=\"button\" aria-label=\"Clear selection\" (click)=\"selectedFieldId.set(null)\">\n <ngs-icon name=\"fluent:dismiss-24-regular\"/>\n </button>\n </div>\n\n @if (selectedField(); as field) {\n <ngs-basic-form-builder-field-settings\n [field]=\"field\"\n [update]=\"updateSelectedField\"/>\n\n <ngs-form-builder-settings-host\n [field]=\"field\"\n [schema]=\"schema()\"\n [definitions]=\"definitions()\"\n [settingsDefinitions]=\"settingsDefinitions()\"\n [update]=\"updateSelectedField\"/>\n\n <button ngsButton=\"text\" type=\"button\" class=\"ngs-form-builder-delete-button\" (click)=\"confirmRemoveField(field)\">\n <ngs-icon name=\"fluent:delete-24-regular\"/>\n Delete field\n </button>\n } @else {\n <div class=\"ngs-form-builder-empty-inspector\">\n Select a field on the canvas or in the fields tree to edit its settings.\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-aside>\n</ngs-panel>\n\n<ng-template #previewDialog>\n <h3 ngs-dialog-title>{{ schema().title || 'Form preview' }}</h3>\n <ngs-dialog-content class=\"ngs-form-builder-preview-dialog-content\">\n <ngs-form-builder-renderer\n [schema]=\"schema()\"\n [showSubmit]=\"false\"/>\n </ngs-dialog-content>\n <ngs-dialog-actions align=\"end\">\n <button ngsButton=\"outlined\" type=\"button\" ngs-dialog-close>Close</button>\n </ngs-dialog-actions>\n</ng-template>\n", styles: [":host{display:block;min-height:640px}:host .ngs-form-builder-shell{height:100%;min-height:640px;overflow:hidden}:host .ngs-form-builder-palette-item{display:flex;width:100%;min-height:calc(var(--spacing, .25rem) * 12);align-items:center;border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-lg);background:var(--ngs-color-surface);color:var(--ngs-color-primary);cursor:grab;font-size:var(--ngs-font-size-sm);font-weight:500;padding:0 calc(var(--spacing, .25rem) * 3);text-align:start;transition:background-color .16s cubic-bezier(0,0,.2,1),border-color .16s cubic-bezier(0,0,.2,1),box-shadow .16s cubic-bezier(0,0,.2,1)}:host .ngs-form-builder-palette-item:hover{border-color:var(--ngs-color-primary);background:var(--ngs-color-primary-container);color:var(--ngs-color-on-primary-container)}:host .ngs-form-builder-palette-item:focus-visible{outline:0;box-shadow:0 0 0 3px var(--ngs-state-focus-ring)}:host .ngs-form-builder-palette-item:active{cursor:grabbing}:host .ngs-form-builder-palette-item-preview{width:calc(var(--spacing, .25rem) * 64)}:host .ngs-form-builder-native-drag-image{position:fixed;inset-block-start:0;inset-inline-start:0;width:calc(var(--spacing, .25rem) * 64);pointer-events:none;transform:translate(-200vw,-200vh);z-index:-1}:host .ngs-form-builder-palette-item-placeholder{width:100%;box-shadow:none;opacity:1;pointer-events:none}:host .ngs-form-builder-palette{background:var(--ngs-color-surface-container-low)}:host .ngs-form-builder-palette-item-content{display:inline-flex;min-width:0;width:100%;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-palette-item-label{display:inline-flex;min-width:0;align-items:center;gap:calc(var(--spacing, .25rem) * 2)}:host .ngs-form-builder-palette-item-label span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ngs-tab-group{--ngs-tab-label-height: calc(var(--spacing, .25rem) * 10)}:host ngs-tree{--ngs-tree-node-gap: calc(var(--spacing, .25rem) * 1);--ngs-tree-node-drop-line-color: var(--ngs-color-primary);--ngs-tree-node-drop-target-outline: 0;--ngs-tree-node-drop-target-bg: var(--ngs-color-primary-container)}:host ngs-tree ngs-tree-node{cursor:grab}:host ngs-tree ngs-tree-node:active{cursor:grabbing}:host .ngs-form-builder-tree-toggle{--ngs-button-height: calc(var(--spacing, .25rem) * 6)}:host .ngs-form-builder-tree-item{min-width:0;flex:1;justify-content:flex-start;overflow:hidden}:host .ngs-form-builder-tree-item span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-tree-item small{margin-inline-start:auto;color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-xs);font-weight:400}:host .ngs-form-builder-tree-item.is-selected{background:var(--ngs-color-primary-container);color:var(--ngs-color-on-primary-container)}:host .ngs-form-builder-tree-item.is-selected small{color:currentColor}:host .ngs-form-builder-tree-drag-placeholder{display:flex;min-width:0;width:100%;min-height:calc(var(--spacing, .25rem) * 10);align-items:center;gap:calc(var(--spacing, .25rem) * 2);border:1px dashed var(--ngs-color-primary);border-radius:var(--ngs-radius-md);background:var(--ngs-color-primary);color:var(--ngs-color-primary);font-size:var(--ngs-font-size-sm);font-weight:500;padding:calc(var(--spacing, .25rem) * 2) calc(var(--spacing, .25rem) * 3);pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-tree-drag-placeholder{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 94%)}}:host .ngs-form-builder-tree-drag-placeholder ngs-icon{flex:none}:host .ngs-form-builder-tree-drag-placeholder span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-drag-icon{display:inline-flex;flex:none;align-items:center;color:var(--ngs-color-on-surface-variant)}:host .ngs-form-builder-canvas{min-height:100%}:host .ngs-form-builder-section{--ngs-card-padding: calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-section-drag-preview{width:min(100%,900px);background:var(--ngs-color-surface)}:host .ngs-form-builder-native-ghost-section{border-color:var(--ngs-color-primary);background:var(--ngs-color-primary);box-shadow:none;opacity:1;pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-native-ghost-section{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 94%)}}:host .ngs-form-builder-canvas-list{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-section-heading{display:flex;min-width:0;flex:1;align-items:center;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-section-handle{flex:none;color:var(--ngs-color-on-surface-variant);cursor:grab}:host .ngs-form-builder-section-handle:active{cursor:grabbing}:host .ngs-form-builder-section-actions{display:flex;align-items:center;gap:calc(var(--spacing, .25rem) * 1)}:host .ngs-form-builder-section-title{width:100%;min-width:0;height:auto;min-height:0;padding:0;border:0;background:transparent;color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-base);font-weight:600;line-height:var(--ngs-line-height-base);outline:0}:host .ngs-form-builder-section-title:focus{outline:2px solid var(--ngs-color-primary);outline-offset:2px;border-radius:var(--ngs-radius-sm)}:host .ngs-form-builder-section-preview-title{min-width:0;overflow:hidden;color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-base);font-weight:600;line-height:var(--ngs-line-height-base);text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-drop-list{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:calc(var(--spacing, .25rem) * 4);min-height:calc(var(--spacing, .25rem) * 16)}:host .ngs-form-builder-field{position:relative;display:flex;flex-direction:column;grid-column:span 12;gap:calc(var(--spacing, .25rem) * 3);border:1px solid transparent;border-radius:var(--ngs-radius-lg);padding:calc(var(--spacing, .25rem) * 3);background:var(--ngs-color-surface);cursor:pointer}:host .ngs-form-builder-field.is-container{align-items:stretch;background:var(--ngs-color-surface-container-lowest)}:host .ngs-form-builder-field.is-width-1{grid-column:span 1}:host .ngs-form-builder-field.is-width-2{grid-column:span 2}:host .ngs-form-builder-field.is-width-3{grid-column:span 3}:host .ngs-form-builder-field.is-width-4{grid-column:span 4}:host .ngs-form-builder-field.is-width-5{grid-column:span 5}:host .ngs-form-builder-field.is-width-6{grid-column:span 6}:host .ngs-form-builder-field.is-width-7{grid-column:span 7}:host .ngs-form-builder-field.is-width-8{grid-column:span 8}:host .ngs-form-builder-field.is-width-9{grid-column:span 9}:host .ngs-form-builder-field.is-width-10{grid-column:span 10}:host .ngs-form-builder-field.is-width-11{grid-column:span 11}:host .ngs-form-builder-field.is-width-12{grid-column:span 12}:host .ngs-form-builder-field:hover{border-color:var(--ngs-color-outline-variant)}:host .ngs-form-builder-field.is-selected{border-color:var(--ngs-color-primary);box-shadow:0 0 0 1px var(--ngs-color-primary)}:host .ngs-form-builder-field-header{display:flex;min-width:0;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-field-label{display:flex;min-width:0;align-items:center;gap:calc(var(--spacing, .25rem) * 2);color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-sm);font-weight:600}:host .ngs-form-builder-field-label ngs-icon{flex:none;color:var(--ngs-color-primary)}:host .ngs-form-builder-field-label span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-field-handle{flex:none;color:var(--ngs-color-on-surface-variant)}:host .ngs-form-builder-field-host{min-width:0;width:100%;grid-column:auto}:host .ngs-form-builder-grid-field{display:flex;min-width:0;flex-direction:column;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-grid-field-header{display:flex;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3);border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-md);background:var(--ngs-color-surface);padding:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-grid-field-header p{overflow:hidden;color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-sm);font-weight:600;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-grid-field-header span{display:block;overflow:hidden;color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-xs);text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-empty-inspector{display:flex;min-height:calc(var(--spacing, .25rem) * 14);align-items:center;justify-content:center;border:1px dashed var(--ngs-color-primary);border-radius:var(--ngs-radius-lg);color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-sm);grid-column:1/-1;padding:calc(var(--spacing, .25rem) * 4);text-align:center}:host .ngs-form-builder-empty-inspector{min-height:calc(var(--spacing, .25rem) * 28);border-color:var(--ngs-color-outline-variant)}:host .ngs-form-builder-delete-button{justify-content:flex-start;color:var(--ngs-color-danger)}:host .ngs-form-builder-preview-dialog-content{width:min(100%,760px)}:host .cdk-drag-preview{box-sizing:border-box;border-radius:var(--ngs-radius-lg);box-shadow:var(--ngs-shadow-lg)}:host .ngs-form-builder-field-placeholder{border-color:var(--ngs-color-primary);background:var(--ngs-color-primary);box-shadow:none;opacity:1;pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-field-placeholder{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 92%)}}:host .ngs-form-builder-field-line-placeholder{min-height:calc(var(--spacing, .25rem) * 6);justify-content:center;padding:0;border:0;background:transparent;box-shadow:none;opacity:1;pointer-events:none}:host .ngs-form-builder-field-line-placeholder:before{content:\"\";display:block;width:100%;height:2px;border-radius:var(--ngs-radius-full);background:var(--ngs-color-primary)}:host .ngs-form-builder-field-drag-preview{width:min(360px,100vw - calc(var(--spacing, .25rem) * 8));min-height:calc(var(--spacing, .25rem) * 20);border-color:var(--ngs-color-primary);background:var(--ngs-color-surface);box-shadow:var(--ngs-shadow-lg);opacity:.94;pointer-events:none}:host .ngs-form-builder-ghost-control{height:calc(var(--spacing, .25rem) * 10);border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-md);background:var(--ngs-color-surface)}:host .ngs-form-builder-canvas-drop-list .cdk-drag-placeholder{min-height:calc(var(--spacing, .25rem) * 18);border:1px dashed var(--ngs-color-primary);background:var(--ngs-color-primary);box-shadow:none;opacity:1;pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-canvas-drop-list .cdk-drag-placeholder{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 94%)}}:host .ngs-form-builder-canvas-drop-list .ngs-form-builder-field-line-placeholder.cdk-drag-placeholder{min-height:calc(var(--spacing, .25rem) * 6);border:0;background:transparent}:host .ngs-form-builder-section-line-placeholder{display:flex;min-height:calc(var(--spacing, .25rem) * 5);align-items:center;padding:0;border:0;background:transparent;box-shadow:none;opacity:1;pointer-events:none}:host .ngs-form-builder-section-line-placeholder:before{content:\"\";display:block;width:100%;height:2px;border-radius:var(--ngs-radius-full);background:var(--ngs-color-primary)}:host .cdk-drag-placeholder{opacity:0}:host .ngs-form-builder-canvas-drop-list .cdk-drag-placeholder,:host .ngs-form-builder-canvas-drop-list .ngs-form-builder-field-line-placeholder.cdk-drag-placeholder{opacity:1}:host .ngs-form-builder-canvas-list .ngs-form-builder-field-line-placeholder.cdk-drag-placeholder,:host .ngs-form-builder-canvas-list .ngs-form-builder-section-line-placeholder.cdk-drag-placeholder{opacity:1}:host .ngs-form-builder-palette .cdk-drag-placeholder{opacity:1}:host .ngs-form-builder-canvas-drop-list.cdk-drop-list-dragging .ngs-form-builder-field:not(.cdk-drag-placeholder){transition:transform .16s cubic-bezier(0,0,.2,1)}:host .ngs-form-builder-canvas-list.cdk-drop-list-dragging .ngs-form-builder-section:not(.cdk-drag-placeholder){transition:transform .16s cubic-bezier(0,0,.2,1)}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "directive", type: CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: Card, selector: "ngs-card", inputs: ["appearance"], exportAs: ["ngsCard"] }, { kind: "component", type: CardAside, selector: "ngs-card-aside, [ngs-card-aside], ngsCardAside" }, { kind: "component", type: CardContent, selector: "ngs-card-content, [ngs-card-content], [ngsCardContent]", inputs: ["withoutPadding"], exportAs: ["ngsCardContent"] }, { kind: "component", type: CardHeader, selector: "ngs-card-header", exportAs: ["ngsCardHeader"] }, { kind: "component", type: DialogActions, selector: "ngs-dialog-actions, [ngs-dialog-actions], [ngsDialogActions]", inputs: ["align"] }, { kind: "directive", type: DialogClose, selector: "[ngs-dialog-close], [ngsDialogClose]", inputs: ["ngs-dialog-close", "ngsDialogClose", "ariaLabel", "type"], exportAs: ["ngsDialogClose"] }, { kind: "component", type: DialogContent, selector: "ngs-dialog-content,[ngs-dialog-content],[ngsDialogContent]" }, { kind: "component", type: DialogTitle, selector: "ngs-dialog-title, [ngs-dialog-title], [ngsDialogTitle]", inputs: ["id"], exportAs: ["ngsDialogTitle"] }, { kind: "component", type: FormField, selector: "ngs-form-field", inputs: ["subscriptHiddenIfEmpty", "sameHeightAsButton"], exportAs: ["ngsFormField"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "component", type: Label, selector: "ngs-label" }, { kind: "component", type: Panel, selector: "ngs-panel", inputs: ["absolute"], exportAs: ["ngsPanel"] }, { kind: "component", type: PanelAside, selector: "ngs-panel-aside" }, { kind: "component", type: PanelContent, selector: "ngs-panel-content", exportAs: ["ngsPanelContent"] }, { kind: "component", type: PanelHeader, selector: "ngs-panel-header", inputs: ["flex", "autoHeight"], exportAs: ["ngsPanelHeader"] }, { kind: "component", type: PanelSidebar, selector: "ngs-panel-sidebar" }, { kind: "component", type: ScrollbarArea, selector: "ngs-scrollbar-area", inputs: ["scrollbarWidth", "autoHide", "absolute"], outputs: ["scrolled"], exportAs: ["ngsScrollbarArea"] }, { kind: "component", type: Tab, selector: "ngs-tab", inputs: ["label", "aria-label", "aria-labelledby", "disabled"], exportAs: ["ngsTab"] }, { kind: "component", type: TabGroup, selector: "ngs-tab-group", inputs: ["selectedIndex", "headerPosition", "preserveContent", "ngs-stretch-tabs", "ngs-align-tabs", "disableRipple", "animationDuration", "animate.enter", "animate.leave"], outputs: ["selectedIndexChange", "selectedTabChange", "focusChange"] }, { kind: "component", type: Tree, selector: "ngs-tree", inputs: ["checkable", "selectable", "draggable", "draggablePredicate", "dropPredicate", "reorderOnDrop", "dragPreview", "childrenKey", "filterValue", "filterPredicate", "filterMode"], outputs: ["checkedChange", "selectedChange", "nodeDrop"], exportAs: ["ngsTree"] }, { kind: "directive", type: TreeDragPlaceholder, selector: "ng-template[ngsTreeDragPlaceholder]" }, { kind: "component", type: TreeNode, selector: "ngs-tree-node", inputs: ["tabIndex", "value", "disabled"], outputs: ["activation", "expandedChange"], exportAs: ["ngsTreeNode"] }, { kind: "directive", type: TreeNodeDef, selector: "[ngsTreeNodeDef]", inputs: ["ngsTreeNodeDefWhen", "ngsTreeNode"] }, { kind: "directive", type: TreeNodePadding, selector: "[ngsTreeNodePadding]", inputs: ["ngsTreeNodePadding", "ngsTreeNodePaddingIndent"] }, { kind: "component", type: FormBuilderFieldHost, selector: "ngs-form-builder-field-host", inputs: ["field", "control", "definitions", "readonly", "editableCanvas"], exportAs: ["ngsFormBuilderFieldHost"] }, { kind: "component", type: FormBuilderRenderer, selector: "ngs-form-builder-renderer", inputs: ["schema", "readonly", "showSubmit", "submitLabel", "value"], outputs: ["valueChange", "formSubmit", "formReady"], exportAs: ["ngsFormBuilderRenderer"] }, { kind: "component", type: FormBuilderSettingsHost, selector: "ngs-form-builder-settings-host", inputs: ["field", "schema", "definitions", "settingsDefinitions", "update"], exportAs: ["ngsFormBuilderSettingsHost"] }, { kind: "component", type: BasicFormBuilderFieldSettings, selector: "ngs-basic-form-builder-field-settings", inputs: ["field", "update"], exportAs: ["ngsBasicFormBuilderFieldSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1537
|
+
}
|
|
1538
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: FormBuilder, decorators: [{
|
|
1539
|
+
type: Component,
|
|
1540
|
+
args: [{ selector: 'ngs-form-builder', exportAs: 'ngsFormBuilder', imports: [
|
|
1541
|
+
NgTemplateOutlet,
|
|
1542
|
+
FormsModule,
|
|
1543
|
+
CdkDrag,
|
|
1544
|
+
CdkDragHandle,
|
|
1545
|
+
CdkDragPlaceholder,
|
|
1546
|
+
CdkDragPreview,
|
|
1547
|
+
CdkDropList,
|
|
1548
|
+
CdkDropListGroup,
|
|
1549
|
+
Button,
|
|
1550
|
+
Card,
|
|
1551
|
+
CardAside,
|
|
1552
|
+
CardContent,
|
|
1553
|
+
CardHeader,
|
|
1554
|
+
DialogActions,
|
|
1555
|
+
DialogClose,
|
|
1556
|
+
DialogContent,
|
|
1557
|
+
DialogTitle,
|
|
1558
|
+
FormField,
|
|
1559
|
+
Icon,
|
|
1560
|
+
Input,
|
|
1561
|
+
Label,
|
|
1562
|
+
Panel,
|
|
1563
|
+
PanelAside,
|
|
1564
|
+
PanelContent,
|
|
1565
|
+
PanelHeader,
|
|
1566
|
+
PanelSidebar,
|
|
1567
|
+
ScrollbarArea,
|
|
1568
|
+
Tab,
|
|
1569
|
+
TabGroup,
|
|
1570
|
+
Tree,
|
|
1571
|
+
TreeDragPlaceholder,
|
|
1572
|
+
TreeNode,
|
|
1573
|
+
TreeNodeDef,
|
|
1574
|
+
TreeNodePadding,
|
|
1575
|
+
FormBuilderFieldHost,
|
|
1576
|
+
FormBuilderRenderer,
|
|
1577
|
+
FormBuilderSettingsHost,
|
|
1578
|
+
BasicFormBuilderFieldSettings
|
|
1579
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1580
|
+
'class': 'ngs-form-builder'
|
|
1581
|
+
}, template: "<ng-template #fieldCard let-field let-section=\"section\" let-dragData=\"dragData\">\n <div\n class=\"ngs-form-builder-field\"\n cdkDrag\n [cdkDragData]=\"dragData || field\"\n [class.is-width-1]=\"(field.width ?? 12) === 1\"\n [class.is-width-2]=\"field.width === 2\"\n [class.is-width-3]=\"field.width === 3\"\n [class.is-width-4]=\"field.width === 4\"\n [class.is-width-5]=\"field.width === 5\"\n [class.is-width-6]=\"field.width === 6\"\n [class.is-width-7]=\"field.width === 7\"\n [class.is-width-8]=\"field.width === 8\"\n [class.is-width-9]=\"field.width === 9\"\n [class.is-width-10]=\"field.width === 10\"\n [class.is-width-11]=\"field.width === 11\"\n [class.is-width-12]=\"(field.width ?? 12) === 12\"\n [class.is-container]=\"isContainerField(field)\"\n [class.is-selected]=\"selectedFieldId() === field.id\"\n [attr.data-form-builder-field-id]=\"field.id\"\n (dragover)=\"nativeContainerFieldDragOver($event, field)\"\n (dragleave)=\"nativeContainerFieldDragLeave($event, field)\"\n (drop)=\"nativeContainerFieldDrop($event, field)\"\n (click)=\"$event.stopPropagation(); selectField(field, section)\">\n <div class=\"ngs-form-builder-field-header\">\n <div class=\"ngs-form-builder-field-label\">\n <ngs-icon [name]=\"fieldIcon(field)\"/>\n <span>{{ field.label }}</span>\n </div>\n\n <button ngsIconButton type=\"button\" class=\"ngs-form-builder-field-handle\" cdkDragHandle aria-label=\"Move field\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </button>\n </div>\n\n @if (isContainerField(field)) {\n <div class=\"ngs-form-builder-grid-field\">\n <ng-container\n [ngTemplateOutlet]=\"fieldList\"\n [ngTemplateOutletContext]=\"{\n $implicit: field.children ?? [],\n containerId: fieldDropListId(field),\n section\n }\"/>\n </div>\n } @else {\n <ngs-form-builder-field-host\n [field]=\"field\"\n [control]=\"previewControl(field)\"\n [definitions]=\"definitions()\"\n [readonly]=\"true\"\n [editableCanvas]=\"true\"/>\n }\n\n <ng-template cdkDragPlaceholder>\n <div\n class=\"ngs-form-builder-field ngs-form-builder-field-line-placeholder\"\n [class.is-width-1]=\"(field.width ?? 12) === 1\"\n [class.is-width-2]=\"field.width === 2\"\n [class.is-width-3]=\"field.width === 3\"\n [class.is-width-4]=\"field.width === 4\"\n [class.is-width-5]=\"field.width === 5\"\n [class.is-width-6]=\"field.width === 6\"\n [class.is-width-7]=\"field.width === 7\"\n [class.is-width-8]=\"field.width === 8\"\n [class.is-width-9]=\"field.width === 9\"\n [class.is-width-10]=\"field.width === 10\"\n [class.is-width-11]=\"field.width === 11\"\n [class.is-width-12]=\"(field.width ?? 12) === 12\">\n </div>\n </ng-template>\n\n <ng-template cdkDragPreview>\n <div\n class=\"ngs-form-builder-field ngs-form-builder-field-drag-preview\"\n [class.is-width-1]=\"(field.width ?? 12) === 1\"\n [class.is-width-2]=\"field.width === 2\"\n [class.is-width-3]=\"field.width === 3\"\n [class.is-width-4]=\"field.width === 4\"\n [class.is-width-5]=\"field.width === 5\"\n [class.is-width-6]=\"field.width === 6\"\n [class.is-width-7]=\"field.width === 7\"\n [class.is-width-8]=\"field.width === 8\"\n [class.is-width-9]=\"field.width === 9\"\n [class.is-width-10]=\"field.width === 10\"\n [class.is-width-11]=\"field.width === 11\"\n [class.is-width-12]=\"(field.width ?? 12) === 12\">\n <div class=\"ngs-form-builder-field-header\">\n <div class=\"ngs-form-builder-field-label\">\n <ngs-icon [name]=\"fieldIcon(field)\"/>\n <span>{{ field.label }}</span>\n </div>\n <span class=\"ngs-form-builder-drag-icon\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </span>\n </div>\n <div class=\"ngs-form-builder-ghost-control\"></div>\n </div>\n </ng-template>\n </div>\n</ng-template>\n\n<ng-template #nativeFieldGhost>\n @if (nativeDragFieldDefinition(); as definition) {\n <div\n class=\"ngs-form-builder-field ngs-form-builder-field-placeholder ngs-form-builder-native-ghost-field\"\n [class.is-width-1]=\"definitionWidth(definition) === 1\"\n [class.is-width-2]=\"definitionWidth(definition) === 2\"\n [class.is-width-3]=\"definitionWidth(definition) === 3\"\n [class.is-width-4]=\"definitionWidth(definition) === 4\"\n [class.is-width-5]=\"definitionWidth(definition) === 5\"\n [class.is-width-6]=\"definitionWidth(definition) === 6\"\n [class.is-width-7]=\"definitionWidth(definition) === 7\"\n [class.is-width-8]=\"definitionWidth(definition) === 8\"\n [class.is-width-9]=\"definitionWidth(definition) === 9\"\n [class.is-width-10]=\"definitionWidth(definition) === 10\"\n [class.is-width-11]=\"definitionWidth(definition) === 11\"\n [class.is-width-12]=\"definitionWidth(definition) === 12\">\n <div class=\"ngs-form-builder-field-header\">\n <div class=\"ngs-form-builder-field-label\">\n <ngs-icon [name]=\"definition.icon || 'fluent:form-24-regular'\"/>\n <span>{{ definition.defaults?.label || definition.label }}</span>\n </div>\n <span class=\"ngs-form-builder-drag-icon\">\n <ngs-icon name=\"fluent:arrow-move-24-regular\"/>\n </span>\n </div>\n <div class=\"ngs-form-builder-ghost-control\"></div>\n </div>\n }\n</ng-template>\n\n<ng-template #nativeSectionGhost>\n <ngs-card class=\"ngs-form-builder-section ngs-form-builder-native-ghost-section\">\n <ngs-card-header>\n <div class=\"ngs-form-builder-section-heading\">\n <span class=\"ngs-form-builder-drag-icon\">\n <ngs-icon name=\"fluent:folder-add-24-regular\"/>\n </span>\n <span class=\"ngs-form-builder-section-title\">Section</span>\n </div>\n </ngs-card-header>\n </ngs-card>\n</ng-template>\n\n<ng-template #fieldList let-fields let-containerId=\"containerId\" let-section=\"section\">\n <div\n cdkDropList\n class=\"ngs-form-builder-drop-list ngs-form-builder-canvas-drop-list\"\n [id]=\"containerId\"\n [cdkDropListData]=\"fields\"\n cdkDropListOrientation=\"mixed\"\n [cdkDropListConnectedTo]=\"fieldContainerDropListIds()\"\n [cdkDropListEnterPredicate]=\"fieldDropListEnterPredicate\"\n (dragover)=\"nativeFieldDragOver($event, containerId)\"\n (dragleave)=\"nativeDragLeave($event, containerId)\"\n (drop)=\"nativeFieldDrop($event, containerId)\"\n (cdkDropListDropped)=\"dropField($event)\">\n @for (field of fields; track field.id; let fieldIndex = $index) {\n @if (isNativeDropTarget(containerId, fieldIndex)) {\n <ng-container [ngTemplateOutlet]=\"nativeFieldGhost\"/>\n }\n <ng-container\n [ngTemplateOutlet]=\"fieldCard\"\n [ngTemplateOutletContext]=\"{\n $implicit: field,\n section\n }\"/>\n }\n\n @if (isNativeDropTarget(containerId, fields.length)) {\n <ng-container [ngTemplateOutlet]=\"nativeFieldGhost\"/>\n }\n </div>\n</ng-template>\n\n<ngs-panel class=\"ngs-form-builder-shell\" cdkDropListGroup>\n <ngs-panel-header autoHeight class=\"flex items-center justify-between gap-4 border-b border-b-subtle px-5 py-3\">\n <div class=\"min-w-0\">\n <p class=\"text-sm text-secondary\">Form builder</p>\n <h2 class=\"truncate text-lg font-semibold\">{{ schema().title || 'Untitled form' }}</h2>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <button ngsButton=\"outlined\" type=\"button\" (click)=\"openPreview(previewDialog)\">\n <ngs-icon name=\"fluent:eye-24-regular\"/>\n Preview\n </button>\n\n </div>\n </ngs-panel-header>\n\n <ngs-panel-sidebar class=\"ngs-form-builder-palette w-80 border-e border-e-subtle\">\n <ngs-scrollbar-area [absolute]=\"true\">\n <div class=\"flex flex-col gap-4 p-4\">\n <h3 class=\"text-base font-semibold\">{{ paletteTitle() }}</h3>\n\n <ngs-tab-group animationDuration=\"0ms\" ngs-stretch-tabs=\"false\">\n <ngs-tab label=\"Placeholders\">\n <div class=\"flex flex-col gap-4 pt-4\">\n <ngs-form-field>\n <ngs-label>Search placeholder</ngs-label>\n <input ngsInput\n [ngModel]=\"search()\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"search.set($event)\">\n </ngs-form-field>\n\n <div class=\"flex flex-col gap-5\">\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-xs font-semibold uppercase tracking-normal text-secondary\">Layout</h4>\n <button\n type=\"button\"\n class=\"ngs-form-builder-palette-item\"\n draggable=\"true\"\n (dragstart)=\"sectionPaletteDragStarted($event)\"\n (dragend)=\"paletteDragEnded()\">\n <span class=\"ngs-form-builder-palette-item-content\">\n <span class=\"ngs-form-builder-palette-item-label\">\n <ngs-icon name=\"fluent:folder-add-24-regular\"/>\n <span>Section</span>\n </span>\n </span>\n </button>\n\n @for (definition of layoutDefinitions(); track definition.type) {\n <button\n type=\"button\"\n class=\"ngs-form-builder-palette-item\"\n draggable=\"true\"\n (dragstart)=\"paletteDragStarted($event, definition)\"\n (dragend)=\"paletteDragEnded()\"\n (click)=\"paletteClicked(definition)\">\n <span class=\"ngs-form-builder-palette-item-content\">\n <span class=\"ngs-form-builder-palette-item-label\">\n <ngs-icon [name]=\"definition.icon || 'fluent:form-24-regular'\"/>\n <span>{{ definition.label }}</span>\n </span>\n </span>\n </button>\n }\n </section>\n\n @for (group of paletteGroups(); track group.name) {\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-xs font-semibold uppercase tracking-normal text-secondary\">{{ group.name }}</h4>\n <div class=\"flex flex-col gap-2\">\n @for (definition of group.fields; track definition.type) {\n <button\n type=\"button\"\n class=\"ngs-form-builder-palette-item\"\n draggable=\"true\"\n (dragstart)=\"paletteDragStarted($event, definition)\"\n (dragend)=\"paletteDragEnded()\"\n (click)=\"paletteClicked(definition)\">\n <span class=\"ngs-form-builder-palette-item-content\">\n <span class=\"ngs-form-builder-palette-item-label\">\n <ngs-icon [name]=\"definition.icon || 'fluent:form-24-regular'\"/>\n <span>{{ definition.label }}</span>\n </span>\n </span>\n </button>\n }\n </div>\n </section>\n }\n </div>\n </div>\n </ngs-tab>\n\n <ngs-tab label=\"Actual fields\">\n <div class=\"pt-4\">\n <ngs-tree\n #actualFieldsTree=\"ngsTree\"\n [dataSource]=\"fieldTree()\"\n [childrenAccessor]=\"fieldTreeChildrenAccessor\"\n [trackBy]=\"trackFieldTreeNode\"\n [draggablePredicate]=\"fieldTreeDraggablePredicate\"\n [dropPredicate]=\"fieldTreeDropPredicate\"\n [reorderOnDrop]=\"false\"\n (nodeDrop)=\"fieldTreeNodeDropped($event)\"\n draggable>\n <ng-template ngsTreeDragPlaceholder let-source=\"source\">\n <div class=\"ngs-form-builder-tree-drag-placeholder\">\n <ngs-icon [name]=\"fieldTreePlaceholderIcon(source)\"/>\n <span>{{ source.label }}</span>\n </div>\n </ng-template>\n\n <ngs-tree-node *ngsTreeNodeDef=\"let node\" ngsTreeNodePadding [value]=\"node.id\">\n <button\n ngsButton=\"text\"\n type=\"button\"\n class=\"ngs-form-builder-tree-item\"\n [class.is-selected]=\"selectedFieldId() === node.id\"\n (click)=\"selectFieldTreeNode(node)\">\n <ngs-icon [name]=\"node.icon\"/>\n <span>{{ node.label }}</span>\n @if (node.name) {\n <small>{{ node.name }}</small>\n }\n </button>\n </ngs-tree-node>\n\n <ngs-tree-node *ngsTreeNodeDef=\"let node; when: hasFieldTreeChildren\"\n ngsTreeNodePadding\n [value]=\"node.id\"\n [cdkTreeNodeTypeaheadLabel]=\"node.label\">\n <button\n ngsIconButton\n class=\"ngs-form-builder-tree-toggle\"\n [attr.aria-label]=\"'Toggle ' + node.label\"\n (click)=\"toggleFieldTreeNode(actualFieldsTree, node, $event)\">\n <ngs-icon [name]=\"isFieldTreeNodeExpanded(actualFieldsTree, node) ? 'fluent:chevron-down-24-regular' : 'fluent:chevron-right-24-regular'\" class=\"size-4\"/>\n </button>\n <button\n ngsButton=\"text\"\n type=\"button\"\n class=\"ngs-form-builder-tree-item\"\n [class.is-selected]=\"selectedFieldId() === node.id\"\n (click)=\"selectFieldTreeNode(node)\">\n <ngs-icon [name]=\"node.icon\"/>\n <span>{{ node.label }}</span>\n @if (node.name) {\n <small>{{ node.name }}</small>\n }\n </button>\n </ngs-tree-node>\n </ngs-tree>\n </div>\n </ngs-tab>\n </ngs-tab-group>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-sidebar>\n\n <ngs-panel-content>\n <ngs-scrollbar-area [absolute]=\"true\">\n <div class=\"ngs-form-builder-canvas flex flex-col gap-4 p-5\">\n <div\n cdkDropList\n class=\"ngs-form-builder-canvas-list\"\n [id]=\"rootDropListId()\"\n [cdkDropListData]=\"canvasItems()\"\n [cdkDropListConnectedTo]=\"fieldContainerDropListIds()\"\n (dragover)=\"nativeCanvasDragOver($event)\"\n (dragleave)=\"nativeDragLeave($event, rootDropListId())\"\n (drop)=\"nativeFieldDrop($event, rootDropListId())\"\n (cdkDropListDropped)=\"dropCanvasItem($event)\">\n @for (item of canvasItems(); track item.kind + ':' + item.id; let itemIndex = $index) {\n @if (isNativeDropTarget(rootDropListId(), itemIndex)) {\n <ng-container [ngTemplateOutlet]=\"nativeDragSection() ? nativeSectionGhost : nativeFieldGhost\"/>\n }\n @if (item.field; as field) {\n <ng-container\n [ngTemplateOutlet]=\"fieldCard\"\n [ngTemplateOutletContext]=\"{\n $implicit: field,\n dragData: item\n }\"/>\n } @else if (item.section; as section) {\n <ngs-card class=\"ngs-form-builder-section\"\n cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragData]=\"item\"\n (cdkDragStarted)=\"sectionDragStarted(section)\"\n (cdkDragEnded)=\"sectionDragEnded(section)\"\n (dragover)=\"nativeSectionDragOver($event, section)\"\n (dragleave)=\"nativeDragLeave($event, sectionDropListId(section))\"\n (drop)=\"nativeSectionDrop($event, section)\">\n <ngs-card-header>\n <div class=\"ngs-form-builder-section-heading\">\n <button ngsIconButton type=\"button\" class=\"ngs-form-builder-section-handle\" cdkDragHandle aria-label=\"Move section\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </button>\n <input ngsInput\n class=\"ngs-form-builder-section-title\"\n [ngModel]=\"section.title\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"updateSection(section, { title: $event })\"\n aria-label=\"Section title\">\n </div>\n @if (section.description) {\n <p class=\"truncate text-sm text-secondary\">{{ section.description }}</p>\n }\n\n <ngs-card-aside>\n <div class=\"ngs-form-builder-section-actions\">\n <button ngsIconButton type=\"button\" aria-label=\"Collapse section\" (click)=\"updateSection(section, { collapsed: !section.collapsed })\">\n <ngs-icon [name]=\"isSectionCollapsed(section) ? 'fluent:chevron-down-24-regular' : 'fluent:chevron-up-24-regular'\"/>\n </button>\n <button ngsIconButton type=\"button\" aria-label=\"Delete section\" (click)=\"removeSection(section)\">\n <ngs-icon name=\"fluent:delete-24-regular\"/>\n </button>\n </div>\n </ngs-card-aside>\n </ngs-card-header>\n\n @if (!isSectionCollapsed(section)) {\n <ngs-card-content>\n <ng-container\n [ngTemplateOutlet]=\"fieldList\"\n [ngTemplateOutletContext]=\"{\n $implicit: section.fields,\n containerId: sectionDropListId(section),\n section\n }\"/>\n </ngs-card-content>\n } @else if (isNativeDropTarget(sectionDropListId(section), section.fields.length)) {\n <ngs-card-content>\n <div class=\"ngs-form-builder-drop-list ngs-form-builder-canvas-drop-list\">\n <ng-container [ngTemplateOutlet]=\"nativeFieldGhost\"/>\n </div>\n </ngs-card-content>\n }\n\n <ng-template cdkDragPlaceholder>\n <div class=\"ngs-form-builder-section-line-placeholder\"></div>\n </ng-template>\n\n <ng-template cdkDragPreview>\n <ngs-card class=\"ngs-form-builder-section ngs-form-builder-section-drag-preview\">\n <ngs-card-header>\n <div class=\"ngs-form-builder-section-heading\">\n <span class=\"ngs-form-builder-section-handle\">\n <ngs-icon name=\"fluent:re-order-dots-vertical-24-regular\"/>\n </span>\n <span class=\"ngs-form-builder-section-preview-title\">{{ section.title }}</span>\n </div>\n <ngs-card-aside>\n <ngs-icon name=\"fluent:chevron-down-24-regular\" class=\"size-5\"/>\n </ngs-card-aside>\n </ngs-card-header>\n </ngs-card>\n </ng-template>\n </ngs-card>\n }\n }\n\n @if (isNativeDropTarget(rootDropListId(), canvasItems().length)) {\n <ng-container [ngTemplateOutlet]=\"nativeDragSection() ? nativeSectionGhost : nativeFieldGhost\"/>\n }\n\n </div>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n\n <ngs-panel-aside class=\"ngs-form-builder-inspector w-88 border-s border-s-subtle\">\n <ngs-scrollbar-area [absolute]=\"true\">\n <div class=\"flex flex-col gap-4 p-5\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <h3 class=\"text-base font-semibold\">{{ inspectorTitle() }}</h3>\n @if (selectedField()) {\n <p class=\"truncate text-sm text-secondary\">{{ selectedField()?.label }}</p>\n }\n </div>\n <button ngsIconButton type=\"button\" aria-label=\"Clear selection\" (click)=\"selectedFieldId.set(null)\">\n <ngs-icon name=\"fluent:dismiss-24-regular\"/>\n </button>\n </div>\n\n @if (selectedField(); as field) {\n <ngs-basic-form-builder-field-settings\n [field]=\"field\"\n [update]=\"updateSelectedField\"/>\n\n <ngs-form-builder-settings-host\n [field]=\"field\"\n [schema]=\"schema()\"\n [definitions]=\"definitions()\"\n [settingsDefinitions]=\"settingsDefinitions()\"\n [update]=\"updateSelectedField\"/>\n\n <button ngsButton=\"text\" type=\"button\" class=\"ngs-form-builder-delete-button\" (click)=\"confirmRemoveField(field)\">\n <ngs-icon name=\"fluent:delete-24-regular\"/>\n Delete field\n </button>\n } @else {\n <div class=\"ngs-form-builder-empty-inspector\">\n Select a field on the canvas or in the fields tree to edit its settings.\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-aside>\n</ngs-panel>\n\n<ng-template #previewDialog>\n <h3 ngs-dialog-title>{{ schema().title || 'Form preview' }}</h3>\n <ngs-dialog-content class=\"ngs-form-builder-preview-dialog-content\">\n <ngs-form-builder-renderer\n [schema]=\"schema()\"\n [showSubmit]=\"false\"/>\n </ngs-dialog-content>\n <ngs-dialog-actions align=\"end\">\n <button ngsButton=\"outlined\" type=\"button\" ngs-dialog-close>Close</button>\n </ngs-dialog-actions>\n</ng-template>\n", styles: [":host{display:block;min-height:640px}:host .ngs-form-builder-shell{height:100%;min-height:640px;overflow:hidden}:host .ngs-form-builder-palette-item{display:flex;width:100%;min-height:calc(var(--spacing, .25rem) * 12);align-items:center;border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-lg);background:var(--ngs-color-surface);color:var(--ngs-color-primary);cursor:grab;font-size:var(--ngs-font-size-sm);font-weight:500;padding:0 calc(var(--spacing, .25rem) * 3);text-align:start;transition:background-color .16s cubic-bezier(0,0,.2,1),border-color .16s cubic-bezier(0,0,.2,1),box-shadow .16s cubic-bezier(0,0,.2,1)}:host .ngs-form-builder-palette-item:hover{border-color:var(--ngs-color-primary);background:var(--ngs-color-primary-container);color:var(--ngs-color-on-primary-container)}:host .ngs-form-builder-palette-item:focus-visible{outline:0;box-shadow:0 0 0 3px var(--ngs-state-focus-ring)}:host .ngs-form-builder-palette-item:active{cursor:grabbing}:host .ngs-form-builder-palette-item-preview{width:calc(var(--spacing, .25rem) * 64)}:host .ngs-form-builder-native-drag-image{position:fixed;inset-block-start:0;inset-inline-start:0;width:calc(var(--spacing, .25rem) * 64);pointer-events:none;transform:translate(-200vw,-200vh);z-index:-1}:host .ngs-form-builder-palette-item-placeholder{width:100%;box-shadow:none;opacity:1;pointer-events:none}:host .ngs-form-builder-palette{background:var(--ngs-color-surface-container-low)}:host .ngs-form-builder-palette-item-content{display:inline-flex;min-width:0;width:100%;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-palette-item-label{display:inline-flex;min-width:0;align-items:center;gap:calc(var(--spacing, .25rem) * 2)}:host .ngs-form-builder-palette-item-label span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ngs-tab-group{--ngs-tab-label-height: calc(var(--spacing, .25rem) * 10)}:host ngs-tree{--ngs-tree-node-gap: calc(var(--spacing, .25rem) * 1);--ngs-tree-node-drop-line-color: var(--ngs-color-primary);--ngs-tree-node-drop-target-outline: 0;--ngs-tree-node-drop-target-bg: var(--ngs-color-primary-container)}:host ngs-tree ngs-tree-node{cursor:grab}:host ngs-tree ngs-tree-node:active{cursor:grabbing}:host .ngs-form-builder-tree-toggle{--ngs-button-height: calc(var(--spacing, .25rem) * 6)}:host .ngs-form-builder-tree-item{min-width:0;flex:1;justify-content:flex-start;overflow:hidden}:host .ngs-form-builder-tree-item span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-tree-item small{margin-inline-start:auto;color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-xs);font-weight:400}:host .ngs-form-builder-tree-item.is-selected{background:var(--ngs-color-primary-container);color:var(--ngs-color-on-primary-container)}:host .ngs-form-builder-tree-item.is-selected small{color:currentColor}:host .ngs-form-builder-tree-drag-placeholder{display:flex;min-width:0;width:100%;min-height:calc(var(--spacing, .25rem) * 10);align-items:center;gap:calc(var(--spacing, .25rem) * 2);border:1px dashed var(--ngs-color-primary);border-radius:var(--ngs-radius-md);background:var(--ngs-color-primary);color:var(--ngs-color-primary);font-size:var(--ngs-font-size-sm);font-weight:500;padding:calc(var(--spacing, .25rem) * 2) calc(var(--spacing, .25rem) * 3);pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-tree-drag-placeholder{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 94%)}}:host .ngs-form-builder-tree-drag-placeholder ngs-icon{flex:none}:host .ngs-form-builder-tree-drag-placeholder span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-drag-icon{display:inline-flex;flex:none;align-items:center;color:var(--ngs-color-on-surface-variant)}:host .ngs-form-builder-canvas{min-height:100%}:host .ngs-form-builder-section{--ngs-card-padding: calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-section-drag-preview{width:min(100%,900px);background:var(--ngs-color-surface)}:host .ngs-form-builder-native-ghost-section{border-color:var(--ngs-color-primary);background:var(--ngs-color-primary);box-shadow:none;opacity:1;pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-native-ghost-section{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 94%)}}:host .ngs-form-builder-canvas-list{display:flex;flex-direction:column;gap:calc(var(--spacing, .25rem) * 4)}:host .ngs-form-builder-section-heading{display:flex;min-width:0;flex:1;align-items:center;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-section-handle{flex:none;color:var(--ngs-color-on-surface-variant);cursor:grab}:host .ngs-form-builder-section-handle:active{cursor:grabbing}:host .ngs-form-builder-section-actions{display:flex;align-items:center;gap:calc(var(--spacing, .25rem) * 1)}:host .ngs-form-builder-section-title{width:100%;min-width:0;height:auto;min-height:0;padding:0;border:0;background:transparent;color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-base);font-weight:600;line-height:var(--ngs-line-height-base);outline:0}:host .ngs-form-builder-section-title:focus{outline:2px solid var(--ngs-color-primary);outline-offset:2px;border-radius:var(--ngs-radius-sm)}:host .ngs-form-builder-section-preview-title{min-width:0;overflow:hidden;color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-base);font-weight:600;line-height:var(--ngs-line-height-base);text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-drop-list{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:calc(var(--spacing, .25rem) * 4);min-height:calc(var(--spacing, .25rem) * 16)}:host .ngs-form-builder-field{position:relative;display:flex;flex-direction:column;grid-column:span 12;gap:calc(var(--spacing, .25rem) * 3);border:1px solid transparent;border-radius:var(--ngs-radius-lg);padding:calc(var(--spacing, .25rem) * 3);background:var(--ngs-color-surface);cursor:pointer}:host .ngs-form-builder-field.is-container{align-items:stretch;background:var(--ngs-color-surface-container-lowest)}:host .ngs-form-builder-field.is-width-1{grid-column:span 1}:host .ngs-form-builder-field.is-width-2{grid-column:span 2}:host .ngs-form-builder-field.is-width-3{grid-column:span 3}:host .ngs-form-builder-field.is-width-4{grid-column:span 4}:host .ngs-form-builder-field.is-width-5{grid-column:span 5}:host .ngs-form-builder-field.is-width-6{grid-column:span 6}:host .ngs-form-builder-field.is-width-7{grid-column:span 7}:host .ngs-form-builder-field.is-width-8{grid-column:span 8}:host .ngs-form-builder-field.is-width-9{grid-column:span 9}:host .ngs-form-builder-field.is-width-10{grid-column:span 10}:host .ngs-form-builder-field.is-width-11{grid-column:span 11}:host .ngs-form-builder-field.is-width-12{grid-column:span 12}:host .ngs-form-builder-field:hover{border-color:var(--ngs-color-outline-variant)}:host .ngs-form-builder-field.is-selected{border-color:var(--ngs-color-primary);box-shadow:0 0 0 1px var(--ngs-color-primary)}:host .ngs-form-builder-field-header{display:flex;min-width:0;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-field-label{display:flex;min-width:0;align-items:center;gap:calc(var(--spacing, .25rem) * 2);color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-sm);font-weight:600}:host .ngs-form-builder-field-label ngs-icon{flex:none;color:var(--ngs-color-primary)}:host .ngs-form-builder-field-label span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-field-handle{flex:none;color:var(--ngs-color-on-surface-variant)}:host .ngs-form-builder-field-host{min-width:0;width:100%;grid-column:auto}:host .ngs-form-builder-grid-field{display:flex;min-width:0;flex-direction:column;gap:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-grid-field-header{display:flex;align-items:center;justify-content:space-between;gap:calc(var(--spacing, .25rem) * 3);border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-md);background:var(--ngs-color-surface);padding:calc(var(--spacing, .25rem) * 3)}:host .ngs-form-builder-grid-field-header p{overflow:hidden;color:var(--ngs-color-on-surface);font-size:var(--ngs-font-size-sm);font-weight:600;text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-grid-field-header span{display:block;overflow:hidden;color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-xs);text-overflow:ellipsis;white-space:nowrap}:host .ngs-form-builder-empty-inspector{display:flex;min-height:calc(var(--spacing, .25rem) * 14);align-items:center;justify-content:center;border:1px dashed var(--ngs-color-primary);border-radius:var(--ngs-radius-lg);color:var(--ngs-color-on-surface-variant);font-size:var(--ngs-font-size-sm);grid-column:1/-1;padding:calc(var(--spacing, .25rem) * 4);text-align:center}:host .ngs-form-builder-empty-inspector{min-height:calc(var(--spacing, .25rem) * 28);border-color:var(--ngs-color-outline-variant)}:host .ngs-form-builder-delete-button{justify-content:flex-start;color:var(--ngs-color-danger)}:host .ngs-form-builder-preview-dialog-content{width:min(100%,760px)}:host .cdk-drag-preview{box-sizing:border-box;border-radius:var(--ngs-radius-lg);box-shadow:var(--ngs-shadow-lg)}:host .ngs-form-builder-field-placeholder{border-color:var(--ngs-color-primary);background:var(--ngs-color-primary);box-shadow:none;opacity:1;pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-field-placeholder{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 92%)}}:host .ngs-form-builder-field-line-placeholder{min-height:calc(var(--spacing, .25rem) * 6);justify-content:center;padding:0;border:0;background:transparent;box-shadow:none;opacity:1;pointer-events:none}:host .ngs-form-builder-field-line-placeholder:before{content:\"\";display:block;width:100%;height:2px;border-radius:var(--ngs-radius-full);background:var(--ngs-color-primary)}:host .ngs-form-builder-field-drag-preview{width:min(360px,100vw - calc(var(--spacing, .25rem) * 8));min-height:calc(var(--spacing, .25rem) * 20);border-color:var(--ngs-color-primary);background:var(--ngs-color-surface);box-shadow:var(--ngs-shadow-lg);opacity:.94;pointer-events:none}:host .ngs-form-builder-ghost-control{height:calc(var(--spacing, .25rem) * 10);border:1px solid var(--ngs-color-outline-variant);border-radius:var(--ngs-radius-md);background:var(--ngs-color-surface)}:host .ngs-form-builder-canvas-drop-list .cdk-drag-placeholder{min-height:calc(var(--spacing, .25rem) * 18);border:1px dashed var(--ngs-color-primary);background:var(--ngs-color-primary);box-shadow:none;opacity:1;pointer-events:none}@supports (color: color-mix(in lab,red,red)){:host .ngs-form-builder-canvas-drop-list .cdk-drag-placeholder{background:color-mix(in srgb,var(--ngs-color-primary),var(--ngs-color-surface) 94%)}}:host .ngs-form-builder-canvas-drop-list .ngs-form-builder-field-line-placeholder.cdk-drag-placeholder{min-height:calc(var(--spacing, .25rem) * 6);border:0;background:transparent}:host .ngs-form-builder-section-line-placeholder{display:flex;min-height:calc(var(--spacing, .25rem) * 5);align-items:center;padding:0;border:0;background:transparent;box-shadow:none;opacity:1;pointer-events:none}:host .ngs-form-builder-section-line-placeholder:before{content:\"\";display:block;width:100%;height:2px;border-radius:var(--ngs-radius-full);background:var(--ngs-color-primary)}:host .cdk-drag-placeholder{opacity:0}:host .ngs-form-builder-canvas-drop-list .cdk-drag-placeholder,:host .ngs-form-builder-canvas-drop-list .ngs-form-builder-field-line-placeholder.cdk-drag-placeholder{opacity:1}:host .ngs-form-builder-canvas-list .ngs-form-builder-field-line-placeholder.cdk-drag-placeholder,:host .ngs-form-builder-canvas-list .ngs-form-builder-section-line-placeholder.cdk-drag-placeholder{opacity:1}:host .ngs-form-builder-palette .cdk-drag-placeholder{opacity:1}:host .ngs-form-builder-canvas-drop-list.cdk-drop-list-dragging .ngs-form-builder-field:not(.cdk-drag-placeholder){transition:transform .16s cubic-bezier(0,0,.2,1)}:host .ngs-form-builder-canvas-list.cdk-drop-list-dragging .ngs-form-builder-section:not(.cdk-drag-placeholder){transition:transform .16s cubic-bezier(0,0,.2,1)}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"] }]
|
|
1582
|
+
}], propDecorators: { actualFieldsTree: [{ type: i0.ViewChild, args: ['actualFieldsTree', { isSignal: true }] }], schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }, { type: i0.Output, args: ["schemaChange"] }], paletteTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paletteTitle", required: false }] }], inspectorTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "inspectorTitle", required: false }] }], fieldSelected: [{ type: i0.Output, args: ["fieldSelected"] }], fieldAdded: [{ type: i0.Output, args: ["fieldAdded"] }], fieldRemoved: [{ type: i0.Output, args: ["fieldRemoved"] }] } });
|
|
1583
|
+
function createDefaultFormBuilderSchema() {
|
|
1584
|
+
const sectionId = uniqueId('section');
|
|
1585
|
+
return {
|
|
1586
|
+
title: 'New form',
|
|
1587
|
+
fields: [],
|
|
1588
|
+
layout: [{ kind: 'section', id: sectionId }],
|
|
1589
|
+
sections: [
|
|
1590
|
+
{
|
|
1591
|
+
id: sectionId,
|
|
1592
|
+
title: 'General information',
|
|
1593
|
+
fields: []
|
|
1594
|
+
}
|
|
1595
|
+
]
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function uniqueId(prefix) {
|
|
1599
|
+
return `${prefix}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1600
|
+
}
|
|
1601
|
+
function cloneSchema(schema) {
|
|
1602
|
+
return {
|
|
1603
|
+
...schema,
|
|
1604
|
+
fields: (schema.fields ?? []).map(cloneField),
|
|
1605
|
+
layout: schema.layout?.map(item => ({ ...item })),
|
|
1606
|
+
sections: schema.sections.map(section => ({
|
|
1607
|
+
...section,
|
|
1608
|
+
fields: section.fields.map(cloneField)
|
|
1609
|
+
}))
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function cloneField(field) {
|
|
1613
|
+
return {
|
|
1614
|
+
...field,
|
|
1615
|
+
options: field.options?.map(option => ({ ...option })),
|
|
1616
|
+
validation: field.validation?.map(rule => ({ ...rule })),
|
|
1617
|
+
visibility: field.visibility ? { ...field.visibility } : undefined,
|
|
1618
|
+
settings: field.settings ? { ...field.settings } : undefined,
|
|
1619
|
+
children: field.children?.map(cloneField)
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function isFormBuilderLayoutItem(value) {
|
|
1623
|
+
if (!value || typeof value !== 'object') {
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
const item = value;
|
|
1627
|
+
return (item.kind === 'field' || item.kind === 'section') && typeof item.id === 'string';
|
|
1628
|
+
}
|
|
1629
|
+
function isFormBuilderField(value) {
|
|
1630
|
+
if (!value || typeof value !== 'object') {
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
const field = value;
|
|
1634
|
+
return typeof field.id === 'string' && typeof field.type === 'string';
|
|
1635
|
+
}
|
|
1636
|
+
function clampIndex(index, length) {
|
|
1637
|
+
return Math.max(0, Math.min(index, length));
|
|
1638
|
+
}
|
|
1639
|
+
function toFieldName(label) {
|
|
1640
|
+
return label
|
|
1641
|
+
.trim()
|
|
1642
|
+
.toLowerCase()
|
|
1643
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
1644
|
+
.replace(/^_+|_+$/g, '') || 'field';
|
|
1645
|
+
}
|
|
1646
|
+
function uniqueFieldName(baseName, schema) {
|
|
1647
|
+
const usedNames = new Set([
|
|
1648
|
+
...(schema.fields ?? []).flatMap(field => collectFieldNames(field)),
|
|
1649
|
+
...schema.sections.flatMap(section => section.fields.flatMap(field => collectFieldNames(field)))
|
|
1650
|
+
]);
|
|
1651
|
+
if (!usedNames.has(baseName)) {
|
|
1652
|
+
return baseName;
|
|
1653
|
+
}
|
|
1654
|
+
let index = 2;
|
|
1655
|
+
let nextName = `${baseName}_${index}`;
|
|
1656
|
+
while (usedNames.has(nextName)) {
|
|
1657
|
+
index += 1;
|
|
1658
|
+
nextName = `${baseName}_${index}`;
|
|
1659
|
+
}
|
|
1660
|
+
return nextName;
|
|
1661
|
+
}
|
|
1662
|
+
function collectFieldNames(field) {
|
|
1663
|
+
return [field.name, ...(field.children ?? []).flatMap(collectFieldNames)];
|
|
1664
|
+
}
|
|
1665
|
+
function findFieldLocationInFields(fields, fieldId, section) {
|
|
1666
|
+
const index = fields.findIndex(field => field.id === fieldId);
|
|
1667
|
+
if (index !== -1) {
|
|
1668
|
+
return {
|
|
1669
|
+
field: fields[index],
|
|
1670
|
+
section,
|
|
1671
|
+
siblings: fields,
|
|
1672
|
+
index
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
for (const field of fields) {
|
|
1676
|
+
const location = findFieldLocationInFields(field.children ?? [], fieldId, section);
|
|
1677
|
+
if (location) {
|
|
1678
|
+
return location;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
function containsField(field, fieldId) {
|
|
1684
|
+
if (!fieldId) {
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
return (field.children ?? []).some(child => child.id === fieldId || containsField(child, fieldId));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Generated bundle index. Do not edit.
|
|
1692
|
+
*/
|
|
1693
|
+
|
|
1694
|
+
export { BasicFormBuilderFieldSettings, DEFAULT_FORM_BUILDER_FIELDS, FORM_BUILDER_FIELDS, FORM_BUILDER_SETTINGS, FormBuilder, FormBuilderFieldHost, FormBuilderRenderer, FormBuilderSettingsHost, FormBuilderRenderer as FormRenderer, createDefaultFormBuilderSchema, formBuilderField, formBuilderSettings, provideFormBuilder, validatorsFromRules };
|
|
1695
|
+
//# sourceMappingURL=ngstarter-ui-components-form-builder.mjs.map
|