@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
323 lines (322 loc) • 11.2 kB
JavaScript
"use client";
import React, { useMemo, useRef, useEffect, useReducer, createRef, useContext, useCallback } from 'react';
import * as z from 'zod';
import classnames from 'classnames';
import pointer from "../../utils/json-pointer/index.js";
import { useFieldProps } from "../../hooks/index.js";
import { makeUniqueId } from "../../../../shared/component-helper.js";
import { Flex, FormStatus, HeightAnimation } from "../../../../components/index.js";
import { Span } from "../../../../elements/index.js";
import { pickSpacingProps } from "../../../../components/flex/utils.js";
import useMountEffect from "../../../../shared/helpers/useMountEffect.js";
import useUpdateEffect from "../../../../shared/helpers/useUpdateEffect.js";
import { pickFlexContainerProps } from "../../../../components/flex/Container.js";
import IterateItemContext from "../IterateItemContext.js";
import SummaryListContext from "../../Value/SummaryList/SummaryListContext.js";
import ValueBlockContext from "../../ValueBlock/ValueBlockContext.js";
import FieldBoundaryProvider from "../../DataContext/FieldBoundary/FieldBoundaryProvider.js";
import DataContext from "../../DataContext/Context.js";
import useDataValue from "../../hooks/useDataValue.js";
import { useArrayLimit, useItemPath, useSwitchContainerMode } from "../hooks/index.js";
import { getMessagesFromError } from "../../FieldBlock/index.js";
import { clearedArray } from "../../hooks/useFieldProps.js";
import { structuredClone } from "../../../../shared/helpers/structuredClone.js";
function ArrayComponent(props) {
const [salt, forceUpdate] = useReducer(() => ({}), {});
const {
path: pathProp,
itemPath: itemPathProp,
reverse,
countPath,
countPathTransform,
countPathLimit = Infinity,
omitSectionPath
} = props || {};
const dataContext = useContext(DataContext);
const summaryListContext = useContext(SummaryListContext);
const valueBlockContext = useContext(ValueBlockContext);
const {
absolutePath
} = useItemPath(itemPathProp);
const {
setLimitProps,
error: limitWarning
} = useArrayLimit(pathProp || absolutePath);
const {
getValueByPath
} = useDataValue();
const countValue = useMemo(() => {
if (!countPath) {
return -1;
}
let countValue = parseFloat(getValueByPath(countPath, dataContext.data));
if (!(countValue >= 0)) {
countValue = 0;
}
if (countValue > countPathLimit) {
countValue = countPathLimit;
}
return countValue;
}, [countPath, countPathLimit, getValueByPath, dataContext.data]);
const validateRequired = useCallback((value, {
emptyValue,
required,
error
}) => {
if (required && (!value || value?.length === 0 || value === emptyValue)) {
return error;
}
}, []);
const preparedProps = useMemo(() => {
const shared = {
schema: undefined,
emptyValue: undefined,
required: false,
validateRequired,
...props
};
if (typeof props.minItems === 'number' || typeof props.maxItems === 'number') {
shared.schema = p => {
let s = z.array(z.any());
if (typeof p.minItems === 'number') {
s = s.min(p.minItems, {
message: 'IterateArray.errorMinItems'
});
}
if (typeof p.maxItems === 'number') {
s = s.max(p.maxItems, {
message: 'IterateArray.errorMaxItems'
});
}
return s;
};
}
if (countPath) {
const arrayValue = getValueByPath(pathProp);
const newValue = [];
for (let i = 0, l = countValue; i < l; i++) {
const value = arrayValue?.[i];
newValue.push(countPathTransform ? countPathTransform({
value,
index: i
}) : value);
}
return {
...shared,
value: newValue
};
}
return {
...shared
};
}, [countPath, countPathTransform, countValue, getValueByPath, pathProp, props, validateRequired]);
const {
path,
itemPath,
value: arrayValue,
limit,
error,
withoutFlex,
placeholder,
containerMode,
animate,
handleChange,
setChanged,
onChange,
validateValue,
children
} = useFieldProps(preparedProps, {
updateContextDataInSync: true,
omitMultiplePathWarning: true,
forceUpdateWhenContextDataIsSet: Boolean(countPath),
omitSectionPath
});
const countValueRef = useRef();
useUpdateEffect(() => {
if (countPath) {
if (typeof countValueRef.current === 'number' && countValue !== countValueRef.current) {
window.requestAnimationFrame(() => {
dataContext.handlePathChange(path, getValueByPath(path));
});
}
countValueRef.current = countValue;
}
}, [countValue]);
const idsRef = useRef([]);
const isNewRef = useRef({});
const modesRef = useRef({});
const valueCountRef = useRef(arrayValue);
const arrayValueRef = useRef(arrayValue);
const containerRef = useRef();
const hadPushRef = useRef();
const innerRefs = useRef({});
const omitFlex = withoutFlex !== null && withoutFlex !== void 0 ? withoutFlex : summaryListContext || valueBlockContext;
const {
getNextContainerMode
} = useSwitchContainerMode();
useEffect(() => {
valueCountRef.current = arrayValue || [];
arrayValueRef.current = arrayValue || [];
}, [arrayValue]);
const arrayItems = useMemo(() => {
const list = Array.isArray(arrayValue) ? arrayValue : [];
const limitedList = typeof limit === 'number' ? list.slice(0, limit) : list;
const arrayItems = limitedList.map((value, index) => {
const id = idsRef.current[index] || makeUniqueId();
const hasNewItems = arrayValue?.length > valueCountRef.current?.length;
if (!idsRef.current[index]) {
isNewRef.current[id] = hasNewItems;
idsRef.current.push(id);
}
const isNew = isNewRef.current[id] || false;
if (!modesRef.current[id]?.current) {
var _getNextContainerMode;
modesRef.current[id] = {
current: containerMode !== null && containerMode !== void 0 ? containerMode : isNew ? (_getNextContainerMode = getNextContainerMode()) !== null && _getNextContainerMode !== void 0 ? _getNextContainerMode : 'edit' : 'auto'
};
}
const itemContext = {
id,
path,
itemPath,
value,
index,
arrayValue,
containerRef,
isNew,
containerMode: modesRef.current[id].current,
previousContainerMode: modesRef.current[id].previous,
initialContainerMode: containerMode || 'auto',
modeOptions: modesRef.current[id].options,
nestedIteratePath: absolutePath,
switchContainerMode: (mode, options = {}) => {
modesRef.current[id].previous = modesRef.current[id].current;
modesRef.current[id].current = mode;
modesRef.current[id].options = options;
delete isNewRef.current?.[id];
if (options?.preventUpdate !== true) {
forceUpdate();
}
},
handleChange: (path, value) => {
const newArrayValue = structuredClone(arrayValueRef.current || []);
newArrayValue[index] = {
...newArrayValue[index]
};
pointer.set(newArrayValue, path, value);
handleChange(newArrayValue);
},
handlePush: element => {
hadPushRef.current = true;
handleChange([...(arrayValueRef.current || []), element]);
},
handleRemove: ({
keepItems = false
} = {}) => {
if (keepItems) {
return;
}
itemContext.fulfillRemove();
},
fulfillRemove: () => {
const newArrayValue = structuredClone(arrayValueRef.current || []);
newArrayValue.splice(index, 1);
handleChange(newArrayValue.length === 0 ? clearedArray : newArrayValue);
delete modesRef.current?.[id];
delete isNewRef.current?.[id];
const findIndex = idsRef.current.indexOf(id);
idsRef.current.splice(findIndex, 1);
forceUpdate();
},
restoreOriginalValue: value => {
if (value) {
const newArrayValue = structuredClone(arrayValueRef.current || []);
newArrayValue[index] = value;
handleChange(newArrayValue);
}
}
};
return itemContext;
});
if (reverse) {
return arrayItems.reverse();
}
return arrayItems;
}, [salt, arrayValue, limit, path, itemPath, absolutePath, reverse, handleChange]);
const total = arrayItems.length;
useEffect(() => {
if (limit) {
setLimitProps({
limit,
total
});
}
}, [total, limit, setLimitProps]);
useUpdateEffect(() => {
validateValue();
}, [total, validateValue]);
useMountEffect(() => {
setChanged(true);
});
useMemo(() => {
const last = arrayItems?.[arrayItems.length - 1];
if (last?.isNew && !hadPushRef.current) {
onChange?.(arrayValue);
} else {
hadPushRef.current = false;
}
}, [arrayValue, arrayItems, onChange]);
const flexProps = {
className: classnames("dnb-forms-iterate dnb-forms-section", props?.className),
...pickFlexContainerProps(props),
...pickSpacingProps(props),
innerRef: containerRef
};
const arrayElements = total === 0 ? typeof placeholder === 'string' ? React.createElement(Span, {
size: "small"
}, placeholder) : placeholder : arrayItems.map(itemProps => {
const {
id,
value,
index
} = itemProps;
const elementRef = innerRefs.current[id] = innerRefs.current[id] || createRef();
const renderChildren = elementChild => {
return typeof elementChild === 'function' ? elementChild(value, index, arrayItems) : elementChild;
};
const contextValue = {
...itemProps,
elementRef
};
const content = Array.isArray(children) ? children.map(child => renderChildren(child)) : renderChildren(children);
if (omitFlex) {
return React.createElement(IterateItemContext.Provider, {
key: `element-${id}`,
value: contextValue
}, React.createElement(FieldBoundaryProvider, null, content));
}
return React.createElement(Flex.Item, {
className: "dnb-forms-iterate__element",
tabIndex: -1,
innerRef: elementRef,
key: `element-${id}`
}, React.createElement(IterateItemContext.Provider, {
value: contextValue
}, React.createElement(FieldBoundaryProvider, null, content)));
});
const content = omitFlex ? arrayElements : React.createElement(Flex.Stack, flexProps, arrayElements);
return React.createElement(React.Fragment, null, animate ? React.createElement(HeightAnimation, null, content) : content, React.createElement(FormStatus, {
show: Boolean(error || limitWarning),
state: !error && limitWarning ? 'warning' : undefined,
shellSpace: {
top: 0,
bottom: 'medium'
},
no_animation: false
}, getMessagesFromError({
content: error || limitWarning
})[0]));
}
ArrayComponent._supportsSpacingProps = true;
export default ArrayComponent;
//# sourceMappingURL=Array.js.map