@mosaicoo/form-angular 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/fesm2022/mosaicoo-form-angular-designer.mjs +257 -17
- package/fesm2022/mosaicoo-form-angular-designer.mjs.map +1 -1
- package/fesm2022/mosaicoo-form-angular.mjs +191 -41
- package/fesm2022/mosaicoo-form-angular.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mosaicoo-form-angular-designer.d.ts +40 -2
- package/types/mosaicoo-form-angular.d.ts +25 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NgComponentOutlet, NgTemplateOutlet, DOCUMENT } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
import { InjectionToken, makeEnvironmentProviders, input, signal, inject, ChangeDetectorRef, reflectComponentType, effect, untracked, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, output } from '@angular/core';
|
|
4
|
-
import { maskValue, importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
|
|
4
|
+
import { maskValue, formatNumber, parseNumber, importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
|
|
5
5
|
|
|
6
6
|
/** Multi-provider token: maps merge, later providers win on conflicts. */
|
|
7
7
|
const MFORM_FIELD_COMPONENTS = new InjectionToken('MFORM_FIELD_COMPONENTS');
|
|
@@ -130,6 +130,8 @@ class FormNodeComponent {
|
|
|
130
130
|
/** Discards resolutions that arrive after a newer request started. */
|
|
131
131
|
optionsRequestSeq = 0;
|
|
132
132
|
constructor() {
|
|
133
|
+
// Autocomplete fields refresh their cached results on observed changes.
|
|
134
|
+
effect(() => this.watchReferenceRefresh());
|
|
133
135
|
effect(() => {
|
|
134
136
|
const node = this.node();
|
|
135
137
|
const registry = this.providers();
|
|
@@ -244,10 +246,28 @@ class FormNodeComponent {
|
|
|
244
246
|
return 'array';
|
|
245
247
|
if (node.type === 'tabs')
|
|
246
248
|
return 'tabs';
|
|
249
|
+
if (node.type === 'accordion')
|
|
250
|
+
return 'accordion';
|
|
247
251
|
if (node.type === 'columns' && node.columns?.length)
|
|
248
252
|
return 'columns';
|
|
249
253
|
return 'block';
|
|
250
254
|
}
|
|
255
|
+
/** Open accordion sections; the first one starts expanded. */
|
|
256
|
+
openSections = signal(null, ...(ngDevMode ? [{ debugName: "openSections" }] : /* istanbul ignore next */ []));
|
|
257
|
+
isSectionOpen(index) {
|
|
258
|
+
const open = this.openSections();
|
|
259
|
+
return open === null ? index === 0 : open.has(index);
|
|
260
|
+
}
|
|
261
|
+
toggleSection(index) {
|
|
262
|
+
this.openSections.update((current) => {
|
|
263
|
+
const next = new Set(current ?? [0]);
|
|
264
|
+
if (next.has(index))
|
|
265
|
+
next.delete(index);
|
|
266
|
+
else
|
|
267
|
+
next.add(index);
|
|
268
|
+
return next;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
251
271
|
columns() {
|
|
252
272
|
const node = this.container();
|
|
253
273
|
return (node.columns ?? []).map((column) => column.children
|
|
@@ -268,11 +288,29 @@ class FormNodeComponent {
|
|
|
268
288
|
const value = this.value();
|
|
269
289
|
if (value === undefined || value === null)
|
|
270
290
|
return '';
|
|
271
|
-
const
|
|
272
|
-
if (mask && typeof value === 'string')
|
|
273
|
-
return maskValue(value, mask).masked;
|
|
291
|
+
const format = this.field().format;
|
|
292
|
+
if (format?.mask && typeof value === 'string')
|
|
293
|
+
return maskValue(value, format.mask).masked;
|
|
294
|
+
// Numbers show formatted while idle and raw while being edited, so the
|
|
295
|
+
// caret never fights the thousands separators.
|
|
296
|
+
if (format?.number && !this.focused())
|
|
297
|
+
return formatNumber(value, format.number);
|
|
274
298
|
return String(value);
|
|
275
299
|
}
|
|
300
|
+
/** Tracks focus so numeric formatting steps aside during typing. */
|
|
301
|
+
focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
|
|
302
|
+
onFocus() {
|
|
303
|
+
if (this.field().format?.number)
|
|
304
|
+
this.focused.set(true);
|
|
305
|
+
}
|
|
306
|
+
onBlur() {
|
|
307
|
+
this.focused.set(false);
|
|
308
|
+
this.touch();
|
|
309
|
+
}
|
|
310
|
+
/** Numeric inputs stay `text` when formatted (a number input rejects `R$`). */
|
|
311
|
+
numericFormatted() {
|
|
312
|
+
return this.field().format?.number !== undefined;
|
|
313
|
+
}
|
|
276
314
|
mapValue() {
|
|
277
315
|
const value = this.value();
|
|
278
316
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -331,11 +369,16 @@ class FormNodeComponent {
|
|
|
331
369
|
setString(event) {
|
|
332
370
|
const target = event.target;
|
|
333
371
|
const raw = target.value;
|
|
334
|
-
const
|
|
335
|
-
if (mask) {
|
|
336
|
-
const { masked, raw: stripped } = maskValue(raw, mask);
|
|
372
|
+
const format = this.field().format;
|
|
373
|
+
if (format?.mask) {
|
|
374
|
+
const { masked, raw: stripped } = maskValue(raw, format.mask);
|
|
337
375
|
target.value = masked; // the control always shows the masked shape
|
|
338
|
-
this.setValue(
|
|
376
|
+
this.setValue(format.keepMask ? masked : stripped);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (format?.number) {
|
|
380
|
+
// Locale-aware parse: `1.234,56` and `R$ 1.234,56` both land as a number.
|
|
381
|
+
this.setValue(parseNumber(raw, format.number));
|
|
339
382
|
return;
|
|
340
383
|
}
|
|
341
384
|
if (this.field().dataType === 'number') {
|
|
@@ -487,11 +530,41 @@ class FormNodeComponent {
|
|
|
487
530
|
refTimer = null;
|
|
488
531
|
/** Label of the chosen option (falls back to the raw value). */
|
|
489
532
|
refLabel = signal(null, ...(ngDevMode ? [{ debugName: "refLabel" }] : /* istanbul ignore next */ []));
|
|
533
|
+
/** Last query typed, replayed when `refreshOn` invalidates the results. */
|
|
534
|
+
refQuery = '';
|
|
535
|
+
refRefreshKey = null;
|
|
536
|
+
/**
|
|
537
|
+
* `refreshOn` for autocomplete: when the observed data changes, cached
|
|
538
|
+
* results are dropped so the next search runs against the new context
|
|
539
|
+
* (e.g. properties filtered by the selected client).
|
|
540
|
+
*/
|
|
541
|
+
watchReferenceRefresh() {
|
|
542
|
+
const field = this.field();
|
|
543
|
+
const source = field.optionsSource;
|
|
544
|
+
if (field.type !== 'reference' || source?.type !== 'provider' || !source.refreshOn)
|
|
545
|
+
return;
|
|
546
|
+
this.tick();
|
|
547
|
+
const engine = untracked(() => this.engine());
|
|
548
|
+
const key = source.refreshOn === 'data'
|
|
549
|
+
? JSON.stringify(engine.getData())
|
|
550
|
+
: JSON.stringify(engine.getValue(source.refreshOn) ?? null);
|
|
551
|
+
if (this.refRefreshKey === null) {
|
|
552
|
+
this.refRefreshKey = key;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (key === this.refRefreshKey)
|
|
556
|
+
return;
|
|
557
|
+
this.refRefreshKey = key;
|
|
558
|
+
this.refOptions.set([]);
|
|
559
|
+
if (this.refOpen())
|
|
560
|
+
this.refResolve(this.refQuery);
|
|
561
|
+
}
|
|
490
562
|
refDisplay() {
|
|
491
563
|
return this.refLabel() ?? this.stringValue();
|
|
492
564
|
}
|
|
493
565
|
refSearch(event) {
|
|
494
566
|
const query = event.target.value;
|
|
567
|
+
this.refQuery = query;
|
|
495
568
|
this.refLabel.set(null);
|
|
496
569
|
if (this.refTimer)
|
|
497
570
|
clearTimeout(this.refTimer);
|
|
@@ -583,6 +656,42 @@ class FormNodeComponent {
|
|
|
583
656
|
}
|
|
584
657
|
</div>
|
|
585
658
|
}
|
|
659
|
+
@case ('accordion') {
|
|
660
|
+
<div class="mform-accordion">
|
|
661
|
+
@for (section of container().children; track section.key; let i = $index) {
|
|
662
|
+
<section class="mform-acc-item" [class.is-open]="isSectionOpen(i)">
|
|
663
|
+
<h3 class="mform-acc-head">
|
|
664
|
+
<button
|
|
665
|
+
type="button"
|
|
666
|
+
class="mform-acc-toggle"
|
|
667
|
+
[attr.aria-expanded]="isSectionOpen(i)"
|
|
668
|
+
[attr.aria-controls]="controlId() + '-sec-' + i"
|
|
669
|
+
(click)="toggleSection(i)"
|
|
670
|
+
>
|
|
671
|
+
<span class="mform-acc-caret" aria-hidden="true">
|
|
672
|
+
{{ isSectionOpen(i) ? '▾' : '▸' }}
|
|
673
|
+
</span>
|
|
674
|
+
{{ section.label || 'Section ' + (i + 1) }}
|
|
675
|
+
</button>
|
|
676
|
+
</h3>
|
|
677
|
+
@if (isSectionOpen(i)) {
|
|
678
|
+
<div class="mform-acc-body" [id]="controlId() + '-sec-' + i" role="region">
|
|
679
|
+
<mform-node
|
|
680
|
+
[node]="section"
|
|
681
|
+
[engine]="engine()"
|
|
682
|
+
[tick]="tick()"
|
|
683
|
+
[providers]="providers()"
|
|
684
|
+
[mode]="mode()"
|
|
685
|
+
[notify]="notify()"
|
|
686
|
+
[scope]="childScope()"
|
|
687
|
+
[bare]="true"
|
|
688
|
+
/>
|
|
689
|
+
</div>
|
|
690
|
+
}
|
|
691
|
+
</section>
|
|
692
|
+
}
|
|
693
|
+
</div>
|
|
694
|
+
}
|
|
586
695
|
@case ('tabs') {
|
|
587
696
|
<div class="mform-tabs">
|
|
588
697
|
<div class="mform-tabbar" role="tablist">
|
|
@@ -843,18 +952,19 @@ class FormNodeComponent {
|
|
|
843
952
|
}
|
|
844
953
|
<input
|
|
845
954
|
class="mform-control"
|
|
846
|
-
[type]="inputType()"
|
|
955
|
+
[type]="numericFormatted() ? 'text' : inputType()"
|
|
847
956
|
[id]="controlId()"
|
|
848
957
|
[value]="stringValue()"
|
|
849
958
|
[placeholder]="field().placeholder || ''"
|
|
850
959
|
[disabled]="controlDisabled()"
|
|
851
|
-
[attr.step]="field().type === 'currency' ? '0.01' : null"
|
|
852
|
-
[attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
|
|
960
|
+
[attr.step]="field().type === 'currency' && !numericFormatted() ? '0.01' : null"
|
|
961
|
+
[attr.inputmode]="field().type === 'currency' || numericFormatted() ? 'decimal' : null"
|
|
853
962
|
[attr.aria-required]="required() || null"
|
|
854
963
|
[attr.aria-invalid]="errors().length > 0 || null"
|
|
855
964
|
[attr.aria-describedby]="describedBy()"
|
|
856
965
|
(input)="setString($event)"
|
|
857
|
-
(
|
|
966
|
+
(focus)="onFocus()"
|
|
967
|
+
(blur)="onBlur()"
|
|
858
968
|
/>
|
|
859
969
|
@if (suffix(); as s) {
|
|
860
970
|
<span class="mform-suffix" aria-hidden="true">{{ s }}</span>
|
|
@@ -918,17 +1028,11 @@ class FormNodeComponent {
|
|
|
918
1028
|
</ul>
|
|
919
1029
|
}
|
|
920
1030
|
</ng-template>
|
|
921
|
-
`, isInline: true, dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1031
|
+
`, isInline: true, styles: [".mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-field.is-pending .mform-control{background-image:linear-gradient(90deg,transparent 0%,rgb(109 75 208 / 12%) 50%,transparent 100%);background-size:200% 100%;animation:mform-pending 1.2s linear infinite}@keyframes mform-pending{0%{background-position:200% 0}to{background-position:-200% 0}}.mform-files{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-direction:column;gap:4px}.mform-files.is-gallery{flex-direction:row;flex-wrap:wrap;gap:10px}.mform-file{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--mform-muted, #6b6580)}.mform-files.is-gallery .mform-file{flex-direction:column;gap:4px;width:var(--mform-thumb-size, 96px);text-align:center;position:relative}.mform-thumb{width:var(--mform-thumb-size, 96px);height:var(--mform-thumb-size, 96px);object-fit:cover;border-radius:var(--mform-control-radius, 8px);border:1px solid var(--mform-border, #d9d5e6);background:var(--mform-bg, #f6f5fa)}.mform-file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.mform-file-remove{border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer;font-size:12px;padding:0 4px}.mform-file-remove:hover{color:var(--mform-danger, #c62842)}.mform-files.is-gallery .mform-file-remove{position:absolute;top:2px;right:2px;background:var(--mform-surface, #fff);border-radius:50%;width:20px;height:20px;line-height:1;box-shadow:0 1px 4px #1f1b2e33}.mform-reference{position:relative}.mform-ref-list{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;margin:0;padding:4px;list-style:none;background:var(--mform-control-bg, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);box-shadow:0 8px 24px #1f1b2e1f;max-height:220px;overflow-y:auto}.mform-ref-list button{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:7px 10px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.mform-ref-list button:hover{background:var(--mform-bg, #f6f5fa)}.mform-accordion{display:flex;flex-direction:column;border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-acc-item+.mform-acc-item{border-top:1px solid var(--mform-border, #d9d5e6)}.mform-acc-head{margin:0}.mform-acc-toggle{display:flex;align-items:center;gap:8px;width:100%;font:inherit;font-size:14px;font-weight:600;text-align:left;padding:12px 16px;border:none;background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);cursor:pointer}.mform-acc-caret{font-size:11px;color:var(--mform-muted, #6b6580)}.mform-acc-body{padding:var(--mform-panel-padding, 16px);display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}\n"], dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
922
1032
|
}
|
|
923
1033
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, decorators: [{
|
|
924
1034
|
type: Component,
|
|
925
|
-
args: [{
|
|
926
|
-
selector: 'mform-node',
|
|
927
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
928
|
-
encapsulation: ViewEncapsulation.None,
|
|
929
|
-
host: { class: 'mform-node' },
|
|
930
|
-
imports: [NgComponentOutlet, NgTemplateOutlet],
|
|
931
|
-
template: `
|
|
1035
|
+
args: [{ selector: 'mform-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'mform-node' }, imports: [NgComponentOutlet, NgTemplateOutlet], template: `
|
|
932
1036
|
@let n = node();
|
|
933
1037
|
@if (visible()) {
|
|
934
1038
|
@switch (n.kind) {
|
|
@@ -963,6 +1067,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
963
1067
|
}
|
|
964
1068
|
</div>
|
|
965
1069
|
}
|
|
1070
|
+
@case ('accordion') {
|
|
1071
|
+
<div class="mform-accordion">
|
|
1072
|
+
@for (section of container().children; track section.key; let i = $index) {
|
|
1073
|
+
<section class="mform-acc-item" [class.is-open]="isSectionOpen(i)">
|
|
1074
|
+
<h3 class="mform-acc-head">
|
|
1075
|
+
<button
|
|
1076
|
+
type="button"
|
|
1077
|
+
class="mform-acc-toggle"
|
|
1078
|
+
[attr.aria-expanded]="isSectionOpen(i)"
|
|
1079
|
+
[attr.aria-controls]="controlId() + '-sec-' + i"
|
|
1080
|
+
(click)="toggleSection(i)"
|
|
1081
|
+
>
|
|
1082
|
+
<span class="mform-acc-caret" aria-hidden="true">
|
|
1083
|
+
{{ isSectionOpen(i) ? '▾' : '▸' }}
|
|
1084
|
+
</span>
|
|
1085
|
+
{{ section.label || 'Section ' + (i + 1) }}
|
|
1086
|
+
</button>
|
|
1087
|
+
</h3>
|
|
1088
|
+
@if (isSectionOpen(i)) {
|
|
1089
|
+
<div class="mform-acc-body" [id]="controlId() + '-sec-' + i" role="region">
|
|
1090
|
+
<mform-node
|
|
1091
|
+
[node]="section"
|
|
1092
|
+
[engine]="engine()"
|
|
1093
|
+
[tick]="tick()"
|
|
1094
|
+
[providers]="providers()"
|
|
1095
|
+
[mode]="mode()"
|
|
1096
|
+
[notify]="notify()"
|
|
1097
|
+
[scope]="childScope()"
|
|
1098
|
+
[bare]="true"
|
|
1099
|
+
/>
|
|
1100
|
+
</div>
|
|
1101
|
+
}
|
|
1102
|
+
</section>
|
|
1103
|
+
}
|
|
1104
|
+
</div>
|
|
1105
|
+
}
|
|
966
1106
|
@case ('tabs') {
|
|
967
1107
|
<div class="mform-tabs">
|
|
968
1108
|
<div class="mform-tabbar" role="tablist">
|
|
@@ -1223,18 +1363,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1223
1363
|
}
|
|
1224
1364
|
<input
|
|
1225
1365
|
class="mform-control"
|
|
1226
|
-
[type]="inputType()"
|
|
1366
|
+
[type]="numericFormatted() ? 'text' : inputType()"
|
|
1227
1367
|
[id]="controlId()"
|
|
1228
1368
|
[value]="stringValue()"
|
|
1229
1369
|
[placeholder]="field().placeholder || ''"
|
|
1230
1370
|
[disabled]="controlDisabled()"
|
|
1231
|
-
[attr.step]="field().type === 'currency' ? '0.01' : null"
|
|
1232
|
-
[attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
|
|
1371
|
+
[attr.step]="field().type === 'currency' && !numericFormatted() ? '0.01' : null"
|
|
1372
|
+
[attr.inputmode]="field().type === 'currency' || numericFormatted() ? 'decimal' : null"
|
|
1233
1373
|
[attr.aria-required]="required() || null"
|
|
1234
1374
|
[attr.aria-invalid]="errors().length > 0 || null"
|
|
1235
1375
|
[attr.aria-describedby]="describedBy()"
|
|
1236
1376
|
(input)="setString($event)"
|
|
1237
|
-
(
|
|
1377
|
+
(focus)="onFocus()"
|
|
1378
|
+
(blur)="onBlur()"
|
|
1238
1379
|
/>
|
|
1239
1380
|
@if (suffix(); as s) {
|
|
1240
1381
|
<span class="mform-suffix" aria-hidden="true">{{ s }}</span>
|
|
@@ -1298,8 +1439,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1298
1439
|
</ul>
|
|
1299
1440
|
}
|
|
1300
1441
|
</ng-template>
|
|
1301
|
-
`,
|
|
1302
|
-
}]
|
|
1442
|
+
`, styles: [".mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-field.is-pending .mform-control{background-image:linear-gradient(90deg,transparent 0%,rgb(109 75 208 / 12%) 50%,transparent 100%);background-size:200% 100%;animation:mform-pending 1.2s linear infinite}@keyframes mform-pending{0%{background-position:200% 0}to{background-position:-200% 0}}.mform-files{list-style:none;margin:6px 0 0;padding:0;display:flex;flex-direction:column;gap:4px}.mform-files.is-gallery{flex-direction:row;flex-wrap:wrap;gap:10px}.mform-file{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--mform-muted, #6b6580)}.mform-files.is-gallery .mform-file{flex-direction:column;gap:4px;width:var(--mform-thumb-size, 96px);text-align:center;position:relative}.mform-thumb{width:var(--mform-thumb-size, 96px);height:var(--mform-thumb-size, 96px);object-fit:cover;border-radius:var(--mform-control-radius, 8px);border:1px solid var(--mform-border, #d9d5e6);background:var(--mform-bg, #f6f5fa)}.mform-file-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}.mform-file-remove{border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer;font-size:12px;padding:0 4px}.mform-file-remove:hover{color:var(--mform-danger, #c62842)}.mform-files.is-gallery .mform-file-remove{position:absolute;top:2px;right:2px;background:var(--mform-surface, #fff);border-radius:50%;width:20px;height:20px;line-height:1;box-shadow:0 1px 4px #1f1b2e33}.mform-reference{position:relative}.mform-ref-list{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;margin:0;padding:4px;list-style:none;background:var(--mform-control-bg, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);box-shadow:0 8px 24px #1f1b2e1f;max-height:220px;overflow-y:auto}.mform-ref-list button{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:7px 10px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.mform-ref-list button:hover{background:var(--mform-bg, #f6f5fa)}.mform-accordion{display:flex;flex-direction:column;border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-acc-item+.mform-acc-item{border-top:1px solid var(--mform-border, #d9d5e6)}.mform-acc-head{margin:0}.mform-acc-toggle{display:flex;align-items:center;gap:8px;width:100%;font:inherit;font-size:14px;font-weight:600;text-align:left;padding:12px 16px;border:none;background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);cursor:pointer}.mform-acc-caret{font-size:11px;color:var(--mform-muted, #6b6580)}.mform-acc-body{padding:var(--mform-panel-padding, 16px);display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}\n"] }]
|
|
1303
1443
|
}], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], tick: [{ type: i0.Input, args: [{ isSignal: true, alias: "tick", required: true }] }], scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: false }] }], bare: [{ type: i0.Input, args: [{ isSignal: true, alias: "bare", required: false }] }], providers: [{ type: i0.Input, args: [{ isSignal: true, alias: "providers", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], notify: [{ type: i0.Input, args: [{ isSignal: true, alias: "notify", required: false }] }] } });
|
|
1304
1444
|
|
|
1305
1445
|
/**
|
|
@@ -1427,19 +1567,29 @@ class FormRendererComponent {
|
|
|
1427
1567
|
this.errorSummary.set([]);
|
|
1428
1568
|
this.currentStep.update((step) => Math.max(0, step - 1));
|
|
1429
1569
|
}
|
|
1430
|
-
|
|
1570
|
+
/**
|
|
1571
|
+
* Advances only when the step is valid INCLUDING async rules, so a
|
|
1572
|
+
* server-side check can still block the transition.
|
|
1573
|
+
*/
|
|
1574
|
+
async nextStep() {
|
|
1431
1575
|
const engine = this.engine();
|
|
1432
1576
|
const step = this.steps()[this.currentStep()];
|
|
1433
|
-
if (!engine || !step)
|
|
1434
|
-
return;
|
|
1435
|
-
const errors = engine.validateContainer(step.key);
|
|
1436
|
-
if (errors.length > 0) {
|
|
1437
|
-
this.errorSummary.set(errors);
|
|
1438
|
-
this.focusFirstError(errors);
|
|
1577
|
+
if (!engine || !step || this.submitting())
|
|
1439
1578
|
return;
|
|
1579
|
+
this.submitting.set(true);
|
|
1580
|
+
try {
|
|
1581
|
+
const errors = await engine.validateContainerAsync(step.key);
|
|
1582
|
+
if (errors.length > 0) {
|
|
1583
|
+
this.errorSummary.set(errors);
|
|
1584
|
+
this.focusFirstError(errors);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
this.errorSummary.set([]);
|
|
1588
|
+
this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
|
|
1589
|
+
}
|
|
1590
|
+
finally {
|
|
1591
|
+
this.submitting.set(false);
|
|
1440
1592
|
}
|
|
1441
|
-
this.errorSummary.set([]);
|
|
1442
|
-
this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
|
|
1443
1593
|
}
|
|
1444
1594
|
// -- submission -----------------------------------------------------------
|
|
1445
1595
|
async onSubmit(event) {
|
|
@@ -1536,8 +1686,8 @@ class FormRendererComponent {
|
|
|
1536
1686
|
</button>
|
|
1537
1687
|
}
|
|
1538
1688
|
@if (currentStep() < steps().length - 1) {
|
|
1539
|
-
<button type="button" class="mform-btn" (click)="nextStep()">
|
|
1540
|
-
{{ labels().next }}
|
|
1689
|
+
<button type="button" class="mform-btn" [disabled]="submitting()" (click)="nextStep()">
|
|
1690
|
+
{{ submitting() ? '…' : labels().next }}
|
|
1541
1691
|
</button>
|
|
1542
1692
|
} @else if (mode() === 'edit') {
|
|
1543
1693
|
<button type="submit" class="mform-btn" [disabled]="submitting()">
|
|
@@ -1555,7 +1705,7 @@ class FormRendererComponent {
|
|
|
1555
1705
|
}
|
|
1556
1706
|
</form>
|
|
1557
1707
|
}
|
|
1558
|
-
`, isInline: true, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-
|
|
1708
|
+
`, isInline: true, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"], dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1559
1709
|
}
|
|
1560
1710
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, decorators: [{
|
|
1561
1711
|
type: Component,
|
|
@@ -1603,8 +1753,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1603
1753
|
</button>
|
|
1604
1754
|
}
|
|
1605
1755
|
@if (currentStep() < steps().length - 1) {
|
|
1606
|
-
<button type="button" class="mform-btn" (click)="nextStep()">
|
|
1607
|
-
{{ labels().next }}
|
|
1756
|
+
<button type="button" class="mform-btn" [disabled]="submitting()" (click)="nextStep()">
|
|
1757
|
+
{{ submitting() ? '…' : labels().next }}
|
|
1608
1758
|
</button>
|
|
1609
1759
|
} @else if (mode() === 'edit') {
|
|
1610
1760
|
<button type="submit" class="mform-btn" [disabled]="submitting()">
|
|
@@ -1622,7 +1772,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1622
1772
|
}
|
|
1623
1773
|
</form>
|
|
1624
1774
|
}
|
|
1625
|
-
`, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-
|
|
1775
|
+
`, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"] }]
|
|
1626
1776
|
}], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], initialData: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialData", required: false }] }], optionsProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionsProviders", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], validators: [{ type: i0.Input, args: [{ isSignal: true, alias: "validators", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], submitHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitHandler", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }] } });
|
|
1627
1777
|
|
|
1628
1778
|
/*
|