@rjsf/utils 6.0.0-beta.21 → 6.0.0-beta.23
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/dist/index.cjs +237 -2
- package/dist/index.cjs.map +4 -4
- package/dist/utils.esm.js +237 -2
- package/dist/utils.esm.js.map +4 -4
- package/dist/utils.umd.js +226 -2
- package/lib/enums.d.ts +4 -0
- package/lib/enums.js +4 -0
- package/lib/enums.js.map +1 -1
- package/lib/getDateElementProps.d.ts +1 -2
- package/lib/index.d.ts +8 -4
- package/lib/index.js +5 -1
- package/lib/index.js.map +1 -1
- package/lib/nameGenerators.d.ts +13 -0
- package/lib/nameGenerators.js +30 -0
- package/lib/nameGenerators.js.map +1 -0
- package/lib/shouldRenderOptionalField.js.map +1 -1
- package/lib/toFieldPathId.d.ts +4 -2
- package/lib/toFieldPathId.js +10 -3
- package/lib/toFieldPathId.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +70 -30
- package/lib/useAltDateWidgetProps.d.ts +39 -0
- package/lib/useAltDateWidgetProps.js +71 -0
- package/lib/useAltDateWidgetProps.js.map +1 -0
- package/lib/useDeepCompareMemo.d.ts +8 -0
- package/lib/useDeepCompareMemo.js +17 -0
- package/lib/useDeepCompareMemo.js.map +1 -0
- package/lib/useFileWidgetProps.d.ts +29 -0
- package/lib/useFileWidgetProps.js +119 -0
- package/lib/useFileWidgetProps.js.map +1 -0
- package/package.json +1 -1
- package/src/enums.ts +4 -0
- package/src/getDateElementProps.ts +1 -1
- package/src/index.ts +22 -5
- package/src/nameGenerators.ts +43 -0
- package/src/shouldRenderOptionalField.ts +3 -3
- package/src/toFieldPathId.ts +12 -2
- package/src/types.ts +83 -35
- package/src/useAltDateWidgetProps.tsx +163 -0
- package/src/useDeepCompareMemo.ts +17 -0
- package/src/useFileWidgetProps.ts +155 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NameGeneratorFunction, FieldPathList } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates bracketed names
|
|
5
|
+
* Example: root[tasks][0][title]
|
|
6
|
+
* For multi-value fields (checkboxes, multi-select): root[hobbies][]
|
|
7
|
+
*/
|
|
8
|
+
export const bracketNameGenerator: NameGeneratorFunction = (
|
|
9
|
+
path: FieldPathList,
|
|
10
|
+
idPrefix: string,
|
|
11
|
+
isMultiValue?: boolean,
|
|
12
|
+
): string => {
|
|
13
|
+
if (!path || path.length === 0) {
|
|
14
|
+
return idPrefix;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const baseName = path.reduce<string>((acc, pathUnit, index) => {
|
|
18
|
+
if (index === 0) {
|
|
19
|
+
return `${idPrefix}[${String(pathUnit)}]`;
|
|
20
|
+
}
|
|
21
|
+
return `${acc}[${String(pathUnit)}]`;
|
|
22
|
+
}, '');
|
|
23
|
+
|
|
24
|
+
// For multi-value fields, append [] to allow multiple values with the same name
|
|
25
|
+
return isMultiValue ? `${baseName}[]` : baseName;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generates dot-notation names
|
|
30
|
+
* Example: root.tasks.0.title
|
|
31
|
+
* Multi-value fields are handled the same as single-value fields in dot notation
|
|
32
|
+
*/
|
|
33
|
+
export const dotNotationNameGenerator: NameGeneratorFunction = (
|
|
34
|
+
path: FieldPathList,
|
|
35
|
+
idPrefix: string,
|
|
36
|
+
_isMultiValue?: boolean,
|
|
37
|
+
): string => {
|
|
38
|
+
if (!path || path.length === 0) {
|
|
39
|
+
return idPrefix;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `${idPrefix}.${path.map(String).join('.')}`;
|
|
43
|
+
};
|
|
@@ -40,14 +40,14 @@ export default function shouldRenderOptionalField<
|
|
|
40
40
|
const { enableOptionalDataFieldForType = [] } = getUiOptions<T, S, F>(uiSchema, registry.globalUiOptions);
|
|
41
41
|
let schemaType: ReturnType<typeof getSchemaType<S>>;
|
|
42
42
|
if (ANY_OF_KEY in schema && Array.isArray(schema[ANY_OF_KEY])) {
|
|
43
|
-
schemaType = getSchemaTypesForXxxOf(schema[ANY_OF_KEY] as S[]);
|
|
43
|
+
schemaType = getSchemaTypesForXxxOf<S>(schema[ANY_OF_KEY] as S[]);
|
|
44
44
|
} else if (ONE_OF_KEY in schema && Array.isArray(schema[ONE_OF_KEY])) {
|
|
45
|
-
schemaType = getSchemaTypesForXxxOf(schema[ONE_OF_KEY] as S[]);
|
|
45
|
+
schemaType = getSchemaTypesForXxxOf<S>(schema[ONE_OF_KEY] as S[]);
|
|
46
46
|
} else {
|
|
47
47
|
schemaType = getSchemaType<S>(schema);
|
|
48
48
|
}
|
|
49
49
|
return (
|
|
50
|
-
!isRootSchema(registry, schema) &&
|
|
50
|
+
!isRootSchema<T, S, F>(registry, schema) &&
|
|
51
51
|
!required &&
|
|
52
52
|
!!schemaType &&
|
|
53
53
|
!Array.isArray(schemaType) &&
|
package/src/toFieldPathId.ts
CHANGED
|
@@ -4,21 +4,31 @@ import { FieldPathId, FieldPathList, GlobalFormOptions } from './types';
|
|
|
4
4
|
/** Constructs the `FieldPathId` for `fieldPath`. If `parentPathId` is provided, the `fieldPath` is appended to the end
|
|
5
5
|
* of the parent path. Then the `ID_KEY` of the resulting `FieldPathId` is constructed from the `idPrefix` and
|
|
6
6
|
* `idSeparator` contained within the `globalFormOptions`. If `fieldPath` is passed as an empty string, it will simply
|
|
7
|
-
* generate the path from the `parentPath` (if provided) and the `idPrefix` and `idSeparator`
|
|
7
|
+
* generate the path from the `parentPath` (if provided) and the `idPrefix` and `idSeparator`. If a `nameGenerator`
|
|
8
|
+
* is provided in `globalFormOptions`, it will also generate the HTML `name` attribute.
|
|
8
9
|
*
|
|
9
10
|
* @param fieldPath - The property name or array index of the current field element
|
|
10
11
|
* @param globalFormOptions - The `GlobalFormOptions` used to get the `idPrefix` and `idSeparator`
|
|
11
12
|
* @param [parentPath] - The optional `FieldPathId` or `FieldPathList` of the parent element for this field element
|
|
13
|
+
* @param [isMultiValue] - Optional flag indicating this field accepts multiple values
|
|
12
14
|
* @returns - The `FieldPathId` for the given `fieldPath` and the optional `parentPathId`
|
|
13
15
|
*/
|
|
14
16
|
export default function toFieldPathId(
|
|
15
17
|
fieldPath: string | number,
|
|
16
18
|
globalFormOptions: GlobalFormOptions,
|
|
17
19
|
parentPath?: FieldPathId | FieldPathList,
|
|
20
|
+
isMultiValue?: boolean,
|
|
18
21
|
): FieldPathId {
|
|
19
22
|
const basePath = Array.isArray(parentPath) ? parentPath : parentPath?.path;
|
|
20
23
|
const childPath = fieldPath === '' ? [] : [fieldPath];
|
|
21
24
|
const path = basePath ? basePath.concat(...childPath) : childPath;
|
|
22
25
|
const id = [globalFormOptions.idPrefix, ...path].join(globalFormOptions.idSeparator);
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
// Generate name attribute if nameGenerator is provided
|
|
28
|
+
let name: string | undefined;
|
|
29
|
+
if (globalFormOptions.nameGenerator && path.length > 0) {
|
|
30
|
+
name = globalFormOptions.nameGenerator(path, globalFormOptions.idPrefix, isMultiValue);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { path, [ID_KEY]: id, ...(name !== undefined && { name }) };
|
|
24
34
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ButtonHTMLAttributes,
|
|
3
3
|
ChangeEvent,
|
|
4
4
|
ComponentType,
|
|
5
|
+
FocusEvent,
|
|
5
6
|
HTMLAttributes,
|
|
6
7
|
ReactElement,
|
|
7
8
|
ReactNode,
|
|
@@ -35,6 +36,9 @@ export type FormContextType = GenericObjectType;
|
|
|
35
36
|
*/
|
|
36
37
|
export type TestIdShape = Record<string, string>;
|
|
37
38
|
|
|
39
|
+
/** Function to generate HTML name attributes from path segments */
|
|
40
|
+
export type NameGeneratorFunction = (path: FieldPathList, idPrefix: string, isMultiValue?: boolean) => string;
|
|
41
|
+
|
|
38
42
|
/** Experimental feature that specifies the Array `minItems` default form state behavior
|
|
39
43
|
*/
|
|
40
44
|
export type Experimental_ArrayMinItems = {
|
|
@@ -175,6 +179,8 @@ export type FieldPathId = {
|
|
|
175
179
|
$id: string;
|
|
176
180
|
/** The path for a field */
|
|
177
181
|
path: FieldPathList;
|
|
182
|
+
/** The optional HTML name attribute for a field, generated by nameGenerator if provided */
|
|
183
|
+
name?: string;
|
|
178
184
|
};
|
|
179
185
|
|
|
180
186
|
/** Type describing a name used for a field in the `PathSchema` */
|
|
@@ -327,9 +333,9 @@ export type TemplatesType<T = any, S extends StrictRJSFSchema = RJSFSchema, F ex
|
|
|
327
333
|
/** The template to use while rendering the description for an array field */
|
|
328
334
|
ArrayFieldDescriptionTemplate: ComponentType<ArrayFieldDescriptionProps<T, S, F>>;
|
|
329
335
|
/** The template to use while rendering the buttons for an item in an array field */
|
|
330
|
-
ArrayFieldItemButtonsTemplate: ComponentType<
|
|
336
|
+
ArrayFieldItemButtonsTemplate: ComponentType<ArrayFieldItemButtonsTemplateProps<T, S, F>>;
|
|
331
337
|
/** The template to use while rendering an item in an array field */
|
|
332
|
-
ArrayFieldItemTemplate: ComponentType<
|
|
338
|
+
ArrayFieldItemTemplate: ComponentType<ArrayFieldItemTemplateProps<T, S, F>>;
|
|
333
339
|
/** The template to use while rendering the title for an array field */
|
|
334
340
|
ArrayFieldTitleTemplate: ComponentType<ArrayFieldTitleProps<T, S, F>>;
|
|
335
341
|
/** The template to use while rendering the standard html input */
|
|
@@ -338,6 +344,8 @@ export type TemplatesType<T = any, S extends StrictRJSFSchema = RJSFSchema, F ex
|
|
|
338
344
|
DescriptionFieldTemplate: ComponentType<DescriptionFieldProps<T, S, F>>;
|
|
339
345
|
/** The template to use while rendering the errors for the whole form */
|
|
340
346
|
ErrorListTemplate: ComponentType<ErrorListProps<T, S, F>>;
|
|
347
|
+
/** The template to use while rendering a fallback field for schemas that have an empty or unknown 'type' */
|
|
348
|
+
FallbackFieldTemplate: ComponentType<FallbackFieldTemplateProps<T, S, F>>;
|
|
341
349
|
/** The template to use while rendering the errors for a single field */
|
|
342
350
|
FieldErrorTemplate: ComponentType<FieldErrorProps<T, S, F>>;
|
|
343
351
|
/** The template to use while rendering the errors for a single field */
|
|
@@ -420,6 +428,16 @@ export type GlobalFormOptions = {
|
|
|
420
428
|
readonly idSeparator: string;
|
|
421
429
|
/** The component update strategy used by the Form and its fields for performance optimization */
|
|
422
430
|
readonly experimental_componentUpdateStrategy?: 'customDeep' | 'shallow' | 'always';
|
|
431
|
+
/** Optional function to generate custom HTML name attributes for form elements. Receives the field path segments
|
|
432
|
+
* and element type (object or array), and returns a custom name string. This allows backends like PHP/Rails
|
|
433
|
+
* (`root[tasks][0][title]`) or Django (`root__tasks-0__title`) to receive form data in their expected format.
|
|
434
|
+
*/
|
|
435
|
+
readonly nameGenerator?: NameGeneratorFunction;
|
|
436
|
+
/**
|
|
437
|
+
* Boolean flag that, when set to true, will cause the form to use a fallback UI when encountering a schema type that
|
|
438
|
+
* is not supported by RJSF or a custom field. When false, the UnsupportedField error component will be shown instead.
|
|
439
|
+
*/
|
|
440
|
+
readonly useFallbackUiForUnsupportedType?: boolean;
|
|
423
441
|
};
|
|
424
442
|
|
|
425
443
|
/** The object containing the registered core, theme and custom fields and widgets as well as the root schema, form
|
|
@@ -547,10 +565,38 @@ export type FieldTemplateProps<
|
|
|
547
565
|
formData?: T;
|
|
548
566
|
/** The value change event handler; Can be called with a new value to change the value for this field */
|
|
549
567
|
onChange: FieldProps<T, S, F>['onChange'];
|
|
550
|
-
/**
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
568
|
+
/** Callback used to handle the changing of an additional property key's name with the new value
|
|
569
|
+
*/
|
|
570
|
+
onKeyRename: (newKey: string) => void;
|
|
571
|
+
/** Callback used to handle the changing of an additional property key's name when the input is blurred. The event's
|
|
572
|
+
* target's value will be used as the new value. Its a wrapper callback around `onKeyRename`
|
|
573
|
+
*/
|
|
574
|
+
onKeyRenameBlur: (event: FocusEvent<HTMLInputElement>) => void;
|
|
575
|
+
/** Callback used to handle the removal of the additionalProperty */
|
|
576
|
+
onRemoveProperty: () => void;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The properties that are passed to a `FallbackField` implementation
|
|
581
|
+
*/
|
|
582
|
+
export type FallbackFieldProps<
|
|
583
|
+
T = any,
|
|
584
|
+
S extends StrictRJSFSchema = RJSFSchema,
|
|
585
|
+
F extends FormContextType = any,
|
|
586
|
+
> = FieldProps<T, S, F>;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The properties that are passed to a `FallbackFieldTemplate` implementation
|
|
590
|
+
*/
|
|
591
|
+
export type FallbackFieldTemplateProps<
|
|
592
|
+
T = any,
|
|
593
|
+
S extends StrictRJSFSchema = RJSFSchema,
|
|
594
|
+
F extends FormContextType = any,
|
|
595
|
+
> = RJSFBaseProps<T, S, F> & {
|
|
596
|
+
/** A ReactNode that allows the selecting a different type for the field */
|
|
597
|
+
typeSelector: ReactNode;
|
|
598
|
+
/** A ReactNode that renders the field with the present formData and matches the selected type */
|
|
599
|
+
schemaField: ReactNode;
|
|
554
600
|
};
|
|
555
601
|
|
|
556
602
|
/** The properties that are passed to the `UnsupportedFieldTemplate` implementation */
|
|
@@ -620,7 +666,7 @@ export type ArrayFieldDescriptionProps<
|
|
|
620
666
|
};
|
|
621
667
|
|
|
622
668
|
/** The properties of the buttons to render for each element in the ArrayFieldTemplateProps.items array */
|
|
623
|
-
export type
|
|
669
|
+
export type ArrayFieldItemButtonsTemplateProps<
|
|
624
670
|
T = any,
|
|
625
671
|
S extends StrictRJSFSchema = RJSFSchema,
|
|
626
672
|
F extends FormContextType = any,
|
|
@@ -647,20 +693,22 @@ export type ArrayFieldItemButtonsTemplateType<
|
|
|
647
693
|
index: number;
|
|
648
694
|
/** A number stating the total number `items` in the array */
|
|
649
695
|
totalItems: number;
|
|
650
|
-
/**
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
|
|
696
|
+
/** Callback function that adds a new item below this item */
|
|
697
|
+
onAddItem: (event?: any) => void;
|
|
698
|
+
/** Callback function that copies this item below itself */
|
|
699
|
+
onCopyItem: (event?: any) => void;
|
|
700
|
+
/** Callback function that moves the item up one spot in the list */
|
|
701
|
+
onMoveUpItem: (event?: any) => void;
|
|
702
|
+
/** Callback function that moves the item down one spot in the list */
|
|
703
|
+
onMoveDownItem: (event?: any) => void;
|
|
704
|
+
/** Callback function that removes the item from the list */
|
|
705
|
+
onRemoveItem: (event?: any) => void;
|
|
658
706
|
/** A boolean value stating if the array item is read-only */
|
|
659
707
|
readonly?: boolean;
|
|
660
708
|
};
|
|
661
709
|
|
|
662
|
-
/** The properties
|
|
663
|
-
export type
|
|
710
|
+
/** The properties used to render the ArrayFieldItemTemplate */
|
|
711
|
+
export type ArrayFieldItemTemplateProps<
|
|
664
712
|
T = any,
|
|
665
713
|
S extends StrictRJSFSchema = RJSFSchema,
|
|
666
714
|
F extends FormContextType = any,
|
|
@@ -668,7 +716,7 @@ export type ArrayFieldItemTemplateType<
|
|
|
668
716
|
/** The html for the item's content */
|
|
669
717
|
children: ReactNode;
|
|
670
718
|
/** The props to pass to the `ArrayFieldItemButtonTemplate` */
|
|
671
|
-
buttonsProps:
|
|
719
|
+
buttonsProps: ArrayFieldItemButtonsTemplateProps<T, S, F>;
|
|
672
720
|
/** The className string */
|
|
673
721
|
className: string;
|
|
674
722
|
/** A boolean value stating if the array item is disabled */
|
|
@@ -682,18 +730,13 @@ export type ArrayFieldItemTemplateType<
|
|
|
682
730
|
/** A boolean value stating if the array item is read-only */
|
|
683
731
|
readonly?: boolean;
|
|
684
732
|
/** A stable, unique key for the array item */
|
|
685
|
-
|
|
733
|
+
itemKey: string;
|
|
734
|
+
/** The UI schema of the array item's parent array field used for
|
|
735
|
+
* customization in some themes
|
|
736
|
+
*/
|
|
737
|
+
parentUiSchema?: UiSchema<T, S, F>;
|
|
686
738
|
};
|
|
687
739
|
|
|
688
|
-
/**
|
|
689
|
-
* @deprecated - Use `ArrayFieldItemTemplateType` instead
|
|
690
|
-
*/
|
|
691
|
-
export type ArrayFieldTemplateItemType<
|
|
692
|
-
T = any,
|
|
693
|
-
S extends StrictRJSFSchema = RJSFSchema,
|
|
694
|
-
F extends FormContextType = any,
|
|
695
|
-
> = ArrayFieldItemTemplateType<T, S, F>;
|
|
696
|
-
|
|
697
740
|
/** The common properties of the two container templates: `ArrayFieldTemplateProps` and `ObjectFieldTemplateProps` */
|
|
698
741
|
export type ContainerFieldTemplateProps<
|
|
699
742
|
T = any,
|
|
@@ -730,9 +773,9 @@ export type ArrayFieldTemplateProps<
|
|
|
730
773
|
> = ContainerFieldTemplateProps<T, S, F> & {
|
|
731
774
|
/** A boolean value stating whether new elements can be added to the array */
|
|
732
775
|
canAdd?: boolean;
|
|
733
|
-
/** An array of
|
|
734
|
-
items:
|
|
735
|
-
/** A function that adds a new item to the array */
|
|
776
|
+
/** An array of React elements representing the items in the array */
|
|
777
|
+
items: ReactElement[];
|
|
778
|
+
/** A function that adds a new item to the end of the array */
|
|
736
779
|
onAddClick: (event?: any) => void;
|
|
737
780
|
/** An array of strings listing all generated error messages from encountered errors for this widget */
|
|
738
781
|
rawErrors?: string[];
|
|
@@ -762,8 +805,10 @@ export type ObjectFieldTemplateProps<
|
|
|
762
805
|
description?: string | ReactElement;
|
|
763
806
|
/** An array of objects representing the properties in the object */
|
|
764
807
|
properties: ObjectFieldTemplatePropertyType[];
|
|
765
|
-
/**
|
|
766
|
-
|
|
808
|
+
/** Callback to use in order to add an new additionalProperty to the object field (to be used with
|
|
809
|
+
* additionalProperties and patternProperties)
|
|
810
|
+
*/
|
|
811
|
+
onAddProperty: () => void;
|
|
767
812
|
/** A boolean value stating if the object is read-only */
|
|
768
813
|
readonly?: boolean;
|
|
769
814
|
/** A boolean value stating if the object is required */
|
|
@@ -815,8 +860,9 @@ export type WrapIfAdditionalTemplateProps<
|
|
|
815
860
|
| 'disabled'
|
|
816
861
|
| 'schema'
|
|
817
862
|
| 'uiSchema'
|
|
818
|
-
| '
|
|
819
|
-
| '
|
|
863
|
+
| 'onKeyRename'
|
|
864
|
+
| 'onKeyRenameBlur'
|
|
865
|
+
| 'onRemoveProperty'
|
|
820
866
|
| 'registry'
|
|
821
867
|
>;
|
|
822
868
|
|
|
@@ -882,6 +928,8 @@ export interface WidgetProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F
|
|
|
882
928
|
multiple?: boolean;
|
|
883
929
|
/** An array of strings listing all generated error messages from encountered errors for this widget */
|
|
884
930
|
rawErrors?: string[];
|
|
931
|
+
/** The optional custom HTML name attribute generated by the nameGenerator function, if provided */
|
|
932
|
+
htmlName?: string;
|
|
885
933
|
}
|
|
886
934
|
|
|
887
935
|
/** The definition of a React-based Widget component */
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import dateRangeOptions from './dateRangeOptions';
|
|
4
|
+
import getDateElementProps, { DateElementFormat, DateElementProp } from './getDateElementProps';
|
|
5
|
+
import { ariaDescribedByIds } from './idGenerators';
|
|
6
|
+
import parseDateString from './parseDateString';
|
|
7
|
+
import toDateString from './toDateString';
|
|
8
|
+
import { DateObject, FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from './types';
|
|
9
|
+
|
|
10
|
+
/** Function that checks to see if a `DateObject` is ready for the onChange callback to be triggered
|
|
11
|
+
*
|
|
12
|
+
* @param state - The current `DateObject`
|
|
13
|
+
* @returns - True if the `state` is ready to trigger an onChange
|
|
14
|
+
*/
|
|
15
|
+
function readyForChange(state: DateObject) {
|
|
16
|
+
return Object.values(state).every((value) => value !== -1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The Props for the `DateElement` component */
|
|
20
|
+
export type DateElementProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> = Pick<
|
|
21
|
+
WidgetProps<T, S, F>,
|
|
22
|
+
'value' | 'name' | 'disabled' | 'readonly' | 'autofocus' | 'registry' | 'onBlur' | 'onFocus' | 'className'
|
|
23
|
+
> & {
|
|
24
|
+
/** The root id of the field */
|
|
25
|
+
rootId: string;
|
|
26
|
+
/** The selector function for a specific prop within the `DateObject`, for a value */
|
|
27
|
+
select: (property: keyof DateObject, value: any) => void;
|
|
28
|
+
/** The type of the date element */
|
|
29
|
+
type: DateElementProp['type'];
|
|
30
|
+
/** The range for the date element */
|
|
31
|
+
range: DateElementProp['range'];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** The `DateElement` component renders one of the 6 date element selectors for an `AltDateWidget`, using the `select`
|
|
35
|
+
* widget from the registry.
|
|
36
|
+
*
|
|
37
|
+
* @param props - The `DateElementProps` for the date element
|
|
38
|
+
*/
|
|
39
|
+
export function DateElement<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
|
|
40
|
+
props: DateElementProps<T, S, F>,
|
|
41
|
+
) {
|
|
42
|
+
const {
|
|
43
|
+
className = 'form-control',
|
|
44
|
+
type,
|
|
45
|
+
range,
|
|
46
|
+
value,
|
|
47
|
+
select,
|
|
48
|
+
rootId,
|
|
49
|
+
name,
|
|
50
|
+
disabled,
|
|
51
|
+
readonly,
|
|
52
|
+
autofocus,
|
|
53
|
+
registry,
|
|
54
|
+
onBlur,
|
|
55
|
+
onFocus,
|
|
56
|
+
} = props;
|
|
57
|
+
const id = `${rootId}_${type}`;
|
|
58
|
+
const { SelectWidget } = registry.widgets;
|
|
59
|
+
const onChange = useCallback((value: any) => select(type as keyof DateObject, value), [select, type]);
|
|
60
|
+
return (
|
|
61
|
+
<SelectWidget
|
|
62
|
+
schema={{ type: 'integer' } as S}
|
|
63
|
+
id={id}
|
|
64
|
+
name={name}
|
|
65
|
+
className={className}
|
|
66
|
+
options={{ enumOptions: dateRangeOptions<S>(range[0], range[1]) }}
|
|
67
|
+
placeholder={type}
|
|
68
|
+
value={value}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
readonly={readonly}
|
|
71
|
+
autofocus={autofocus}
|
|
72
|
+
onChange={onChange}
|
|
73
|
+
onBlur={onBlur}
|
|
74
|
+
onFocus={onFocus}
|
|
75
|
+
registry={registry}
|
|
76
|
+
label=''
|
|
77
|
+
aria-describedby={ariaDescribedByIds(rootId)}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The result of a call to the `useAltDateWidgetProps()` hook */
|
|
83
|
+
export type UseAltDateWidgetResult = {
|
|
84
|
+
/** The list of `DateElementProp` data to render for the `AltDateWidget` */
|
|
85
|
+
elements: DateElementProp[];
|
|
86
|
+
/** The callback that handles the changing of DateElement components */
|
|
87
|
+
handleChange: (property: keyof DateObject, value?: string) => void;
|
|
88
|
+
/** The callback that will clear the `AltDateWidget` when a button is clicked */
|
|
89
|
+
handleClear: (event: MouseEvent) => void;
|
|
90
|
+
/** The callback that will set the `AltDateWidget` to NOW when a button is clicked */
|
|
91
|
+
handleSetNow: (event: MouseEvent) => void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Hook which encapsulates the logic needed to render an `AltDateWidget` with optional `time` elements. It contains
|
|
95
|
+
* the `state` of the current date(/time) selections in the widget. It returns a `UseAltDateWidgetResult` object
|
|
96
|
+
* that contains the `elements: DateElementProp[]` and three callbacks needed to change one of the rendered `elements`,
|
|
97
|
+
* and to handle the clicking of the `clear` and `setNow` buttons.
|
|
98
|
+
*
|
|
99
|
+
* @param props - The `WidgetProps` for the `AltDateWidget`
|
|
100
|
+
*/
|
|
101
|
+
export default function useAltDateWidgetProps<
|
|
102
|
+
T = any,
|
|
103
|
+
S extends StrictRJSFSchema = RJSFSchema,
|
|
104
|
+
F extends FormContextType = any,
|
|
105
|
+
>(props: WidgetProps<T, S, F>): UseAltDateWidgetResult {
|
|
106
|
+
const { time = false, disabled = false, readonly = false, options, onChange, value } = props;
|
|
107
|
+
const [state, setState] = useState(parseDateString(value, time));
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
setState(parseDateString(value, time));
|
|
111
|
+
}, [time, value]);
|
|
112
|
+
|
|
113
|
+
const handleChange = useCallback(
|
|
114
|
+
(property: keyof DateObject, value?: string) => {
|
|
115
|
+
const nextState = {
|
|
116
|
+
...state,
|
|
117
|
+
[property]: typeof value === 'undefined' ? -1 : value,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (readyForChange(nextState)) {
|
|
121
|
+
onChange(toDateString(nextState, time));
|
|
122
|
+
} else {
|
|
123
|
+
setState(nextState);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[state, onChange, time],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const handleClear = useCallback(
|
|
130
|
+
(event: MouseEvent) => {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
if (disabled || readonly) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
onChange(undefined);
|
|
136
|
+
},
|
|
137
|
+
[disabled, readonly, onChange],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const handleSetNow = useCallback(
|
|
141
|
+
(event: MouseEvent) => {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
if (disabled || readonly) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const nextState = parseDateString(new Date().toJSON(), time);
|
|
147
|
+
onChange(toDateString(nextState, time));
|
|
148
|
+
},
|
|
149
|
+
[disabled, readonly, time, onChange],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const elements = useMemo(
|
|
153
|
+
() =>
|
|
154
|
+
getDateElementProps(
|
|
155
|
+
state,
|
|
156
|
+
time,
|
|
157
|
+
options.yearsRange as [number, number] | undefined,
|
|
158
|
+
options.format as DateElementFormat | undefined,
|
|
159
|
+
),
|
|
160
|
+
[state, time, options.yearsRange, options.format],
|
|
161
|
+
);
|
|
162
|
+
return { elements, handleChange, handleClear, handleSetNow };
|
|
163
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import isEqual from 'lodash/isEqual';
|
|
3
|
+
|
|
4
|
+
/** Hook that stores and returns a `T` value. If `newValue` is the same as the stored one, then the stored one is
|
|
5
|
+
* returned to avoid having a component rerender due it being a different object. Otherwise, the `newValue` is stored
|
|
6
|
+
* and returned.
|
|
7
|
+
*
|
|
8
|
+
* @param newValue - The potential new `T` value
|
|
9
|
+
* @returns - The latest stored `T` value
|
|
10
|
+
*/
|
|
11
|
+
export default function useDeepCompareMemo<T = unknown>(newValue: T): T {
|
|
12
|
+
const valueRef = useRef<T>(newValue);
|
|
13
|
+
if (!isEqual(newValue, valueRef.current)) {
|
|
14
|
+
valueRef.current = newValue;
|
|
15
|
+
}
|
|
16
|
+
return valueRef.current;
|
|
17
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import dataURItoBlob from './dataURItoBlob';
|
|
4
|
+
|
|
5
|
+
/** The information about files used by a FileWidget */
|
|
6
|
+
export type FileInfoType = {
|
|
7
|
+
/** The url of the data containing the file */
|
|
8
|
+
dataURL?: string | null;
|
|
9
|
+
/** The name of the file */
|
|
10
|
+
name: string;
|
|
11
|
+
/** The size of the file */
|
|
12
|
+
size: number;
|
|
13
|
+
/** The type of the file */
|
|
14
|
+
type: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface UseFileWidgetPropsResult {
|
|
18
|
+
/** The list of FileInfoType contained within the FileWidget */
|
|
19
|
+
filesInfo: FileInfoType[];
|
|
20
|
+
/** The callback handler to pass to the onChange of the input */
|
|
21
|
+
handleChange: (files: FileList) => void;
|
|
22
|
+
/** The callback handler to pass in order to delete a file */
|
|
23
|
+
handleRemove: (index: number) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Updated the given `dataUrl` to add the `name` to it
|
|
27
|
+
*
|
|
28
|
+
* @param dataURL - The url description string
|
|
29
|
+
* @param name - The name of the file to add to the dataUrl
|
|
30
|
+
* @returns - The `dataUrl` updated to include the name
|
|
31
|
+
*/
|
|
32
|
+
function addNameToDataURL(dataURL: string, name: string) {
|
|
33
|
+
return dataURL.replace(';base64', `;name=${encodeURIComponent(name)};base64`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Returns a promise that will read the file from the browser and return it as the result of the promise.
|
|
37
|
+
*
|
|
38
|
+
* @param file - The `File` information to read
|
|
39
|
+
* @returns - A promise that resolves to the read file.
|
|
40
|
+
*/
|
|
41
|
+
function processFile(file: File): Promise<FileInfoType> {
|
|
42
|
+
const { name, size, type } = file;
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const reader = new window.FileReader();
|
|
45
|
+
reader.onerror = reject;
|
|
46
|
+
reader.onload = (event) => {
|
|
47
|
+
if (typeof event.target?.result === 'string') {
|
|
48
|
+
resolve({
|
|
49
|
+
dataURL: addNameToDataURL(event.target.result, name),
|
|
50
|
+
name,
|
|
51
|
+
size,
|
|
52
|
+
type,
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
resolve({
|
|
56
|
+
dataURL: null,
|
|
57
|
+
name,
|
|
58
|
+
size,
|
|
59
|
+
type,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
reader.readAsDataURL(file);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reads a list of files from the browser, returning the results of the promises of each individual file read.
|
|
68
|
+
*
|
|
69
|
+
* @param files - The list of files to read
|
|
70
|
+
* @returns - The list of read files
|
|
71
|
+
*/
|
|
72
|
+
function processFiles(files: FileList) {
|
|
73
|
+
return Promise.all(Array.from(files).map(processFile));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Extracts the file information from the data URLs
|
|
77
|
+
*
|
|
78
|
+
* @param dataURLs - The information about the files
|
|
79
|
+
* @returns - The list of `FileInfoType` objects extracted from the data urls
|
|
80
|
+
*/
|
|
81
|
+
function extractFileInfo(dataURLs: string[]): FileInfoType[] {
|
|
82
|
+
return dataURLs.reduce((acc, dataURL) => {
|
|
83
|
+
if (!dataURL) {
|
|
84
|
+
return acc;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const { blob, name } = dataURItoBlob(dataURL);
|
|
88
|
+
return [
|
|
89
|
+
...acc,
|
|
90
|
+
{
|
|
91
|
+
dataURL,
|
|
92
|
+
name: name,
|
|
93
|
+
size: blob.size,
|
|
94
|
+
type: blob.type,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
} catch {
|
|
98
|
+
// Invalid dataURI, so just ignore it.
|
|
99
|
+
return acc;
|
|
100
|
+
}
|
|
101
|
+
}, [] as FileInfoType[]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Hook which encapsulates the logic needed to read and convert a `value` of `File` or `File[]` into the
|
|
105
|
+
* `filesInfo: FileInfoType[]` and the two callback implementations needed to change the list or to remove a
|
|
106
|
+
* `File` from the list. To be used by theme specific `FileWidget` implementations.
|
|
107
|
+
*
|
|
108
|
+
* @param value - The current value of the `FileWidget`
|
|
109
|
+
* @param onChange - The onChange handler for the `FileWidget`
|
|
110
|
+
* @param [multiple=false] - Flag indicating whether the control supports multiple selections
|
|
111
|
+
* @returns - The `UseFileWidgetPropsResult` to be used within a `FileWidget` implementation
|
|
112
|
+
*/
|
|
113
|
+
export default function useFileWidgetProps(
|
|
114
|
+
value: string | string[] | undefined | null,
|
|
115
|
+
onChange: (value?: string | null | (string | null)[]) => void,
|
|
116
|
+
multiple = false,
|
|
117
|
+
): UseFileWidgetPropsResult {
|
|
118
|
+
const values: (string | null)[] = useMemo(() => {
|
|
119
|
+
if (multiple && value) {
|
|
120
|
+
return Array.isArray(value) ? value : [value];
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}, [value, multiple]);
|
|
124
|
+
const filesInfo = useMemo(
|
|
125
|
+
() => (Array.isArray(value) ? extractFileInfo(value) : extractFileInfo([value || ''])),
|
|
126
|
+
[value],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const handleChange = useCallback(
|
|
130
|
+
(files: FileList) => {
|
|
131
|
+
processFiles(files).then((filesInfoEvent) => {
|
|
132
|
+
const newValue = filesInfoEvent.map((fileInfo) => fileInfo.dataURL || null);
|
|
133
|
+
if (multiple) {
|
|
134
|
+
onChange(values.concat(...newValue));
|
|
135
|
+
} else {
|
|
136
|
+
onChange(newValue[0]);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
[values, multiple, onChange],
|
|
141
|
+
);
|
|
142
|
+
const handleRemove = useCallback(
|
|
143
|
+
(index: number) => {
|
|
144
|
+
if (multiple) {
|
|
145
|
+
const newValue = values.filter((_, i: number) => i !== index);
|
|
146
|
+
onChange(newValue);
|
|
147
|
+
} else {
|
|
148
|
+
onChange(undefined);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[values, multiple, onChange],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return { filesInfo, handleChange, handleRemove };
|
|
155
|
+
}
|