@react-typed-forms/schemas 14.2.0 → 14.3.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/src/util.ts ADDED
@@ -0,0 +1,1039 @@
1
+ import {
2
+ ControlActionHandler,
3
+ ControlDataVisitor,
4
+ ControlDefinition,
5
+ ControlDefinitionType,
6
+ DataControlDefinition,
7
+ DataRenderType,
8
+ DisplayOnlyRenderOptions,
9
+ fieldPathForDefinition,
10
+ GroupRenderOptions,
11
+ isAutoCompleteClasses,
12
+ isCheckEntryClasses,
13
+ isDataControl,
14
+ isDataGroupRenderer,
15
+ isDisplayOnlyRenderer,
16
+ isGroupControl,
17
+ } from "./controlDefinition";
18
+ import { MutableRefObject, useRef } from "react";
19
+ import clsx from "clsx";
20
+ import {
21
+ CompoundField,
22
+ FieldOption,
23
+ findField,
24
+ getTagParam,
25
+ isCompoundField,
26
+ isCompoundNode,
27
+ isScalarField,
28
+ relativePath,
29
+ rootSchemaNode,
30
+ SchemaDataNode,
31
+ SchemaField,
32
+ schemaForFieldPath,
33
+ SchemaNode,
34
+ SchemaTags,
35
+ } from "./schemaField";
36
+ import {
37
+ Control,
38
+ ControlChange,
39
+ ensureMetaValue,
40
+ getElementIndex,
41
+ newControl,
42
+ } from "@react-typed-forms/core";
43
+ import { ActionRendererProps } from "./controlRender";
44
+
45
+ /**
46
+ * Interface representing the classes for a control.
47
+ */
48
+ export interface ControlClasses {
49
+ styleClass?: string;
50
+ layoutClass?: string;
51
+ labelClass?: string;
52
+ }
53
+
54
+ /**
55
+ * Type representing a JSON path, which can be a string or a number.
56
+ */
57
+ export type JsonPath = string | number;
58
+
59
+ /**
60
+ * Applies default values to the given record based on the provided schema fields.
61
+ * @param v - The record to apply default values to.
62
+ * @param fields - The schema fields to use for applying default values.
63
+ * @param doneSet - A set to keep track of processed records.
64
+ * @returns The record with default values applied.
65
+ */
66
+ export function applyDefaultValues(
67
+ v: Record<string, any> | undefined,
68
+ fields: SchemaField[],
69
+ doneSet?: Set<Record<string, any>>,
70
+ ): any {
71
+ if (!v) return defaultValueForFields(fields);
72
+ if (doneSet && doneSet.has(v)) return v;
73
+ doneSet ??= new Set();
74
+ doneSet.add(v);
75
+ const applyValue = fields.filter(
76
+ (x) => isCompoundField(x) || !(x.field in v),
77
+ );
78
+ if (!applyValue.length) return v;
79
+ const out = { ...v };
80
+ applyValue.forEach((x) => {
81
+ out[x.field] =
82
+ x.field in v
83
+ ? applyDefaultForField(v[x.field], x, fields, false, doneSet)
84
+ : defaultValueForField(x);
85
+ });
86
+ return out;
87
+ }
88
+
89
+ /**
90
+ * Applies default values to a specific field based on the provided schema field.
91
+ * @param v - The value to apply default values to.
92
+ * @param field - The schema field to use for applying default values.
93
+ * @param parent - The parent schema fields.
94
+ * @param notElement - Flag indicating if the field is not an element.
95
+ * @param doneSet - A set to keep track of processed records.
96
+ * @returns The value with default values applied.
97
+ */
98
+ export function applyDefaultForField(
99
+ v: any,
100
+ field: SchemaField,
101
+ parent: SchemaField[],
102
+ notElement?: boolean,
103
+ doneSet?: Set<Record<string, any>>,
104
+ ): any {
105
+ if (field.collection && !notElement) {
106
+ return ((v as any[]) ?? []).map((x) =>
107
+ applyDefaultForField(x, field, parent, true, doneSet),
108
+ );
109
+ }
110
+ if (isCompoundField(field)) {
111
+ if (!v && !field.required) return v;
112
+ return applyDefaultValues(
113
+ v,
114
+ field.treeChildren ? parent : field.children,
115
+ doneSet,
116
+ );
117
+ }
118
+ return defaultValueForField(field);
119
+ }
120
+
121
+ /**
122
+ * Returns the default values for the provided schema fields.
123
+ * @param fields - The schema fields to get default values for.
124
+ * @returns The default values for the schema fields.
125
+ */
126
+ export function defaultValueForFields(fields: SchemaField[]): any {
127
+ return Object.fromEntries(
128
+ fields.map((x) => [x.field, defaultValueForField(x)]),
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Returns the default value for a specific schema field.
134
+ * @param sf - The schema field to get the default value for.
135
+ * @param required - Flag indicating if the field is required.
136
+ * @returns The default value for the schema field.
137
+ */
138
+ export function defaultValueForField(
139
+ sf: SchemaField,
140
+ required?: boolean | null,
141
+ ): any {
142
+ if (sf.defaultValue !== undefined) return sf.defaultValue;
143
+ const isRequired = !!(required || sf.required);
144
+ if (isCompoundField(sf)) {
145
+ if (isRequired) {
146
+ const childValue = defaultValueForFields(sf.children);
147
+ return sf.collection ? [childValue] : childValue;
148
+ }
149
+ return sf.notNullable ? (sf.collection ? [] : {}) : undefined;
150
+ }
151
+ if (sf.collection && sf.notNullable) {
152
+ return [];
153
+ }
154
+ return undefined;
155
+ }
156
+
157
+ /**
158
+ * Returns the element value for a specific schema field.
159
+ * @param sf - The schema field to get the element value for.
160
+ * @returns The element value for the schema field.
161
+ */
162
+ export function elementValueForField(sf: SchemaField): any {
163
+ if (isCompoundField(sf)) {
164
+ return defaultValueForFields(sf.children);
165
+ }
166
+ return sf.defaultValue;
167
+ }
168
+
169
+ /**
170
+ * Finds a scalar field in the provided schema fields.
171
+ * @param fields - The schema fields to search in.
172
+ * @param field - The field name to search for.
173
+ * @returns The found scalar field, or undefined if not found.
174
+ */
175
+ export function findScalarField(
176
+ fields: SchemaField[],
177
+ field: string,
178
+ ): SchemaField | undefined {
179
+ return findField(fields, field);
180
+ }
181
+
182
+ /**
183
+ * Finds a compound field in the provided schema fields.
184
+ * @param fields - The schema fields to search in.
185
+ * @param field - The field name to search for.
186
+ * @returns The found compound field, or undefined if not found.
187
+ */
188
+ export function findCompoundField(
189
+ fields: SchemaField[],
190
+ field: string,
191
+ ): CompoundField | undefined {
192
+ return findField(fields, field) as CompoundField | undefined;
193
+ }
194
+
195
+ /**
196
+ * Checks if a field has a specific tag.
197
+ * @param field - The field to check.
198
+ * @param tag - The tag to check for.
199
+ * @returns True if the field has the tag, false otherwise.
200
+ */
201
+ export function fieldHasTag(field: SchemaField, tag: string) {
202
+ return Boolean(field.tags?.includes(tag));
203
+ }
204
+
205
+ /**
206
+ * Returns the display name for a specific field.
207
+ * @param field - The field to get the display name for.
208
+ * @returns The display name for the field.
209
+ */
210
+ export function fieldDisplayName(field: SchemaField) {
211
+ return field.displayName ?? field.field;
212
+ }
213
+
214
+ /**
215
+ * Checks if an object has options.
216
+ * @param o - The object to check.
217
+ * @returns True if the object has options, false otherwise.
218
+ */
219
+ export function hasOptions(o: { options: FieldOption[] | undefined | null }) {
220
+ return (o.options?.length ?? 0) > 0;
221
+ }
222
+
223
+ /**
224
+ * Returns the default control definition for a specific schema field.
225
+ * @param sf - The schema field to get the default control definition for.
226
+ * @param noChildren - Flag indicating if children should not be included.
227
+ * @returns The default control definition for the schema field.
228
+ */
229
+ export function defaultControlForField(
230
+ sf: SchemaField,
231
+ noChildren?: boolean,
232
+ ): DataControlDefinition {
233
+ if (isCompoundField(sf)) {
234
+ const ref = getTagParam(sf, SchemaTags.ControlRef);
235
+ return {
236
+ type: ControlDefinitionType.Data,
237
+ title: sf.displayName,
238
+ field: sf.field,
239
+ required: sf.required,
240
+ childRefId: ref,
241
+ children:
242
+ !noChildren && !ref
243
+ ? sf.children
244
+ .filter((x) => !fieldHasTag(x, SchemaTags.NoControl))
245
+ .map((x) => defaultControlForField(x))
246
+ : undefined,
247
+ };
248
+ } else if (isScalarField(sf)) {
249
+ const htmlEditor = fieldHasTag(sf, SchemaTags.HtmlEditor);
250
+ return {
251
+ type: ControlDefinitionType.Data,
252
+ title: sf.displayName,
253
+ field: sf.field,
254
+ required: sf.required,
255
+ renderOptions: {
256
+ type: htmlEditor ? DataRenderType.HtmlEditor : DataRenderType.Standard,
257
+ },
258
+ };
259
+ }
260
+ throw "Unknown schema field";
261
+ }
262
+
263
+ /**
264
+ * Finds a referenced control in the provided control definition.
265
+ * @param field - The field name to search for.
266
+ * @param control - The control definition to search in.
267
+ * @returns The found control definition, or undefined if not found.
268
+ */
269
+ function findReferencedControl(
270
+ field: string,
271
+ control: ControlDefinition,
272
+ ): ControlDefinition | undefined {
273
+ if (isDataControl(control) && field === control.field) return control;
274
+ if (isGroupControl(control)) {
275
+ if (control.compoundField)
276
+ return field === control.compoundField ? control : undefined;
277
+ return findReferencedControlInArray(field, control.children ?? []);
278
+ }
279
+ return undefined;
280
+ }
281
+
282
+ /**
283
+ * Finds a referenced control in an array of control definitions.
284
+ * @param field - The field name to search for.
285
+ * @param controls - The array of control definitions to search in.
286
+ * @returns The found control definition, or undefined if not found.
287
+ */
288
+ function findReferencedControlInArray(
289
+ field: string,
290
+ controls: ControlDefinition[],
291
+ ): ControlDefinition | undefined {
292
+ for (const c of controls) {
293
+ const ref = findReferencedControl(field, c);
294
+ if (ref) return ref;
295
+ }
296
+ return undefined;
297
+ }
298
+
299
+ export function findControlsForCompound(
300
+ compound: SchemaNode,
301
+ definition: ControlDefinition,
302
+ ): ControlDefinition[] {
303
+ if (isDataControl(definition) && compound.field.field === definition.field) {
304
+ return [definition];
305
+ }
306
+ if (isGroupControl(definition)) {
307
+ if (definition.compoundField === compound.field.field) return [definition];
308
+ return (
309
+ definition.children?.flatMap((d) =>
310
+ findControlsForCompound(compound, d),
311
+ ) ?? []
312
+ );
313
+ }
314
+ return [];
315
+ }
316
+
317
+ /**
318
+ * Finds non-data groups in the provided control definitions.
319
+ * @param controls - The control definitions to search in.
320
+ * @returns An array of found non-data groups.
321
+ */
322
+ export function findNonDataGroups(
323
+ controls: ControlDefinition[],
324
+ ): ControlDefinition[] {
325
+ return controls.flatMap((control) =>
326
+ isGroupControl(control) && !control.compoundField
327
+ ? [control, ...findNonDataGroups(control.children ?? [])]
328
+ : [],
329
+ );
330
+ }
331
+
332
+ /**
333
+ * Adds missing controls to the provided control definitions based on the schema fields.
334
+ * @param fields - The schema fields to use for adding missing controls.
335
+ * @param controls - The control definitions to add missing controls to.
336
+ * @returns The control definitions with missing controls added.
337
+ */
338
+ export function addMissingControls(
339
+ fields: SchemaField[],
340
+ controls: ControlDefinition[],
341
+ ) {
342
+ return addMissingControlsForSchema(rootSchemaNode(fields), controls);
343
+ }
344
+
345
+ interface ControlAndSchema {
346
+ control: ControlDefinition;
347
+ children: ControlAndSchema[];
348
+ schema?: SchemaNode;
349
+ parent?: ControlAndSchema;
350
+ }
351
+ /**
352
+ * Adds missing controls to the provided control definitions based on the schema fields.
353
+ * @param schema - The root schema node to use for adding missing controls.
354
+ * @param controls - The control definitions to add missing controls to.
355
+ * @returns The control definitions with missing controls added.
356
+ */
357
+ export function addMissingControlsForSchema(
358
+ schema: SchemaNode,
359
+ controls: ControlDefinition[],
360
+ ) {
361
+ const controlMap: { [k: string]: ControlAndSchema } = {};
362
+ const schemaControlMap: { [k: string]: ControlAndSchema[] } = {};
363
+ const rootControls = controls.map((c) => toControlAndSchema(c, schema));
364
+ const rootSchema = { schema, children: rootControls } as ControlAndSchema;
365
+ addSchemaMapEntry("", rootSchema);
366
+ rootControls.forEach(addReferences);
367
+ const fields = schema.getChildNodes();
368
+ fields.forEach(addMissing);
369
+ return rootControls.map(toDefinition);
370
+
371
+ function toDefinition(c: ControlAndSchema): ControlDefinition {
372
+ const children = c.children.length ? c.children.map(toDefinition) : null;
373
+ return { ...c.control, children };
374
+ }
375
+
376
+ function addMissing(schemaNode: SchemaNode) {
377
+ if (fieldHasTag(schemaNode.field, SchemaTags.NoControl)) return;
378
+ const existingControls = schemaControlMap[schemaNode.id];
379
+ if (!existingControls) {
380
+ const eligibleParents = getEligibleParents(schemaNode);
381
+ const desiredGroup = getTagParam(
382
+ schemaNode.field,
383
+ SchemaTags.ControlGroup,
384
+ );
385
+ let parentGroup = desiredGroup ? controlMap[desiredGroup] : undefined;
386
+ if (!parentGroup && desiredGroup)
387
+ console.warn("No group '" + desiredGroup + "' for " + schemaNode.id);
388
+ if (parentGroup && eligibleParents.indexOf(parentGroup.schema!.id) < 0) {
389
+ console.warn(
390
+ `Target group '${desiredGroup}' is not an eligible parent for '${schemaNode.id}'`,
391
+ );
392
+ parentGroup = undefined;
393
+ }
394
+ if (!parentGroup && eligibleParents.length) {
395
+ parentGroup = schemaControlMap[eligibleParents[0]]?.[0];
396
+ }
397
+ if (parentGroup) {
398
+ const newControl = defaultControlForField(schemaNode.field, true);
399
+ newControl.field = relativePath(parentGroup.schema!, schemaNode);
400
+ parentGroup.children.push(
401
+ toControlAndSchema(newControl, parentGroup.schema!, parentGroup),
402
+ );
403
+ } else
404
+ console.warn("Could not find a parent group for: " + schemaNode.id);
405
+ }
406
+ schemaNode.getChildNodes(true).forEach(addMissing);
407
+ }
408
+
409
+ function getEligibleParents(schemaNode: SchemaNode) {
410
+ const eligibleParents: string[] = [];
411
+ let parent = schemaNode.parent;
412
+ while (parent) {
413
+ eligibleParents.push(parent.id);
414
+ if (parent.field.collection) break;
415
+ if (!parent.parent) parent.getChildNodes(true).forEach(addCompound);
416
+ parent = parent.parent;
417
+ }
418
+ return eligibleParents;
419
+
420
+ function addCompound(node: SchemaNode) {
421
+ if (isCompoundNode(node) && !node.field.collection) {
422
+ eligibleParents.push(node.id);
423
+ node.getChildNodes(true).forEach(addCompound);
424
+ }
425
+ }
426
+ }
427
+
428
+ function addReferences(c: ControlAndSchema) {
429
+ c.children.forEach(addReferences);
430
+ if (c.control.childRefId) {
431
+ const ref = controlMap[c.control.childRefId];
432
+ if (ref) {
433
+ ref.children.forEach((x) =>
434
+ toControlAndSchema(x.control, c.schema!, c, true),
435
+ );
436
+ return;
437
+ }
438
+ console.warn("Missing reference", c.control.childRefId);
439
+ }
440
+ }
441
+
442
+ function addSchemaMapEntry(schemaId: string, entry: ControlAndSchema) {
443
+ if (!schemaControlMap[schemaId]) schemaControlMap[schemaId] = [];
444
+ schemaControlMap[schemaId].push(entry);
445
+ }
446
+ function toControlAndSchema(
447
+ c: ControlDefinition,
448
+ parentSchema: SchemaNode,
449
+ parentNode?: ControlAndSchema,
450
+ dontRegister?: boolean,
451
+ ): ControlAndSchema {
452
+ const controlPath = fieldPathForDefinition(c);
453
+ let dataSchema = controlPath
454
+ ? schemaForFieldPath(controlPath, parentSchema)
455
+ : undefined;
456
+ if (isGroupControl(c) && dataSchema == null) dataSchema = parentSchema;
457
+ const entry: ControlAndSchema = {
458
+ schema: dataSchema,
459
+ control: c,
460
+ children: [],
461
+ parent: parentNode,
462
+ };
463
+ entry.children =
464
+ c.children?.map((x) =>
465
+ toControlAndSchema(x, dataSchema ?? parentSchema, entry, dontRegister),
466
+ ) ?? [];
467
+ if (!dontRegister && c.id) controlMap[c.id] = entry;
468
+ if (dataSchema) {
469
+ addSchemaMapEntry(dataSchema.id, entry);
470
+ }
471
+ return entry;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Custom hook to use an updated reference.
477
+ * @param a - The value to create a reference for.
478
+ * @returns A mutable reference object.
479
+ */
480
+ export function useUpdatedRef<A>(a: A): MutableRefObject<A> {
481
+ const r = useRef(a);
482
+ r.current = a;
483
+ return r;
484
+ }
485
+
486
+ /**
487
+ * Checks if a control definition is readonly.
488
+ * @param c - The control definition to check.
489
+ * @returns True if the control definition is readonly, false otherwise.
490
+ */
491
+ export function isControlReadonly(c: ControlDefinition): boolean {
492
+ return isDataControl(c) && !!c.readonly;
493
+ }
494
+
495
+ /**
496
+ * Checks if a control definition is disabled.
497
+ * @param c - The control definition to check.
498
+ * @returns True if the control definition is disabled, false otherwise.
499
+ */
500
+ export function isControlDisabled(c: ControlDefinition): boolean {
501
+ return isDataControl(c) && !!c.disabled;
502
+ }
503
+
504
+ /**
505
+ * Returns the display-only render options for a control definition.
506
+ * @param d - The control definition to get the display-only render options for.
507
+ * @returns The display-only render options, or undefined if not applicable.
508
+ */
509
+ export function getDisplayOnlyOptions(
510
+ d: ControlDefinition,
511
+ ): DisplayOnlyRenderOptions | undefined {
512
+ return isDataControl(d) &&
513
+ d.renderOptions &&
514
+ isDisplayOnlyRenderer(d.renderOptions)
515
+ ? d.renderOptions
516
+ : undefined;
517
+ }
518
+
519
+ /**
520
+ * Cleans data for a schema based on the provided schema fields.
521
+ * @param v - The data to clean.
522
+ * @param fields - The schema fields to use for cleaning the data.
523
+ * @param removeIfDefault - Flag indicating if default values should be removed.
524
+ * @returns The cleaned data.
525
+ */
526
+ export function cleanDataForSchema(
527
+ v: { [k: string]: any } | undefined,
528
+ fields: SchemaField[],
529
+ removeIfDefault?: boolean,
530
+ ): any {
531
+ if (!v) return v;
532
+ const typeField = fields.find((x) => x.isTypeField);
533
+ const typeValue = typeField ? v[typeField.field] : undefined;
534
+ const cleanableFields = !removeIfDefault
535
+ ? fields.filter(
536
+ (x) => isCompoundField(x) || (x.onlyForTypes?.length ?? 0) > 0,
537
+ )
538
+ : fields;
539
+ if (!cleanableFields.length) return v;
540
+ const out = { ...v };
541
+ cleanableFields.forEach((x) => {
542
+ const childValue = v[x.field];
543
+ if (
544
+ x.onlyForTypes?.includes(typeValue) === false ||
545
+ (!x.notNullable && canBeNull())
546
+ ) {
547
+ delete out[x.field];
548
+ return;
549
+ }
550
+ if (isCompoundField(x)) {
551
+ const childFields = x.treeChildren ? fields : x.children;
552
+ if (x.collection) {
553
+ if (Array.isArray(childValue)) {
554
+ out[x.field] = childValue.map((cv) =>
555
+ cleanDataForSchema(cv, childFields, removeIfDefault),
556
+ );
557
+ }
558
+ } else {
559
+ out[x.field] = cleanDataForSchema(
560
+ childValue,
561
+ childFields,
562
+ removeIfDefault,
563
+ );
564
+ }
565
+ }
566
+ function canBeNull() {
567
+ return (
568
+ (removeIfDefault && x.defaultValue === childValue) ||
569
+ (x.collection && Array.isArray(childValue) && !childValue.length)
570
+ //|| (x.type === FieldType.Bool && childValue === false)
571
+ );
572
+ }
573
+ });
574
+ return out;
575
+ }
576
+
577
+ /**
578
+ * Returns all referenced classes for a control definition.
579
+ * @param c - The control definition to get the referenced classes for.
580
+ * @param collectExtra - Optional function to collect extra classes.
581
+ * @returns An array of referenced classes.
582
+ */
583
+ export function getAllReferencedClasses(
584
+ c: ControlDefinition,
585
+ collectExtra?: (c: ControlDefinition) => (string | undefined | null)[],
586
+ ): string[] {
587
+ const childClasses = c.children?.flatMap((x) =>
588
+ getAllReferencedClasses(x, collectExtra),
589
+ );
590
+ const go = getGroupClassOverrides(c);
591
+
592
+ const { entryWrapperClass, selectedClass, notSelectedClass } =
593
+ isDataControl(c) && isCheckEntryClasses(c.renderOptions)
594
+ ? c.renderOptions
595
+ : {};
596
+
597
+ const {
598
+ listContainerClass,
599
+ listEntryClass,
600
+ chipContainerClass,
601
+ chipCloseButtonClass,
602
+ } =
603
+ isDataControl(c) && isAutoCompleteClasses(c.renderOptions)
604
+ ? c.renderOptions
605
+ : {};
606
+
607
+ const tc = clsx(
608
+ [
609
+ c.styleClass,
610
+ c.layoutClass,
611
+ c.labelClass,
612
+ ...Object.values(go),
613
+ ...(collectExtra?.(c) ?? []),
614
+ entryWrapperClass,
615
+ selectedClass,
616
+ notSelectedClass,
617
+ listContainerClass,
618
+ listEntryClass,
619
+ chipContainerClass,
620
+ chipCloseButtonClass,
621
+ ].map(getOverrideClass),
622
+ );
623
+ if (childClasses && !tc) return childClasses;
624
+ if (!tc) return [];
625
+ if (childClasses) return [tc, ...childClasses];
626
+ return [tc];
627
+ }
628
+
629
+ /**
630
+ * Converts a JSON path array to a string.
631
+ * @param jsonPath - The JSON path array to convert.
632
+ * @param customIndex - Optional function to customize the index format.
633
+ * @returns The JSON path string.
634
+ */
635
+ export function jsonPathString(
636
+ jsonPath: JsonPath[],
637
+ customIndex?: (n: number) => string,
638
+ ) {
639
+ let out = "";
640
+ jsonPath.forEach((v, i) => {
641
+ if (typeof v === "number") {
642
+ out += customIndex?.(v) ?? "[" + v + "]";
643
+ } else {
644
+ if (i > 0) out += ".";
645
+ out += v;
646
+ }
647
+ });
648
+ return out;
649
+ }
650
+
651
+ /**
652
+ * Finds a child control definition within a parent control definition.
653
+ * @param parent - The parent control definition.
654
+ * @param childPath - The path to the child control definition, either as a single index or an array of indices.
655
+ * @returns The found child control definition.
656
+ */
657
+ export function findChildDefinition(
658
+ parent: ControlDefinition,
659
+ childPath: number | number[],
660
+ ): ControlDefinition {
661
+ if (Array.isArray(childPath)) {
662
+ let base = parent;
663
+ childPath.forEach((x) => (base = base.children![x]));
664
+ return base;
665
+ }
666
+ return parent.children![childPath];
667
+ }
668
+
669
+ /**
670
+ * Returns the override class name if the class name starts with "@ ".
671
+ * Otherwise, returns the original class name.
672
+ * @param className - The class name to check and potentially modify.
673
+ * @returns The override class name or the original class name.
674
+ */
675
+ export function getOverrideClass(className?: string | null) {
676
+ if (className && className.startsWith("@ ")) {
677
+ return className.substring(2);
678
+ }
679
+ return className;
680
+ }
681
+
682
+ /**
683
+ * Returns the appropriate class name for a renderer.
684
+ * If the global class name starts with "@ ", it overrides the control class name.
685
+ * Otherwise, it combines the control class name and the global class name.
686
+ *
687
+ * @param controlClass - The class name for the control.
688
+ * @param globalClass - The global class name.
689
+ * @returns The appropriate class name for the renderer.
690
+ */
691
+ export function rendererClass(
692
+ controlClass?: string | null,
693
+ globalClass?: string | null,
694
+ ) {
695
+ const gc = getOverrideClass(globalClass);
696
+ if (gc !== globalClass) return globalClass ? globalClass : undefined;
697
+ const oc = getOverrideClass(controlClass);
698
+ if (oc === controlClass) return clsx(controlClass, globalClass);
699
+ return oc ? oc : undefined;
700
+ }
701
+
702
+ /**
703
+ * Applies length restrictions to a value.
704
+ * @template Min - The type of the minimum value.
705
+ * @template Max - The type of the maximum value.
706
+ * @param {number} length - The length to check.
707
+ * @param {number | null | undefined} min - The minimum length.
708
+ * @param {number | null | undefined} max - The maximum length.
709
+ * @param {Min} minValue - The value to return if the length is greater than the minimum.
710
+ * @param {Max} maxValue - The value to return if the length is less than the maximum.
711
+ * @returns {[Min | undefined, Max | undefined]} - An array containing the minimum and maximum values if the length restrictions are met.
712
+ */
713
+ export function applyLengthRestrictions<Min, Max>(
714
+ length: number,
715
+ min: number | null | undefined,
716
+ max: number | null | undefined,
717
+ minValue: Min,
718
+ maxValue: Max,
719
+ ): [Min | undefined, Max | undefined] {
720
+ return [
721
+ min == null || length > min ? minValue : undefined,
722
+ max == null || length < max ? maxValue : undefined,
723
+ ];
724
+ }
725
+
726
+ /**
727
+ * Finds the path to a field in the schema fields.
728
+ * @param {SchemaField[]} fields - The schema fields to search in.
729
+ * @param {string | undefined} fieldPath - The path to the field.
730
+ * @returns {SchemaField[] | undefined} - An array of schema fields representing the path, or undefined if not found.
731
+ */
732
+ export function findFieldPath(
733
+ fields: SchemaField[],
734
+ fieldPath: string | undefined,
735
+ ): SchemaField[] | undefined {
736
+ if (!fieldPath) return undefined;
737
+ const fieldNames = fieldPath.split("/");
738
+ const foundFields: SchemaField[] = [];
739
+ let i = 0;
740
+ let currentFields: SchemaField[] | undefined = fields;
741
+ while (i < fieldNames.length && currentFields) {
742
+ const cf = fieldNames[i];
743
+ const nextField = findField(currentFields, cf);
744
+ if (!nextField) return undefined;
745
+ foundFields.push(nextField);
746
+ currentFields =
747
+ isCompoundField(nextField) && !nextField.collection
748
+ ? nextField.children
749
+ : undefined;
750
+ i++;
751
+ }
752
+ return foundFields.length === fieldNames.length ? foundFields : undefined;
753
+ }
754
+
755
+ /**
756
+ * Merges two objects.
757
+ * @template A - The type of the objects to merge.
758
+ * @param {A} o1 - The first object.
759
+ * @param {A} o2 - The second object.
760
+ * @param {(k: keyof NonNullable<A>, v1: unknown, v2: unknown) => unknown} [doMerge] - Optional function to merge values.
761
+ * @returns {A} - The merged object.
762
+ */
763
+ export function mergeObjects<A extends Record<string, any> | undefined>(
764
+ o1: A,
765
+ o2: A,
766
+ doMerge: (k: keyof NonNullable<A>, v1: unknown, v2: unknown) => unknown = (
767
+ _,
768
+ v1,
769
+ v2,
770
+ ) => v1 ?? v2,
771
+ ): A {
772
+ if (!o1) return o2;
773
+ if (!o2) return o1;
774
+ const result = { ...o1 };
775
+ for (const key in o2) {
776
+ if (o2.hasOwnProperty(key)) {
777
+ const value1 = o1[key];
778
+ const value2 = o2[key];
779
+ result[key] = doMerge(key, value1, value2) as any;
780
+ }
781
+ }
782
+ return result;
783
+ }
784
+
785
+ /**
786
+ * Coerces a value to a string.
787
+ * @param {unknown} v - The value to coerce.
788
+ * @returns {string} - The coerced string.
789
+ */
790
+ export function coerceToString(v: unknown) {
791
+ return v == null
792
+ ? ""
793
+ : typeof v === "object"
794
+ ? "error: " + JSON.stringify(v)
795
+ : v.toString();
796
+ }
797
+
798
+ /**
799
+ * Returns the group renderer options for a control definition.
800
+ * @param {ControlDefinition} def - The control definition to get the group renderer options for.
801
+ * @returns {GroupRenderOptions | undefined} - The group renderer options, or undefined if not applicable.
802
+ */
803
+ export function getGroupRendererOptions(
804
+ def: ControlDefinition,
805
+ ): GroupRenderOptions | undefined {
806
+ return isGroupControl(def)
807
+ ? def.groupOptions
808
+ : isDataControl(def) && isDataGroupRenderer(def.renderOptions)
809
+ ? def.renderOptions.groupOptions
810
+ : undefined;
811
+ }
812
+
813
+ /**
814
+ * Returns the group class overrides for a control definition.
815
+ * @param {ControlDefinition} def - The control definition to get the group class overrides for.
816
+ * @returns {ControlClasses} - The group class overrides.
817
+ */
818
+ export function getGroupClassOverrides(def: ControlDefinition): ControlClasses {
819
+ let go = getGroupRendererOptions(def);
820
+
821
+ if (!go) return {};
822
+ const { childLayoutClass, childStyleClass, childLabelClass } = go;
823
+ const out: ControlClasses = {};
824
+ if (childLayoutClass) out.layoutClass = childLayoutClass;
825
+ if (childStyleClass) out.styleClass = childStyleClass;
826
+ if (childLabelClass) out.labelClass = childLabelClass;
827
+ return out;
828
+ }
829
+
830
+ /**
831
+ * Checks if a control definition is display-only.
832
+ * @param {ControlDefinition} def - The control definition to check.
833
+ * @returns {boolean} - True if the control definition is display-only, false otherwise.
834
+ */
835
+ export function isControlDisplayOnly(def: ControlDefinition): boolean {
836
+ return Boolean(getGroupRendererOptions(def)?.displayOnly);
837
+ }
838
+
839
+ /**
840
+ * Combines multiple action handlers into a single handler.
841
+ * @param {...(ControlActionHandler | undefined)[]} handlers - The action handlers to combine.
842
+ * @returns {ControlActionHandler} - The combined action handler.
843
+ */
844
+ export function actionHandlers(
845
+ ...handlers: (ControlActionHandler | undefined)[]
846
+ ): ControlActionHandler {
847
+ return (actionId, actionData, ctx) => {
848
+ for (let i = 0; i < handlers.length; i++) {
849
+ const res = handlers[i]?.(actionId, actionData, ctx);
850
+ if (res) return res;
851
+ }
852
+ return undefined;
853
+ };
854
+ }
855
+
856
+ export function getDiffObject(dataNode: SchemaDataNode, force?: boolean): any {
857
+ const c = dataNode.control;
858
+ const sf = dataNode.schema.field;
859
+ if (!c.dirty && !force) return undefined;
860
+ if (c.isNull) return null;
861
+ if (sf.collection && dataNode.elementIndex == null) {
862
+ const idField = getTagParam(sf, SchemaTags.IdField);
863
+ return c.as<any[]>().elements.map((x, i) => {
864
+ const change = getDiffObject(
865
+ dataNode.getChildElement(i),
866
+ idField !== undefined,
867
+ );
868
+ return idField != null
869
+ ? change
870
+ : { old: getElementIndex(x)?.initialIndex, edit: change };
871
+ });
872
+ } else if (isCompoundField(sf)) {
873
+ const children = dataNode.schema.getChildNodes();
874
+ const idField = getTagParam(sf, SchemaTags.IdField);
875
+ return Object.fromEntries(
876
+ children.flatMap((c) => {
877
+ const diff = getDiffObject(
878
+ dataNode.getChild(c),
879
+ idField === c.field.field,
880
+ );
881
+ return diff !== undefined ? [[c.field.field, diff]] : [];
882
+ }),
883
+ );
884
+ }
885
+ return c.value;
886
+ }
887
+
888
+ export function getNullToggler(c: Control<any>): Control<boolean> {
889
+ return ensureMetaValue(c, "$nullToggler", () => {
890
+ const lastDefined = getLastDefinedValue(c);
891
+ const isEditing = getIsEditing(c);
892
+ const currentNotNull = c.current.value != null;
893
+ c.disabled = !currentNotNull;
894
+ const notNull = newControl(currentNotNull);
895
+ if (!currentNotNull) c.value = null;
896
+ disableIfNotEditing();
897
+ isEditing.subscribe(disableIfNotEditing, ControlChange.Value);
898
+ notNull.subscribe(() => {
899
+ const currentNotNull = notNull.current.value;
900
+ c.value = currentNotNull ? lastDefined.current.value : null;
901
+ c.disabled = !currentNotNull;
902
+ }, ControlChange.Value);
903
+ return notNull;
904
+ function disableIfNotEditing() {
905
+ notNull.disabled = isEditing.current.value === false;
906
+ }
907
+ });
908
+ }
909
+
910
+ export interface ExternalEditAction {
911
+ action: ActionRendererProps;
912
+ dontValidate?: boolean;
913
+ }
914
+ export interface ExternalEditData {
915
+ data: unknown;
916
+ actions: ExternalEditAction[];
917
+ }
918
+
919
+ export function getExternalEditData(
920
+ c: Control<any>,
921
+ ): Control<ExternalEditData | undefined> {
922
+ return ensureMetaValue(c, "$externalEditIndex", () => newControl(undefined));
923
+ }
924
+
925
+ export function getLastDefinedValue<V>(control: Control<V>): Control<V> {
926
+ return ensureMetaValue(control, "$lastDefined", () => {
927
+ const lastDefined = newControl(control.current.value);
928
+ control.subscribe(() => {
929
+ const nv = control.current.value;
930
+ if (nv != null) lastDefined.value = nv;
931
+ }, ControlChange.Value);
932
+ return lastDefined;
933
+ });
934
+ }
935
+
936
+ export function getIsEditing(
937
+ control: Control<any>,
938
+ ): Control<boolean | undefined> {
939
+ const lastDefined = getLastDefinedValue(control);
940
+ return ensureMetaValue(control, "$willEdit", () => {
941
+ const c = newControl(undefined);
942
+ c.subscribe(() => {
943
+ const currentEdit = c.current.value;
944
+ if (currentEdit !== undefined) {
945
+ control.value = currentEdit
946
+ ? lastDefined.current.value
947
+ : control.initialValue;
948
+ }
949
+ }, ControlChange.Value);
950
+ return c;
951
+ });
952
+ }
953
+
954
+ export function getAllValues(control: Control<any>): Control<unknown[]> {
955
+ return ensureMetaValue(control, "$allValues", () =>
956
+ newControl([control.value]),
957
+ );
958
+ }
959
+
960
+ export function applyValues(dataNode: SchemaDataNode, value: unknown): void {
961
+ const c = dataNode.control;
962
+ const sf = dataNode.schema.field;
963
+ if (c.isEqual(c.initialValue, value)) return;
964
+ if (sf.collection) {
965
+ return;
966
+ } else if (isCompoundField(sf)) {
967
+ if (value == null) return;
968
+ dataNode.schema.getChildNodes().forEach((c) => {
969
+ applyValues(
970
+ dataNode.getChild(c),
971
+ (value as Record<string, unknown>)[c.field.field],
972
+ );
973
+ });
974
+ } else {
975
+ const allValues = getAllValues(c);
976
+ allValues.setValue((changes) =>
977
+ changes.every((x) => !c.isEqual(x, value))
978
+ ? [...changes, value]
979
+ : changes,
980
+ );
981
+ }
982
+ }
983
+
984
+ export function collectDifferences(
985
+ dataNode: SchemaDataNode,
986
+ values: unknown[],
987
+ ): () => { editable: number; editing: number } {
988
+ values.forEach((v, i) => {
989
+ if (i == 0) dataNode.control.setInitialValue(v);
990
+ else applyValues(dataNode, v);
991
+ });
992
+ const allEdits: Control<boolean | undefined>[] = [];
993
+ resetMultiValues(dataNode);
994
+ return () => {
995
+ let editable = 0;
996
+ let editing = 0;
997
+ allEdits.forEach((x) => {
998
+ const b = x.value;
999
+ if (b === undefined) return;
1000
+ editable++;
1001
+ if (b) editing++;
1002
+ });
1003
+ return { editing, editable };
1004
+ };
1005
+
1006
+ function resetMultiValues(dataNode: SchemaDataNode): void {
1007
+ const c = dataNode.control;
1008
+ const sf = dataNode.schema.field;
1009
+ if (sf.collection) {
1010
+ return;
1011
+ } else if (isCompoundField(sf)) {
1012
+ if (c.value == null) return;
1013
+ dataNode.schema.getChildNodes().forEach((c) => {
1014
+ resetMultiValues(dataNode.getChild(c));
1015
+ });
1016
+ } else {
1017
+ allEdits.push(getIsEditing(c));
1018
+ const allValues = getAllValues(c);
1019
+ if (allValues.value.length > 1) {
1020
+ c.setInitialValue(undefined);
1021
+ getLastDefinedValue(c).value = null;
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ export function validationVisitor(
1028
+ onInvalid: (data: Control<unknown>) => void,
1029
+ ): ControlDataVisitor<any> {
1030
+ return (s) => {
1031
+ if (isCompoundNode(s.schema)) return undefined;
1032
+ const v = s.control;
1033
+ v.touched = true;
1034
+ if (!v.validate()) {
1035
+ onInvalid(v);
1036
+ }
1037
+ return undefined;
1038
+ };
1039
+ }