@react-fabric/form 1.0.0-beta.9 → 1.0.2

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.
Files changed (215) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +95 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +94 -33
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +107 -78
  7. package/dist/cjs/builder/FormField.js +60 -39
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
  11. package/dist/cjs/builder/components/SchemaField.js +423 -0
  12. package/dist/cjs/form/Controller.js +66 -19
  13. package/dist/cjs/form/Form.js +97 -39
  14. package/dist/cjs/form/useForm.js +32 -0
  15. package/dist/cjs/hooks/useFileUploader.js +74 -45
  16. package/dist/cjs/hooks/useFormBuilder.js +85 -31
  17. package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
  18. package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
  19. package/dist/cjs/i18n/index.js +4 -4
  20. package/dist/cjs/index.js +41 -2
  21. package/dist/cjs/input/ArrayInput.js +259 -135
  22. package/dist/cjs/input/AvatarInput.js +99 -63
  23. package/dist/cjs/input/Checkbox.js +24 -25
  24. package/dist/cjs/input/ColorInput.js +138 -81
  25. package/dist/cjs/input/ColorPicker.js +78 -95
  26. package/dist/cjs/input/DateInput.js +192 -0
  27. package/dist/cjs/input/Field.js +21 -20
  28. package/dist/cjs/input/File.js +85 -80
  29. package/dist/cjs/input/Hidden.js +27 -13
  30. package/dist/cjs/input/Input.js +49 -48
  31. package/dist/cjs/input/Number.js +46 -44
  32. package/dist/cjs/input/Password.js +51 -49
  33. package/dist/cjs/input/Radio.js +26 -16
  34. package/dist/cjs/input/RadioGroup.js +26 -11
  35. package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
  36. package/dist/cjs/input/Search.js +58 -64
  37. package/dist/cjs/input/Slider.js +57 -66
  38. package/dist/cjs/input/Switch.js +29 -27
  39. package/dist/cjs/input/Textarea.js +65 -53
  40. package/dist/cjs/internal/ErrorIcon.js +9 -9
  41. package/dist/cjs/internal/FieldWrapper.js +23 -13
  42. package/dist/cjs/internal/InputWrapper.js +85 -87
  43. package/dist/cjs/ranger/index.js +7 -7
  44. package/dist/cjs/ranger/utils.js +9 -4
  45. package/dist/cjs/select/AutoComplete.js +93 -76
  46. package/dist/cjs/select/DualList.js +235 -154
  47. package/dist/cjs/select/List.js +122 -89
  48. package/dist/cjs/select/Options.js +58 -54
  49. package/dist/cjs/select/Select.js +119 -89
  50. package/dist/cjs/select/helpers.js +5 -5
  51. package/dist/cjs/select/useSelect.js +83 -58
  52. package/dist/cjs/types/schema.js +43 -0
  53. package/dist/cjs/utils/eventHandlers.js +16 -6
  54. package/dist/cjs/utils/getColor.js +6 -6
  55. package/dist/cjs/utils/getProps.js +49 -0
  56. package/dist/esm/builder/FileField.js +109 -80
  57. package/dist/esm/builder/FormField.js +60 -39
  58. package/dist/esm/builder/SchemaEditor.js +72 -0
  59. package/dist/esm/builder/components/AddSchemaFields.js +64 -0
  60. package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
  61. package/dist/esm/builder/components/SchemaField.js +423 -0
  62. package/dist/esm/form/Controller.js +66 -19
  63. package/dist/esm/form/Form.js +97 -39
  64. package/dist/esm/form/useForm.js +32 -0
  65. package/dist/esm/hooks/useFileUploader.js +75 -46
  66. package/dist/esm/hooks/useFormBuilder.js +84 -30
  67. package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
  68. package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
  69. package/dist/esm/i18n/index.js +2 -2
  70. package/dist/esm/index.js +36 -1
  71. package/dist/esm/input/ArrayInput.js +260 -136
  72. package/dist/esm/input/AvatarInput.js +99 -63
  73. package/dist/esm/input/Checkbox.js +26 -27
  74. package/dist/esm/input/ColorInput.js +141 -84
  75. package/dist/esm/input/ColorPicker.js +80 -97
  76. package/dist/esm/input/DateInput.js +192 -0
  77. package/dist/esm/input/Field.js +21 -20
  78. package/dist/esm/input/File.js +89 -84
  79. package/dist/esm/input/Hidden.js +28 -14
  80. package/dist/esm/input/Input.js +51 -50
  81. package/dist/esm/input/Number.js +48 -46
  82. package/dist/esm/input/Password.js +54 -52
  83. package/dist/esm/input/Radio.js +27 -17
  84. package/dist/esm/input/RadioGroup.js +27 -12
  85. package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
  86. package/dist/esm/input/Search.js +61 -67
  87. package/dist/esm/input/Slider.js +58 -67
  88. package/dist/esm/input/Switch.js +31 -29
  89. package/dist/esm/input/Textarea.js +67 -55
  90. package/dist/esm/internal/ErrorIcon.js +11 -11
  91. package/dist/esm/internal/FieldWrapper.js +23 -13
  92. package/dist/esm/internal/InputWrapper.js +88 -90
  93. package/dist/esm/ranger/index.js +7 -7
  94. package/dist/esm/ranger/utils.js +9 -4
  95. package/dist/esm/select/AutoComplete.js +95 -78
  96. package/dist/esm/select/DualList.js +233 -152
  97. package/dist/esm/select/List.js +125 -92
  98. package/dist/esm/select/Options.js +58 -54
  99. package/dist/esm/select/Select.js +122 -92
  100. package/dist/esm/select/helpers.js +5 -5
  101. package/dist/esm/select/useSelect.js +84 -59
  102. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  103. package/dist/esm/utils/eventHandlers.js +16 -6
  104. package/dist/esm/utils/getColor.js +6 -6
  105. package/dist/esm/utils/getProps.js +49 -0
  106. package/dist/types/builder/FileField.d.ts +7 -4
  107. package/dist/types/builder/FormField.d.ts +4 -2
  108. package/dist/types/builder/SchemaEditor.d.ts +6 -0
  109. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  110. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  111. package/dist/types/builder/components/SchemaField.d.ts +12 -0
  112. package/dist/types/form/Controller.d.ts +45 -3
  113. package/dist/types/form/Form.d.ts +38 -7
  114. package/dist/types/form/useForm.d.ts +6 -0
  115. package/dist/types/hooks/useFileUploader.d.ts +24 -3
  116. package/dist/types/hooks/useFormBuilder.d.ts +41 -3
  117. package/dist/types/index.d.ts +32 -2
  118. package/dist/types/input/ArrayInput.d.ts +68 -13
  119. package/dist/types/input/AvatarInput.d.ts +23 -6
  120. package/dist/types/input/Checkbox.d.ts +12 -10
  121. package/dist/types/input/ColorInput.d.ts +15 -5
  122. package/dist/types/input/ColorPicker.d.ts +10 -2
  123. package/dist/types/input/DateInput.d.ts +21 -0
  124. package/dist/types/input/Field.d.ts +9 -36
  125. package/dist/types/input/File.d.ts +16 -4
  126. package/dist/types/input/Hidden.d.ts +7 -6
  127. package/dist/types/input/Input.d.ts +11 -4
  128. package/dist/types/input/Number.d.ts +9 -4
  129. package/dist/types/input/Password.d.ts +9 -4
  130. package/dist/types/input/Radio.d.ts +9 -4
  131. package/dist/types/input/RadioGroup.d.ts +14 -6
  132. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
  133. package/dist/types/input/Search.d.ts +14 -4
  134. package/dist/types/input/Slider.d.ts +9 -6
  135. package/dist/types/input/Switch.d.ts +13 -8
  136. package/dist/types/input/Textarea.d.ts +15 -4
  137. package/dist/types/internal/ErrorIcon.d.ts +4 -1
  138. package/dist/types/internal/FieldWrapper.d.ts +10 -1
  139. package/dist/types/internal/InputWrapper.d.ts +19 -1
  140. package/dist/types/ranger/index.d.ts +7 -3
  141. package/dist/types/ranger/utils.d.ts +2 -2
  142. package/dist/types/select/AutoComplete.d.ts +7 -2
  143. package/dist/types/select/DualList.d.ts +11 -37
  144. package/dist/types/select/List.d.ts +7 -2
  145. package/dist/types/select/Options.d.ts +8 -7
  146. package/dist/types/select/Select.d.ts +7 -2
  147. package/dist/types/select/helpers.d.ts +2 -2
  148. package/dist/types/select/useSelect.d.ts +10 -28
  149. package/dist/types/types/index.d.ts +73 -39
  150. package/dist/types/types/schema.d.ts +78 -32
  151. package/dist/types/utils/eventHandlers.d.ts +4 -4
  152. package/dist/types/utils/getColor.d.ts +4 -4
  153. package/dist/types/utils/getProps.d.ts +26 -0
  154. package/dist/types/utils/index.d.ts +1 -0
  155. package/package.json +25 -25
  156. package/src/builder/FileField.tsx +73 -56
  157. package/src/builder/FormField.tsx +66 -58
  158. package/src/builder/SchemaEditor.tsx +78 -0
  159. package/src/builder/components/AddSchemaFields.tsx +64 -0
  160. package/src/builder/components/SchemaArrayForm.tsx +146 -0
  161. package/src/builder/components/SchemaField.tsx +405 -0
  162. package/src/form/Controller.tsx +61 -24
  163. package/src/form/Form.tsx +74 -50
  164. package/src/form/useForm.ts +53 -0
  165. package/src/hooks/useFileUploader.ts +115 -84
  166. package/src/hooks/useFormBuilder.tsx +80 -41
  167. package/src/i18n/{ar.json → ar/form.json} +5 -4
  168. package/src/i18n/{en.json → en/form.json} +5 -5
  169. package/src/i18n/index.ts +2 -2
  170. package/src/index.ts +34 -20
  171. package/src/input/ArrayInput.tsx +255 -173
  172. package/src/input/AvatarInput.tsx +82 -39
  173. package/src/input/Checkbox.tsx +30 -54
  174. package/src/input/ColorInput.tsx +146 -105
  175. package/src/input/ColorPicker.tsx +76 -102
  176. package/src/input/DateInput.tsx +225 -0
  177. package/src/input/Field.tsx +23 -65
  178. package/src/input/File.tsx +76 -98
  179. package/src/input/Hidden.tsx +36 -24
  180. package/src/input/Input.tsx +55 -53
  181. package/src/input/Number.tsx +49 -54
  182. package/src/input/Password.tsx +61 -66
  183. package/src/input/Radio.tsx +28 -37
  184. package/src/input/RadioGroup.tsx +30 -25
  185. package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
  186. package/src/input/Search.tsx +64 -77
  187. package/src/input/Slider.tsx +80 -109
  188. package/src/input/Switch.tsx +39 -53
  189. package/src/input/Textarea.tsx +72 -59
  190. package/src/internal/ErrorIcon.tsx +22 -21
  191. package/src/internal/FieldWrapper.tsx +33 -23
  192. package/src/internal/InputWrapper.tsx +132 -143
  193. package/src/ranger/index.tsx +37 -117
  194. package/src/ranger/utils.ts +12 -11
  195. package/src/select/AutoComplete.tsx +113 -123
  196. package/src/select/DualList.tsx +218 -204
  197. package/src/select/List.tsx +130 -97
  198. package/src/select/Options.tsx +59 -57
  199. package/src/select/Select.tsx +148 -122
  200. package/src/select/helpers.ts +13 -8
  201. package/src/select/useSelect.tsx +110 -127
  202. package/src/types/index.ts +76 -45
  203. package/src/types/schema.ts +97 -40
  204. package/src/utils/eventHandlers.ts +24 -26
  205. package/src/utils/getColor.ts +9 -8
  206. package/src/utils/getProps.ts +47 -0
  207. package/src/utils/index.ts +1 -0
  208. package/dist/cjs/input/ArrayInput.module.css +0 -1
  209. package/dist/cjs/input/ColorPicker.module.css +0 -1
  210. package/dist/cjs/input/File.module.css +0 -1
  211. package/dist/cjs/internal/Field.module.css +0 -1
  212. package/dist/esm/input/ArrayInput.module.css +0 -1
  213. package/dist/esm/input/ColorPicker.module.css +0 -1
  214. package/dist/esm/input/File.module.css +0 -1
  215. package/dist/esm/internal/Field.module.css +0 -1
@@ -21,29 +21,32 @@
21
21
  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  */
23
23
 
24
- import { DndContext, type DragEndEvent } from "@dnd-kit/core";
25
- import { SortableContext, useSortable } from "@dnd-kit/sortable";
26
- import { Button, CoreIcons, Icon, Tooltip } from "@react-fabric/core";
27
- import { isString } from "@react-fabric/utilities";
28
- import classNames from "classnames";
24
+ import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers";
25
+ import { DragDropProvider, type DragEndEvent } from "@dnd-kit/react";
26
+ import { useSortable } from "@dnd-kit/react/sortable";
27
+ import { Button, cloneChildren, Icon, Tooltip } from "@react-fabric/core";
28
+ import { cn, isFalse, isObject, isString } from "@react-fabric/utilities";
29
29
  import {
30
- Children,
31
30
  cloneElement,
31
+ isValidElement,
32
32
  useCallback,
33
+ useEffect,
33
34
  useImperativeHandle,
34
35
  useMemo,
36
+ useState,
35
37
  type PropsWithChildren,
36
- type ReactNode,
37
- type RefObject,
38
+ type Ref,
38
39
  } from "react";
39
40
  import { useFieldArray, useFormContext } from "react-hook-form";
40
41
  import { useTranslation } from "react-i18next";
41
42
  import { FieldWrapper } from "../internal/FieldWrapper";
42
- import classes from "./ArrayInput.module.css";
43
43
 
44
- import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
45
- import { CSS } from "@dnd-kit/utilities";
46
- export interface ArrayInputProps {
44
+ interface ErrorObject {
45
+ key: string;
46
+ values: Record<string, string>;
47
+ }
48
+
49
+ export interface ArrayInputProps<T extends AnyObject = string> {
47
50
  /**
48
51
  * input label
49
52
  */
@@ -51,7 +54,7 @@ export interface ArrayInputProps {
51
54
  /**
52
55
  * append to label end
53
56
  */
54
- appendLabel?: JSX.Element | string | number | boolean;
57
+ appendLabel?: React.ReactElement | string | number | boolean;
55
58
  /**
56
59
  * info label at bottom
57
60
  */
@@ -77,6 +80,10 @@ export interface ArrayInputProps {
77
80
  * minimum required length
78
81
  */
79
82
  minItems?: number;
83
+ /**
84
+ * maximum length
85
+ */
86
+ maxItems?: number;
80
87
  /**
81
88
  * field name to focus on add
82
89
  */
@@ -87,23 +94,32 @@ export interface ArrayInputProps {
87
94
  enableSorting?: boolean;
88
95
  children:
89
96
  | React.ReactNode
90
- | ((props: { index: number; name: string }) => ReactNode);
97
+ | ((props: { index: number; name: string; item: T; onChange: (val: false | undefined | T) => void }) => React.ReactNode);
91
98
  /**
92
99
  * add item button label
93
100
  */
94
101
  addLabel?: string;
95
- /**
96
- * fixed length list
97
- */
98
- fixedList?: boolean;
99
- arrayRef?: RefObject<{
100
- addItem: (item: AnyObject) => void;
102
+ buttonPosition?: "top" | "bottom" | "both";
103
+ arrayRef?: Ref<{
104
+ addItem: (item: false | undefined | T | Promise<false | undefined | T>) => void;
101
105
  removeItem: (idx: number) => void;
102
106
  }>;
103
107
  /**
104
- * Add item
108
+ * add new item
105
109
  */
106
- onAdd?: () => AnyObject;
110
+ onAdd?: () => false | undefined | T | Promise<false | undefined | T>;
111
+ /**
112
+ * on item remove
113
+ */
114
+ onRemove?: (item: T, idx: number) => void;
115
+ /**
116
+ * before item remove
117
+ */
118
+ onBeforeRemove?: (item: T, idx: number) => Promise<boolean> | boolean;
119
+ /**
120
+ * can remove item
121
+ */
122
+ canRemove?: false | "newonly" | ((props: { item: T; index: number; lastItem: boolean }) => boolean);
107
123
 
108
124
  /**
109
125
  * field width
@@ -119,213 +135,279 @@ export interface ArrayInputProps {
119
135
  labelWidth?: string;
120
136
  }
121
137
 
122
- export const ArrayInput = ({
138
+ function SortableItem({
139
+ id,
140
+ index,
141
+ children,
142
+ className,
143
+ enableSorting,
144
+ }: PropsWithChildren<{ id: string; index: number; className: string; enableSorting?: boolean }>) {
145
+ const { ref, handleRef, isDragging } = useSortable({ id, index, disabled: !enableSorting });
146
+ if (enableSorting) {
147
+ return (
148
+ <div className={className} ref={ref}>
149
+ <Icon ref={handleRef} className={isDragging ? "cursor-grabbing" : "cursor-grab"} icon="icon-[mdi--drag-vertical]" />
150
+ {children}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ return <div className={className}>{children}</div>;
156
+ }
157
+
158
+ function Wrapper({
159
+ children,
160
+ fields,
161
+ enableSorting,
162
+ onMove,
163
+ }: {
164
+ children: React.ReactNode;
165
+ enableSorting?: boolean;
166
+ fields: KeyValue[];
167
+ onMove: (active: number, end: number) => void;
168
+ }) {
169
+ if (enableSorting) {
170
+ // pass id list to dnd context
171
+ const idMap = fields.map((item) => item.__ID__ as string);
172
+ const handleDragEnd = (e: DragEndEvent) => {
173
+ if (e.operation.source?.id !== e.operation.target?.id) {
174
+ // find index of item and drop over
175
+ const active = idMap.indexOf(`${e.operation.source?.id}`);
176
+ const end = idMap.indexOf(`${e.operation.target?.id}`);
177
+ onMove(active, end);
178
+ }
179
+ };
180
+ return (
181
+ <DragDropProvider onDragEnd={handleDragEnd} modifiers={[RestrictToVerticalAxis]}>
182
+ {children}
183
+ </DragDropProvider>
184
+ );
185
+ }
186
+
187
+ return children;
188
+ }
189
+
190
+ /**
191
+ * ArrayInput component for managing an array of items in a form.
192
+ * It allows adding, removing, and sorting items within the array.
193
+ * It integrates with react-hook-form for form state management and validation.
194
+ * It supports fixed-length lists, custom add/remove handlers, and can display validation errors.
195
+ * It also provides options for inline display and custom button positioning.
196
+ *
197
+ * It uses the `useFieldArray` hook from `react-hook-form` to manage the array state and provides a simple interface for adding and removing items.
198
+ *
199
+ * It also supports drag-and-drop sorting using `@dnd-kit/core` and `@dnd-kit/sortable` for a better user experience when reordering items.
200
+ *
201
+ * @example
202
+ * ```jsx
203
+ * <ArrayInput
204
+ * name="items"
205
+ * label="Items"
206
+ * addLabel="Add Item"
207
+ * onAdd={() => ({ name: "", value: "" })}
208
+ * onRemove={(item, index) => console.log("Removed item:", item, "at index:", index)}
209
+ * onBeforeRemove={(item, index) => confirm(`Are you sure you want to remove item at index ${index}?`)}
210
+ * canRemove={({ item, index, lastItem }) => !lastItem} // Prevent removing the last item
211
+ * >
212
+ * {({ index, name }) => (
213
+ * <Input
214
+ * name={name}
215
+ * label={`Item ${index + 1}`}
216
+ * placeholder="Enter item value"
217
+ * required={index === 0} // Make the first item required
218
+ * />
219
+ * )}
220
+ * </ArrayInput>
221
+ * ```
222
+ *
223
+ * @see {@link https://react-hook-form.com/api/usefieldarray} for more details on `useFieldArray`.
224
+ * @see {@link https://react-dnd.github.io/react-dnd/about} for more details on DnD context and sorting.
225
+ */
226
+ export function ArrayInput<T extends AnyObject = string>({
123
227
  name,
124
228
  children,
125
229
  addLabel,
126
- fixedList,
127
230
  focusName = "",
231
+ buttonPosition = "bottom",
128
232
  onAdd,
233
+ onRemove,
234
+ onBeforeRemove,
235
+ canRemove,
129
236
  arrayRef,
130
237
  enableSorting,
131
238
  disabled = false,
132
239
  readOnly = false,
133
240
  minItems = 0,
241
+ maxItems = Number.MAX_SAFE_INTEGER,
134
242
  ...rest
135
- }: ArrayInputProps) => {
243
+ }: ArrayInputProps<T>) {
136
244
  const { t } = useTranslation("form");
137
245
  const form = useFormContext();
138
246
 
139
- if (!form?.control)
140
- throw new Error("ArrayInput must be contained within a Form element");
247
+ if (!form?.control) throw new Error("ArrayInput must be contained within a Form element");
141
248
 
142
249
  const { fields, append, remove, move } = useFieldArray({
143
250
  name,
251
+ keyName: "__ID__",
144
252
  control: form.control,
145
253
  });
146
254
 
147
- const error = useMemo<AnyObject>(
148
- () =>
149
- form.formState.errors[name]?.message ??
150
- form.formState.errors[name]?.root?.message,
151
- [form.formState.errors],
255
+ const [initialList, setInitialList] = useState<string[]>(() => []);
256
+ useEffect(() => {
257
+ // eslint-disable-next-line @eslint-react/set-state-in-effect
258
+ setInitialList(fields.map((f) => f.__ID__));
259
+ }, [fields]);
260
+
261
+ const error = useMemo(
262
+ () => form.formState.errors[name]?.message ?? form.formState.errors[name]?.root?.message,
263
+ [form.formState.errors, name],
264
+ );
265
+
266
+ const removeCheck = useCallback(
267
+ (item: T, index: number) => {
268
+ if (canRemove === false) return false;
269
+ if (canRemove === "newonly") {
270
+ return !initialList.includes((item as KeyValue).__ID__ as string);
271
+ }
272
+ if (typeof canRemove === "function") {
273
+ return canRemove({
274
+ item,
275
+ index,
276
+ lastItem: index + 1 === fields.length,
277
+ });
278
+ }
279
+ return true;
280
+ },
281
+ [canRemove, initialList, fields],
152
282
  );
153
283
 
154
284
  const handleAdd = useCallback(
155
- (item: AnyObject) => {
156
- append(item);
285
+ async (item?: Promise<false | undefined | T>) => {
286
+ if (!item) return;
287
+ append(await item);
157
288
  setTimeout(() => {
158
- form.setFocus(
159
- `${name}.${fields.length}${focusName ? "." + focusName : ""}`,
160
- );
289
+ form.setFocus(`${name}.${fields.length}${focusName ? "." + focusName : ""}`);
161
290
  }, 100);
162
291
  },
163
- [name, fields, focusName],
292
+ [append, form, name, fields.length, focusName],
293
+ );
294
+
295
+ const handleRemove = useCallback(
296
+ (item: T, index: number) => {
297
+ const ret = onBeforeRemove?.(item, index);
298
+ void Promise.resolve(ret).then((b) => {
299
+ if (!isFalse(b)) {
300
+ remove(index);
301
+ onRemove?.(item, index);
302
+ }
303
+ });
304
+ },
305
+ [remove, onRemove, onBeforeRemove],
164
306
  );
165
307
 
166
308
  useImperativeHandle(arrayRef, () => ({
167
- addItem: handleAdd,
309
+ addItem: (t) => handleAdd(Promise.resolve(t)),
168
310
  removeItem: remove,
169
311
  }));
170
312
 
171
- const Wrapper = useCallback(
172
- ({ children }: PropsWithChildren) => {
173
- if (enableSorting) {
174
- // pass id list to dnd context
175
- const idMap = fields.map((item) => item.id);
176
- const handleDragEnd = (e: DragEndEvent) => {
177
- if (e.over && e.active.id !== e.over.id) {
178
- // find index of item and drop over
179
- const active = idMap.indexOf(`${e.active.id}`);
180
- const end = idMap.indexOf(`${e.over.id}`);
181
- move(active, end);
313
+ const addButton = useMemo(
314
+ () => (
315
+ <div className="flex justify-end">
316
+ <Tooltip
317
+ color="danger"
318
+ content={
319
+ isObject<ErrorObject>(error)
320
+ ? t(
321
+ error.key,
322
+ isObject(error.values)
323
+ ? {
324
+ ...error.values,
325
+ label: t(error.values.label ?? "form:badkey", error.values.path),
326
+ }
327
+ : {},
328
+ )
329
+ : isString(error)
330
+ ? error
331
+ : ""
182
332
  }
183
- };
184
- return (
185
- <DndContext
186
- onDragEnd={handleDragEnd}
187
- modifiers={[restrictToVerticalAxis]}
333
+ >
334
+ <Button
335
+ size="sm"
336
+ aria-label="Add item"
337
+ icon="icon-[mdi--plus-circle-outline]"
338
+ onClick={() => handleAdd(Promise.resolve(onAdd?.()))}
339
+ data-invalid={!!error}
340
+ disabled={!!disabled || readOnly || fields.length > maxItems}
341
+ className={cn("fabric-addButton", canRemove !== false && "me-10")}
188
342
  >
189
- <SortableContext items={idMap}>{children}</SortableContext>
190
- </DndContext>
191
- );
192
- }
193
-
194
- return children;
195
- },
196
- [enableSorting, fields],
197
- );
198
- // @ts-expect-error ignore
199
- Wrapper.displayName = "SortableWrapper";
200
-
201
- const SortableItem = useCallback(
202
- ({
203
- id,
204
- children,
205
- className,
206
- }: PropsWithChildren<{ id: AnyObject; className: string }>) => {
207
- if (enableSorting) {
208
- const {
209
- attributes,
210
- listeners,
211
- setNodeRef,
212
- transform,
213
- transition,
214
- isDragging,
215
- } = useSortable({ id });
216
- return (
217
- <div
218
- className={className}
219
- ref={setNodeRef}
220
- style={{
221
- transform: CSS.Transform.toString(transform),
222
- transition,
223
- }}
224
- >
225
- <Icon
226
- className={isDragging ? "cursor-grabbing" : "cursor-grab"}
227
- icon={CoreIcons.dragHandle}
228
- {...attributes}
229
- {...listeners}
230
- />
231
- {children}
232
- </div>
233
- );
234
- }
235
-
236
- return <div className={className}>{children}</div>;
237
- },
238
- [enableSorting],
343
+ {addLabel ?? t("form:addArray")}
344
+ </Button>
345
+ </Tooltip>
346
+ </div>
347
+ ),
348
+ [error, t, disabled, readOnly, fields.length, maxItems, canRemove, addLabel, handleAdd, onAdd],
239
349
  );
240
- // @ts-expect-error ignore
241
- SortableItem.displayName = "SortableItem";
242
350
 
243
351
  return (
244
- <div className={classes.arrayInput}>
245
- <FieldWrapper {...rest}>
246
- <Wrapper>
352
+ <div className={"fabric-arrayInput"}>
353
+ <FieldWrapper {...rest} appendLabel={<div className="-me-2">{onAdd && buttonPosition !== "bottom" && addButton}</div>}>
354
+ <Wrapper onMove={move} enableSorting={enableSorting} fields={fields}>
247
355
  {fields.map((item, index) => (
248
356
  <SortableItem
249
- key={item.id}
250
- id={item.id}
357
+ key={item.__ID__}
358
+ id={item.__ID__}
359
+ index={index}
360
+ enableSorting={enableSorting}
251
361
  className="flex items-center flex-nowrap gap-2 mb-1"
252
362
  >
253
363
  {typeof children === "function"
254
364
  ? children({
255
365
  index,
366
+ item: item as T,
256
367
  name: `${name}.${index}`,
368
+ onChange: (val) => {
369
+ void Promise.resolve(val).then((result) => {
370
+ if (result) form.setValue(`${name}.${index}`, result);
371
+ });
372
+ },
257
373
  })
258
- : Children.map(children, (child: AnyObject) =>
259
- cloneElement(
260
- child,
261
- Object.assign(
262
- {
263
- disabled,
264
- readOnly,
265
- "data-inner": true,
266
- name: `${name}.${index}${
267
- child.props.name
268
- ? "." + (child.props.name as string)
269
- : ""
270
- }`,
271
- },
272
- index > 0 && {
273
- label: undefined,
274
- required: undefined,
275
- appendLabel: undefined,
276
- },
277
- ),
278
- ),
279
- )}
280
- {!fixedList && (
374
+ : cloneChildren(children)?.map((child) => {
375
+ if (isValidElement(child)) {
376
+ const childEl = child as React.ReactElement<{ name: string }>;
377
+ return cloneElement(
378
+ childEl,
379
+ Object.assign(
380
+ {
381
+ disabled,
382
+ readOnly,
383
+ "data-inner": true,
384
+ name: `${name}.${index}${childEl.props.name ? "." + childEl.props.name : ""}`,
385
+ },
386
+ index > 0 && {
387
+ label: undefined,
388
+ required: undefined,
389
+ appendLabel: undefined,
390
+ },
391
+ ),
392
+ );
393
+ }
394
+ })}
395
+ {canRemove !== false && (
281
396
  <Button
282
397
  aria-label="Remove item"
283
- icon={CoreIcons.remove}
398
+ icon="icon-[mdi--minus-circle-outline]"
284
399
  color="danger"
285
400
  variant="link"
286
401
  className="self-end"
287
- disabled={disabled || readOnly || fields.length <= minItems}
288
- onClick={() => remove(index)}
402
+ disabled={disabled || readOnly || fields.length <= minItems || !removeCheck(item as T, index)}
403
+ onClick={() => handleRemove(item as T, index)}
289
404
  />
290
405
  )}
291
406
  </SortableItem>
292
407
  ))}
293
408
  </Wrapper>
294
409
  </FieldWrapper>
295
- {!fixedList && onAdd && (
296
- <div className="flex justify-end">
297
- <Tooltip
298
- color="danger"
299
- content={
300
- isString(error)
301
- ? `${error}`
302
- : (t(
303
- error?.key,
304
- error?.values
305
- ? {
306
- ...error?.values,
307
- label: t(
308
- error.values?.label ?? "form:badkey",
309
- error.values?.path,
310
- ),
311
- }
312
- : {},
313
- ) as string)
314
- }
315
- >
316
- <Button
317
- aria-label="Add item"
318
- icon={CoreIcons.insert}
319
- onClick={() => handleAdd(onAdd())}
320
- data-invalid={!!error}
321
- disabled={!!disabled || readOnly}
322
- className={classNames(classes.addButton, "me-10")}
323
- >
324
- {addLabel ?? t("addArray")}
325
- </Button>
326
- </Tooltip>
327
- </div>
328
- )}
410
+ {onAdd && buttonPosition !== "top" && addButton}
329
411
  </div>
330
412
  );
331
- };
413
+ }