From 110143d91b8e896a88664f21209a285db5fb6542 Mon Sep 17 00:00:00 2001 From: sealday Date: Tue, 9 Apr 2024 02:36:51 +0800 Subject: [PATCH] refactor: migrate formily internal methods into @nocobase/schema --- packages/core/schema/src/index.ts | 249 +++++++++++++++++++++++++++++- 1 file changed, 247 insertions(+), 2 deletions(-) diff --git a/packages/core/schema/src/index.ts b/packages/core/schema/src/index.ts index e18d98698..e14cb0e25 100644 --- a/packages/core/schema/src/index.ts +++ b/packages/core/schema/src/index.ts @@ -1,6 +1,21 @@ +import { + ArrayField, + FormPathPattern, + GeneralField, + IExchangeArrayStateProps, + INodePatch, + ISpliceArrayStateProps, + LifeCycleTypes, + isArrayField, + isDataField, + isVoidField, +} from '@formily/core'; +import { batch } from '@formily/reactive'; +import { unstable_useCompatEffect } from '@formily/reactive-react'; +import { FormPath, each, isNumberLike } from '@formily/shared'; + export * from '@formily/core'; export type { ArrayField, Field, FormPathPattern, GeneralField, ObjectField } from '@formily/core'; -export { exchangeArrayState, spliceArrayState } from '@formily/core/esm/shared/internals'; export * from '@formily/grid'; export * from '@formily/json-schema'; export { @@ -35,8 +50,238 @@ export { useForm, useFormEffects, } from '@formily/react'; -export { useAttach } from '@formily/react/esm/hooks/useAttach'; export * from '@formily/reactive'; export * from '@formily/reactive-react'; export * from '@formily/shared'; export * from '@formily/validator'; +interface IRecycleTarget { + onMount: () => void; + onUnmount: () => void; +} + +export const useAttach = (target: T): T => { + unstable_useCompatEffect(() => { + target.onMount(); + return () => target.onUnmount(); + }, [target]); + return target; +}; + +export const NumberIndexReg = /^\.(\d+)/; +export const exchangeArrayState = (field: ArrayField, props: IExchangeArrayStateProps) => { + const { fromIndex, toIndex } = { + fromIndex: 0, + toIndex: 0, + ...props, + }; + const address = field.address.toString(); + const fields = field.form.fields; + const addrLength = address.length; + const fieldPatches: INodePatch[] = []; + const isArrayChildren = (identifier: string) => { + return identifier.indexOf(address) === 0 && identifier.length > addrLength; + }; + + const isDown = fromIndex < toIndex; + + const isMoveNode = (identifier: string) => { + const afterStr = identifier.slice(address.length); + const number = afterStr.match(NumberIndexReg)?.[1]; + if (number === undefined) return false; + const index = Number(number); + return isDown ? index > fromIndex && index <= toIndex : index < fromIndex && index >= toIndex; + }; + + const isFromNode = (identifier: string) => { + const afterStr = identifier.substring(addrLength); + const number = afterStr.match(NumberIndexReg)?.[1]; + if (number === undefined) return false; + const index = Number(number); + return index === fromIndex; + }; + + const moveIndex = (identifier: string) => { + const preStr = identifier.substring(0, addrLength); + const afterStr = identifier.substring(addrLength); + const number = afterStr.match(NumberIndexReg)[1]; + const current = Number(number); + let index = current; + if (index === fromIndex) { + index = toIndex; + } else { + index += isDown ? -1 : 1; + } + + return `${preStr}${afterStr.replace(/^\.\d+/, `.${index}`)}`; + }; + + batch(() => { + each(fields, (field, identifier) => { + if (isArrayChildren(identifier)) { + if (isMoveNode(identifier) || isFromNode(identifier)) { + const newIdentifier = moveIndex(identifier); + fieldPatches.push({ + type: 'update', + address: newIdentifier, + oldAddress: identifier, + payload: field, + }); + if (!fields[newIdentifier]) { + fieldPatches.push({ + type: 'remove', + address: identifier, + }); + } + } + } + }); + patchFieldStates(fields, fieldPatches); + }); + field.form.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE); +}; + +export const patchFieldStates = (target: Record, patches: INodePatch[]) => { + patches.forEach(({ type, address, oldAddress, payload }) => { + if (type === 'remove') { + destroy(target, address, false); + } else if (type === 'update') { + if (payload) { + target[address] = payload; + if (target[oldAddress] === payload) { + target[oldAddress] = undefined; + } + } + if (address && payload) { + locateNode(payload, address); + } + } + }); +}; + +export const destroy = (target: Record, address: string, forceClear = true) => { + const field = target[address]; + field?.dispose(); + if (isDataField(field) && forceClear) { + const form = field.form; + const path = field.path; + form.deleteValuesIn(path); + form.deleteInitialValuesIn(path); + } + delete target[address]; +}; + +export const locateNode = (field: GeneralField, address: FormPathPattern) => { + field.address = FormPath.parse(address); + field.path = buildFieldPath(field); + field.form.indexes[field.path.toString()] = field.address.toString(); + return field; +}; + +export const buildFieldPath = (field: GeneralField) => { + return buildDataPath(field.form.fields, field.address); +}; + +export const buildDataPath = (fields: Record, pattern: FormPath) => { + let prevArray = false; + const segments = pattern.segments; + const path = segments.reduce((path: string[], key: string, index: number) => { + const currentPath = path.concat(key); + const currentAddress = segments.slice(0, index + 1); + const current = fields[currentAddress.join('.')]; + if (prevArray) { + if (!isVoidField(current)) { + prevArray = false; + } + return path; + } + if (index >= segments.length - 1) { + return currentPath; + } + if (isVoidField(current)) { + const parentAddress = segments.slice(0, index); + const parent = fields[parentAddress.join('.')]; + if (isArrayField(parent) && isNumberLike(key)) { + prevArray = true; + return currentPath; + } + return path; + } else { + prevArray = false; + } + return currentPath; + }, []); + return new FormPath(path); +}; + +export const spliceArrayState = (field: ArrayField, props?: ISpliceArrayStateProps) => { + const { startIndex, deleteCount, insertCount } = { + startIndex: 0, + deleteCount: 0, + insertCount: 0, + ...props, + }; + const address = field.address.toString(); + const addrLength = address.length; + const form = field.form; + const fields = form.fields; + const fieldPatches: INodePatch[] = []; + const offset = insertCount - deleteCount; + const isArrayChildren = (identifier: string) => { + return identifier.indexOf(address) === 0 && identifier.length > addrLength; + }; + const isAfterNode = (identifier: string) => { + const afterStr = identifier.substring(addrLength); + const number = afterStr.match(NumberIndexReg)?.[1]; + if (number === undefined) return false; + const index = Number(number); + return index > startIndex + deleteCount - 1; + }; + const isInsertNode = (identifier: string) => { + const afterStr = identifier.substring(addrLength); + const number = afterStr.match(NumberIndexReg)?.[1]; + if (number === undefined) return false; + const index = Number(number); + return index >= startIndex && index < startIndex + insertCount; + }; + const isDeleteNode = (identifier: string) => { + const preStr = identifier.substring(0, addrLength); + const afterStr = identifier.substring(addrLength); + const number = afterStr.match(NumberIndexReg)?.[1]; + if (number === undefined) return false; + const index = Number(number); + return ( + (index > startIndex && !fields[`${preStr}${afterStr.replace(/^\.\d+/, `.${index + deleteCount}`)}`]) || + index === startIndex + ); + }; + const moveIndex = (identifier: string) => { + if (offset === 0) return identifier; + const preStr = identifier.substring(0, addrLength); + const afterStr = identifier.substring(addrLength); + const number = afterStr.match(NumberIndexReg)?.[1]; + if (number === undefined) return identifier; + const index = Number(number) + offset; + return `${preStr}${afterStr.replace(/^\.\d+/, `.${index}`)}`; + }; + + batch(() => { + each(fields, (field, identifier) => { + if (isArrayChildren(identifier)) { + if (isAfterNode(identifier)) { + const newIdentifier = moveIndex(identifier); + fieldPatches.push({ + type: 'update', + address: newIdentifier, + oldAddress: identifier, + payload: field, + }); + } + if (isInsertNode(identifier) || isDeleteNode(identifier)) { + fieldPatches.push({ type: 'remove', address: identifier }); + } + } + }); + patchFieldStates(fields, fieldPatches); + }); + field.form.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE); +};