@mosaicoo/form-angular 0.5.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 +28 -0
- package/fesm2022/mosaicoo-form-angular-designer.mjs +836 -102
- package/fesm2022/mosaicoo-form-angular-designer.mjs.map +1 -1
- package/fesm2022/mosaicoo-form-angular-material.mjs +2 -0
- package/fesm2022/mosaicoo-form-angular-material.mjs.map +1 -1
- package/fesm2022/mosaicoo-form-angular.mjs +384 -91
- package/fesm2022/mosaicoo-form-angular.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mosaicoo-form-angular-designer.d.ts +104 -3
- package/types/mosaicoo-form-angular.d.ts +37 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { NgComponentOutlet, DOCUMENT } from '@angular/common';
|
|
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');
|
|
@@ -40,6 +40,13 @@ function provideMosaicooForm(config) {
|
|
|
40
40
|
return makeEnvironmentProviders(providers);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function formatBytes(bytes) {
|
|
44
|
+
if (bytes >= 1024 * 1024)
|
|
45
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
46
|
+
if (bytes >= 1024)
|
|
47
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
48
|
+
return `${bytes} B`;
|
|
49
|
+
}
|
|
43
50
|
/**
|
|
44
51
|
* Renders one schema node (input, container or static) and recurses into
|
|
45
52
|
* children. State lives in the headless engine — this component only projects
|
|
@@ -123,6 +130,8 @@ class FormNodeComponent {
|
|
|
123
130
|
/** Discards resolutions that arrive after a newer request started. */
|
|
124
131
|
optionsRequestSeq = 0;
|
|
125
132
|
constructor() {
|
|
133
|
+
// Autocomplete fields refresh their cached results on observed changes.
|
|
134
|
+
effect(() => this.watchReferenceRefresh());
|
|
126
135
|
effect(() => {
|
|
127
136
|
const node = this.node();
|
|
128
137
|
const registry = this.providers();
|
|
@@ -237,10 +246,28 @@ class FormNodeComponent {
|
|
|
237
246
|
return 'array';
|
|
238
247
|
if (node.type === 'tabs')
|
|
239
248
|
return 'tabs';
|
|
249
|
+
if (node.type === 'accordion')
|
|
250
|
+
return 'accordion';
|
|
240
251
|
if (node.type === 'columns' && node.columns?.length)
|
|
241
252
|
return 'columns';
|
|
242
253
|
return 'block';
|
|
243
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
|
+
}
|
|
244
271
|
columns() {
|
|
245
272
|
const node = this.container();
|
|
246
273
|
return (node.columns ?? []).map((column) => column.children
|
|
@@ -261,11 +288,29 @@ class FormNodeComponent {
|
|
|
261
288
|
const value = this.value();
|
|
262
289
|
if (value === undefined || value === null)
|
|
263
290
|
return '';
|
|
264
|
-
const
|
|
265
|
-
if (mask && typeof value === 'string')
|
|
266
|
-
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);
|
|
267
298
|
return String(value);
|
|
268
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
|
+
}
|
|
269
314
|
mapValue() {
|
|
270
315
|
const value = this.value();
|
|
271
316
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -324,11 +369,16 @@ class FormNodeComponent {
|
|
|
324
369
|
setString(event) {
|
|
325
370
|
const target = event.target;
|
|
326
371
|
const raw = target.value;
|
|
327
|
-
const
|
|
328
|
-
if (mask) {
|
|
329
|
-
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);
|
|
330
375
|
target.value = masked; // the control always shows the masked shape
|
|
331
|
-
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));
|
|
332
382
|
return;
|
|
333
383
|
}
|
|
334
384
|
if (this.field().dataType === 'number') {
|
|
@@ -340,7 +390,19 @@ class FormNodeComponent {
|
|
|
340
390
|
}
|
|
341
391
|
}
|
|
342
392
|
setSelect(event) {
|
|
343
|
-
|
|
393
|
+
const select = event.target;
|
|
394
|
+
if (this.field().multiple) {
|
|
395
|
+
this.setValue(Array.from(select.selectedOptions, (option) => option.value));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.setValue(select.value);
|
|
399
|
+
}
|
|
400
|
+
/** Selection state for single and multi-value choice fields. */
|
|
401
|
+
isSelected(optionValue) {
|
|
402
|
+
const value = this.value();
|
|
403
|
+
if (Array.isArray(value))
|
|
404
|
+
return value.some((item) => `${item}` === `${optionValue}`);
|
|
405
|
+
return this.stringValue() === `${optionValue}`;
|
|
344
406
|
}
|
|
345
407
|
setChecked(event) {
|
|
346
408
|
this.setValue(event.target.checked);
|
|
@@ -349,19 +411,48 @@ class FormNodeComponent {
|
|
|
349
411
|
const checked = event.target.checked;
|
|
350
412
|
this.setValue({ ...this.mapValue(), [option]: checked });
|
|
351
413
|
}
|
|
414
|
+
/** Client-side rejection message (size/type), cleared on the next pick. */
|
|
415
|
+
fileRejection = signal(null, ...(ngDevMode ? [{ debugName: "fileRejection" }] : /* istanbul ignore next */ []));
|
|
416
|
+
acceptAttr() {
|
|
417
|
+
const accept = this.field().files?.accept;
|
|
418
|
+
if (accept)
|
|
419
|
+
return accept;
|
|
420
|
+
return this.field().type === 'image' ? 'image/*' : null;
|
|
421
|
+
}
|
|
352
422
|
setFiles(event) {
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
423
|
+
const input = event.target;
|
|
424
|
+
const picked = input.files ? Array.from(input.files) : [];
|
|
425
|
+
this.fileRejection.set(null);
|
|
426
|
+
if (picked.length === 0)
|
|
427
|
+
return;
|
|
428
|
+
// Client-side guards give immediate feedback; the back end stays the
|
|
429
|
+
// authoritative check (file COUNT is a real rule via maxLength).
|
|
430
|
+
const maxSize = this.field().files?.maxSize;
|
|
431
|
+
const tooBig = maxSize ? picked.filter((file) => file.size > maxSize) : [];
|
|
432
|
+
const list = maxSize ? picked.filter((file) => file.size <= maxSize) : picked;
|
|
433
|
+
if (tooBig.length > 0) {
|
|
434
|
+
this.fileRejection.set(`${tooBig.map((f) => f.name).join(', ')} — above ${formatBytes(maxSize)}`);
|
|
435
|
+
}
|
|
436
|
+
if (list.length === 0) {
|
|
437
|
+
input.value = '';
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const previous = this.fileEntries();
|
|
441
|
+
const append = this.field().multiple !== false;
|
|
442
|
+
if (!this.uploader) {
|
|
356
443
|
// no host uploader: only file names are stored
|
|
357
|
-
|
|
444
|
+
const names = list.map((f) => f.name);
|
|
445
|
+
this.setValue(append ? [...previous.map((e) => e.name), ...names] : names);
|
|
358
446
|
return;
|
|
359
447
|
}
|
|
360
448
|
const field = this.field();
|
|
361
449
|
const path = this.path();
|
|
362
450
|
this.uploading.set(true);
|
|
363
451
|
Promise.all(list.map((file) => this.uploader(file, { field, path })))
|
|
364
|
-
.then((refs) =>
|
|
452
|
+
.then((refs) => {
|
|
453
|
+
const current = append ? this.value() ?? [] : [];
|
|
454
|
+
this.setValue([...current, ...refs]);
|
|
455
|
+
})
|
|
365
456
|
.catch(() => { }) // host upload failure keeps the previous value
|
|
366
457
|
.finally(() => {
|
|
367
458
|
this.uploading.set(false);
|
|
@@ -369,14 +460,37 @@ class FormNodeComponent {
|
|
|
369
460
|
this.notify()();
|
|
370
461
|
});
|
|
371
462
|
}
|
|
372
|
-
/**
|
|
373
|
-
|
|
463
|
+
/** Normalized view of the stored value: name + optional preview url. */
|
|
464
|
+
fileEntries() {
|
|
374
465
|
const value = this.value();
|
|
375
466
|
if (!Array.isArray(value))
|
|
376
467
|
return [];
|
|
377
|
-
return value.map((item) =>
|
|
378
|
-
|
|
379
|
-
|
|
468
|
+
return value.map((item) => {
|
|
469
|
+
if (typeof item === 'object' && item !== null) {
|
|
470
|
+
const ref = item;
|
|
471
|
+
return {
|
|
472
|
+
name: String(ref.name ?? ''),
|
|
473
|
+
url: typeof ref.url === 'string' ? ref.url : null,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const name = String(item);
|
|
477
|
+
// A bare string that already looks like a URL doubles as the preview.
|
|
478
|
+
return { name, url: /^(https?:|data:image\/|blob:)/.test(name) ? name : null };
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
showThumbnails() {
|
|
482
|
+
const field = this.field();
|
|
483
|
+
if (field.files?.preview === false)
|
|
484
|
+
return false;
|
|
485
|
+
return field.type === 'image' || field.files?.preview === true;
|
|
486
|
+
}
|
|
487
|
+
removeFile(index) {
|
|
488
|
+
const value = this.value();
|
|
489
|
+
if (!Array.isArray(value))
|
|
490
|
+
return;
|
|
491
|
+
const next = [...value];
|
|
492
|
+
next.splice(index, 1);
|
|
493
|
+
this.setValue(next);
|
|
380
494
|
}
|
|
381
495
|
prefix() {
|
|
382
496
|
const value = this.node().props?.['prefix'];
|
|
@@ -416,11 +530,41 @@ class FormNodeComponent {
|
|
|
416
530
|
refTimer = null;
|
|
417
531
|
/** Label of the chosen option (falls back to the raw value). */
|
|
418
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
|
+
}
|
|
419
562
|
refDisplay() {
|
|
420
563
|
return this.refLabel() ?? this.stringValue();
|
|
421
564
|
}
|
|
422
565
|
refSearch(event) {
|
|
423
566
|
const query = event.target.value;
|
|
567
|
+
this.refQuery = query;
|
|
424
568
|
this.refLabel.set(null);
|
|
425
569
|
if (this.refTimer)
|
|
426
570
|
clearTimeout(this.refTimer);
|
|
@@ -512,6 +656,42 @@ class FormNodeComponent {
|
|
|
512
656
|
}
|
|
513
657
|
</div>
|
|
514
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
|
+
}
|
|
515
695
|
@case ('tabs') {
|
|
516
696
|
<div class="mform-tabs">
|
|
517
697
|
<div class="mform-tabbar" role="tablist">
|
|
@@ -625,17 +805,21 @@ class FormNodeComponent {
|
|
|
625
805
|
class="mform-control"
|
|
626
806
|
[id]="controlId()"
|
|
627
807
|
[disabled]="controlDisabled()"
|
|
808
|
+
[multiple]="field().multiple === true"
|
|
809
|
+
[attr.size]="field().multiple ? 5 : null"
|
|
628
810
|
[attr.aria-required]="required() || null"
|
|
629
811
|
[attr.aria-invalid]="errors().length > 0 || null"
|
|
630
812
|
[attr.aria-describedby]="describedBy()"
|
|
631
813
|
(change)="setSelect($event)"
|
|
632
814
|
(blur)="touch()"
|
|
633
815
|
>
|
|
634
|
-
|
|
816
|
+
@if (!field().multiple) {
|
|
817
|
+
<option value="" [selected]="stringValue() === ''"></option>
|
|
818
|
+
}
|
|
635
819
|
@for (option of options(); track option.value) {
|
|
636
820
|
<option
|
|
637
821
|
[value]="option.value"
|
|
638
|
-
[selected]="
|
|
822
|
+
[selected]="isSelected(option.value)"
|
|
639
823
|
>
|
|
640
824
|
{{ option.label }}
|
|
641
825
|
</option>
|
|
@@ -720,23 +904,10 @@ class FormNodeComponent {
|
|
|
720
904
|
</div>
|
|
721
905
|
}
|
|
722
906
|
@case ('file') {
|
|
723
|
-
<
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
[id]="controlId()"
|
|
728
|
-
[disabled]="controlDisabled() || uploading()"
|
|
729
|
-
[attr.aria-busy]="uploading() || null"
|
|
730
|
-
[attr.aria-describedby]="describedBy()"
|
|
731
|
-
(change)="setFiles($event)"
|
|
732
|
-
(blur)="touch()"
|
|
733
|
-
/>
|
|
734
|
-
@if (uploading()) {
|
|
735
|
-
<div class="mform-desc" role="status">Uploading…</div>
|
|
736
|
-
}
|
|
737
|
-
@for (name of fileNames(); track $index) {
|
|
738
|
-
<div class="mform-file-name">📎 {{ name }}</div>
|
|
739
|
-
}
|
|
907
|
+
<ng-container *ngTemplateOutlet="fileControl" />
|
|
908
|
+
}
|
|
909
|
+
@case ('image') {
|
|
910
|
+
<ng-container *ngTemplateOutlet="fileControl" />
|
|
740
911
|
}
|
|
741
912
|
@case ('reference') {
|
|
742
913
|
<div class="mform-reference">
|
|
@@ -781,18 +952,19 @@ class FormNodeComponent {
|
|
|
781
952
|
}
|
|
782
953
|
<input
|
|
783
954
|
class="mform-control"
|
|
784
|
-
[type]="inputType()"
|
|
955
|
+
[type]="numericFormatted() ? 'text' : inputType()"
|
|
785
956
|
[id]="controlId()"
|
|
786
957
|
[value]="stringValue()"
|
|
787
958
|
[placeholder]="field().placeholder || ''"
|
|
788
959
|
[disabled]="controlDisabled()"
|
|
789
|
-
[attr.step]="field().type === 'currency' ? '0.01' : null"
|
|
790
|
-
[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"
|
|
791
962
|
[attr.aria-required]="required() || null"
|
|
792
963
|
[attr.aria-invalid]="errors().length > 0 || null"
|
|
793
964
|
[attr.aria-describedby]="describedBy()"
|
|
794
965
|
(input)="setString($event)"
|
|
795
|
-
(
|
|
966
|
+
(focus)="onFocus()"
|
|
967
|
+
(blur)="onBlur()"
|
|
796
968
|
/>
|
|
797
969
|
@if (suffix(); as s) {
|
|
798
970
|
<span class="mform-suffix" aria-hidden="true">{{ s }}</span>
|
|
@@ -811,17 +983,56 @@ class FormNodeComponent {
|
|
|
811
983
|
}
|
|
812
984
|
}
|
|
813
985
|
}
|
|
814
|
-
|
|
986
|
+
|
|
987
|
+
<!-- File/image control: same markup for both types, thumbnails when the
|
|
988
|
+
stored reference points at an image. -->
|
|
989
|
+
<ng-template #fileControl>
|
|
990
|
+
<input
|
|
991
|
+
class="mform-control"
|
|
992
|
+
type="file"
|
|
993
|
+
[id]="controlId()"
|
|
994
|
+
[multiple]="field().multiple !== false"
|
|
995
|
+
[attr.accept]="acceptAttr()"
|
|
996
|
+
[disabled]="controlDisabled() || uploading()"
|
|
997
|
+
[attr.aria-busy]="uploading() || null"
|
|
998
|
+
[attr.aria-describedby]="describedBy()"
|
|
999
|
+
(change)="setFiles($event)"
|
|
1000
|
+
(blur)="touch()"
|
|
1001
|
+
/>
|
|
1002
|
+
@if (uploading()) {
|
|
1003
|
+
<div class="mform-desc" role="status">Uploading…</div>
|
|
1004
|
+
}
|
|
1005
|
+
@if (fileRejection(); as rejection) {
|
|
1006
|
+
<div class="mform-error" role="alert">{{ rejection }}</div>
|
|
1007
|
+
}
|
|
1008
|
+
@if (fileEntries().length > 0) {
|
|
1009
|
+
<ul class="mform-files" [class.is-gallery]="showThumbnails()">
|
|
1010
|
+
@for (entry of fileEntries(); track $index; let i = $index) {
|
|
1011
|
+
<li class="mform-file">
|
|
1012
|
+
@if (showThumbnails() && entry.url) {
|
|
1013
|
+
<img class="mform-thumb" [src]="entry.url" [alt]="entry.name" loading="lazy" />
|
|
1014
|
+
} @else {
|
|
1015
|
+
<span class="mform-file-icon" aria-hidden="true">📎</span>
|
|
1016
|
+
}
|
|
1017
|
+
<span class="mform-file-name" [title]="entry.name">{{ entry.name }}</span>
|
|
1018
|
+
@if (interactive()) {
|
|
1019
|
+
<button
|
|
1020
|
+
type="button"
|
|
1021
|
+
class="mform-file-remove"
|
|
1022
|
+
(click)="removeFile(i)"
|
|
1023
|
+
[attr.aria-label]="'Remove ' + entry.name"
|
|
1024
|
+
>✕</button>
|
|
1025
|
+
}
|
|
1026
|
+
</li>
|
|
1027
|
+
}
|
|
1028
|
+
</ul>
|
|
1029
|
+
}
|
|
1030
|
+
</ng-template>
|
|
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 });
|
|
815
1032
|
}
|
|
816
1033
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, decorators: [{
|
|
817
1034
|
type: Component,
|
|
818
|
-
args: [{
|
|
819
|
-
selector: 'mform-node',
|
|
820
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
821
|
-
encapsulation: ViewEncapsulation.None,
|
|
822
|
-
host: { class: 'mform-node' },
|
|
823
|
-
imports: [NgComponentOutlet],
|
|
824
|
-
template: `
|
|
1035
|
+
args: [{ selector: 'mform-node', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'mform-node' }, imports: [NgComponentOutlet, NgTemplateOutlet], template: `
|
|
825
1036
|
@let n = node();
|
|
826
1037
|
@if (visible()) {
|
|
827
1038
|
@switch (n.kind) {
|
|
@@ -856,6 +1067,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
856
1067
|
}
|
|
857
1068
|
</div>
|
|
858
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
|
+
}
|
|
859
1106
|
@case ('tabs') {
|
|
860
1107
|
<div class="mform-tabs">
|
|
861
1108
|
<div class="mform-tabbar" role="tablist">
|
|
@@ -969,17 +1216,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
969
1216
|
class="mform-control"
|
|
970
1217
|
[id]="controlId()"
|
|
971
1218
|
[disabled]="controlDisabled()"
|
|
1219
|
+
[multiple]="field().multiple === true"
|
|
1220
|
+
[attr.size]="field().multiple ? 5 : null"
|
|
972
1221
|
[attr.aria-required]="required() || null"
|
|
973
1222
|
[attr.aria-invalid]="errors().length > 0 || null"
|
|
974
1223
|
[attr.aria-describedby]="describedBy()"
|
|
975
1224
|
(change)="setSelect($event)"
|
|
976
1225
|
(blur)="touch()"
|
|
977
1226
|
>
|
|
978
|
-
|
|
1227
|
+
@if (!field().multiple) {
|
|
1228
|
+
<option value="" [selected]="stringValue() === ''"></option>
|
|
1229
|
+
}
|
|
979
1230
|
@for (option of options(); track option.value) {
|
|
980
1231
|
<option
|
|
981
1232
|
[value]="option.value"
|
|
982
|
-
[selected]="
|
|
1233
|
+
[selected]="isSelected(option.value)"
|
|
983
1234
|
>
|
|
984
1235
|
{{ option.label }}
|
|
985
1236
|
</option>
|
|
@@ -1064,23 +1315,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1064
1315
|
</div>
|
|
1065
1316
|
}
|
|
1066
1317
|
@case ('file') {
|
|
1067
|
-
<
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
[id]="controlId()"
|
|
1072
|
-
[disabled]="controlDisabled() || uploading()"
|
|
1073
|
-
[attr.aria-busy]="uploading() || null"
|
|
1074
|
-
[attr.aria-describedby]="describedBy()"
|
|
1075
|
-
(change)="setFiles($event)"
|
|
1076
|
-
(blur)="touch()"
|
|
1077
|
-
/>
|
|
1078
|
-
@if (uploading()) {
|
|
1079
|
-
<div class="mform-desc" role="status">Uploading…</div>
|
|
1080
|
-
}
|
|
1081
|
-
@for (name of fileNames(); track $index) {
|
|
1082
|
-
<div class="mform-file-name">📎 {{ name }}</div>
|
|
1083
|
-
}
|
|
1318
|
+
<ng-container *ngTemplateOutlet="fileControl" />
|
|
1319
|
+
}
|
|
1320
|
+
@case ('image') {
|
|
1321
|
+
<ng-container *ngTemplateOutlet="fileControl" />
|
|
1084
1322
|
}
|
|
1085
1323
|
@case ('reference') {
|
|
1086
1324
|
<div class="mform-reference">
|
|
@@ -1125,18 +1363,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1125
1363
|
}
|
|
1126
1364
|
<input
|
|
1127
1365
|
class="mform-control"
|
|
1128
|
-
[type]="inputType()"
|
|
1366
|
+
[type]="numericFormatted() ? 'text' : inputType()"
|
|
1129
1367
|
[id]="controlId()"
|
|
1130
1368
|
[value]="stringValue()"
|
|
1131
1369
|
[placeholder]="field().placeholder || ''"
|
|
1132
1370
|
[disabled]="controlDisabled()"
|
|
1133
|
-
[attr.step]="field().type === 'currency' ? '0.01' : null"
|
|
1134
|
-
[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"
|
|
1135
1373
|
[attr.aria-required]="required() || null"
|
|
1136
1374
|
[attr.aria-invalid]="errors().length > 0 || null"
|
|
1137
1375
|
[attr.aria-describedby]="describedBy()"
|
|
1138
1376
|
(input)="setString($event)"
|
|
1139
|
-
(
|
|
1377
|
+
(focus)="onFocus()"
|
|
1378
|
+
(blur)="onBlur()"
|
|
1140
1379
|
/>
|
|
1141
1380
|
@if (suffix(); as s) {
|
|
1142
1381
|
<span class="mform-suffix" aria-hidden="true">{{ s }}</span>
|
|
@@ -1155,8 +1394,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1155
1394
|
}
|
|
1156
1395
|
}
|
|
1157
1396
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1397
|
+
|
|
1398
|
+
<!-- File/image control: same markup for both types, thumbnails when the
|
|
1399
|
+
stored reference points at an image. -->
|
|
1400
|
+
<ng-template #fileControl>
|
|
1401
|
+
<input
|
|
1402
|
+
class="mform-control"
|
|
1403
|
+
type="file"
|
|
1404
|
+
[id]="controlId()"
|
|
1405
|
+
[multiple]="field().multiple !== false"
|
|
1406
|
+
[attr.accept]="acceptAttr()"
|
|
1407
|
+
[disabled]="controlDisabled() || uploading()"
|
|
1408
|
+
[attr.aria-busy]="uploading() || null"
|
|
1409
|
+
[attr.aria-describedby]="describedBy()"
|
|
1410
|
+
(change)="setFiles($event)"
|
|
1411
|
+
(blur)="touch()"
|
|
1412
|
+
/>
|
|
1413
|
+
@if (uploading()) {
|
|
1414
|
+
<div class="mform-desc" role="status">Uploading…</div>
|
|
1415
|
+
}
|
|
1416
|
+
@if (fileRejection(); as rejection) {
|
|
1417
|
+
<div class="mform-error" role="alert">{{ rejection }}</div>
|
|
1418
|
+
}
|
|
1419
|
+
@if (fileEntries().length > 0) {
|
|
1420
|
+
<ul class="mform-files" [class.is-gallery]="showThumbnails()">
|
|
1421
|
+
@for (entry of fileEntries(); track $index; let i = $index) {
|
|
1422
|
+
<li class="mform-file">
|
|
1423
|
+
@if (showThumbnails() && entry.url) {
|
|
1424
|
+
<img class="mform-thumb" [src]="entry.url" [alt]="entry.name" loading="lazy" />
|
|
1425
|
+
} @else {
|
|
1426
|
+
<span class="mform-file-icon" aria-hidden="true">📎</span>
|
|
1427
|
+
}
|
|
1428
|
+
<span class="mform-file-name" [title]="entry.name">{{ entry.name }}</span>
|
|
1429
|
+
@if (interactive()) {
|
|
1430
|
+
<button
|
|
1431
|
+
type="button"
|
|
1432
|
+
class="mform-file-remove"
|
|
1433
|
+
(click)="removeFile(i)"
|
|
1434
|
+
[attr.aria-label]="'Remove ' + entry.name"
|
|
1435
|
+
>✕</button>
|
|
1436
|
+
}
|
|
1437
|
+
</li>
|
|
1438
|
+
}
|
|
1439
|
+
</ul>
|
|
1440
|
+
}
|
|
1441
|
+
</ng-template>
|
|
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"] }]
|
|
1160
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 }] }] } });
|
|
1161
1444
|
|
|
1162
1445
|
/**
|
|
@@ -1284,19 +1567,29 @@ class FormRendererComponent {
|
|
|
1284
1567
|
this.errorSummary.set([]);
|
|
1285
1568
|
this.currentStep.update((step) => Math.max(0, step - 1));
|
|
1286
1569
|
}
|
|
1287
|
-
|
|
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() {
|
|
1288
1575
|
const engine = this.engine();
|
|
1289
1576
|
const step = this.steps()[this.currentStep()];
|
|
1290
|
-
if (!engine || !step)
|
|
1291
|
-
return;
|
|
1292
|
-
const errors = engine.validateContainer(step.key);
|
|
1293
|
-
if (errors.length > 0) {
|
|
1294
|
-
this.errorSummary.set(errors);
|
|
1295
|
-
this.focusFirstError(errors);
|
|
1577
|
+
if (!engine || !step || this.submitting())
|
|
1296
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);
|
|
1297
1592
|
}
|
|
1298
|
-
this.errorSummary.set([]);
|
|
1299
|
-
this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
|
|
1300
1593
|
}
|
|
1301
1594
|
// -- submission -----------------------------------------------------------
|
|
1302
1595
|
async onSubmit(event) {
|
|
@@ -1393,8 +1686,8 @@ class FormRendererComponent {
|
|
|
1393
1686
|
</button>
|
|
1394
1687
|
}
|
|
1395
1688
|
@if (currentStep() < steps().length - 1) {
|
|
1396
|
-
<button type="button" class="mform-btn" (click)="nextStep()">
|
|
1397
|
-
{{ labels().next }}
|
|
1689
|
+
<button type="button" class="mform-btn" [disabled]="submitting()" (click)="nextStep()">
|
|
1690
|
+
{{ submitting() ? '…' : labels().next }}
|
|
1398
1691
|
</button>
|
|
1399
1692
|
} @else if (mode() === 'edit') {
|
|
1400
1693
|
<button type="submit" class="mform-btn" [disabled]="submitting()">
|
|
@@ -1412,7 +1705,7 @@ class FormRendererComponent {
|
|
|
1412
1705
|
}
|
|
1413
1706
|
</form>
|
|
1414
1707
|
}
|
|
1415
|
-
`, 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 });
|
|
1416
1709
|
}
|
|
1417
1710
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, decorators: [{
|
|
1418
1711
|
type: Component,
|
|
@@ -1460,8 +1753,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1460
1753
|
</button>
|
|
1461
1754
|
}
|
|
1462
1755
|
@if (currentStep() < steps().length - 1) {
|
|
1463
|
-
<button type="button" class="mform-btn" (click)="nextStep()">
|
|
1464
|
-
{{ labels().next }}
|
|
1756
|
+
<button type="button" class="mform-btn" [disabled]="submitting()" (click)="nextStep()">
|
|
1757
|
+
{{ submitting() ? '…' : labels().next }}
|
|
1465
1758
|
</button>
|
|
1466
1759
|
} @else if (mode() === 'edit') {
|
|
1467
1760
|
<button type="submit" class="mform-btn" [disabled]="submitting()">
|
|
@@ -1479,7 +1772,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1479
1772
|
}
|
|
1480
1773
|
</form>
|
|
1481
1774
|
}
|
|
1482
|
-
`, 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"] }]
|
|
1483
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"] }] } });
|
|
1484
1777
|
|
|
1485
1778
|
/*
|