This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import type { InternalNamePath } from '../interface';
interface KV<T> {
key: InternalNamePath;
value: T;
}
/**
* NameMap like a `Map` but accepts `string[]` as key.
*/
declare class NameMap<T> {
private kvs;
set(key: InternalNamePath, value: T): void;
get(key: InternalNamePath): T;
getAsPrefix(key: InternalNamePath): T[];
update(key: InternalNamePath, updater: (origin: T) => T | null): void;
delete(key: InternalNamePath): void;
map<U>(callback: (kv: KV<T>) => U): U[];
toJSON(): Record<string, T>;
}
export default NameMap;
+76
View File
@@ -0,0 +1,76 @@
const SPLIT = '__@field_split__';
/**
* Convert name path into string to fast the fetch speed of Map.
*/
function normalize(namePath) {
return namePath.map(cell => `${typeof cell}:${cell}`)
// Magic split
.join(SPLIT);
}
/**
* NameMap like a `Map` but accepts `string[]` as key.
*/
class NameMap {
kvs = new Map();
set(key, value) {
this.kvs.set(normalize(key), value);
}
get(key) {
return this.kvs.get(normalize(key));
}
getAsPrefix(key) {
const normalizedKey = normalize(key);
const normalizedPrefix = normalizedKey + SPLIT;
const results = [];
const current = this.kvs.get(normalizedKey);
if (current !== undefined) {
results.push(current);
}
this.kvs.forEach((value, itemNormalizedKey) => {
if (itemNormalizedKey.startsWith(normalizedPrefix)) {
results.push(value);
}
});
return results;
}
update(key, updater) {
const origin = this.get(key);
const next = updater(origin);
if (!next) {
this.delete(key);
} else {
this.set(key, next);
}
}
delete(key) {
this.kvs.delete(normalize(key));
}
// Since we only use this in test, let simply realize this
map(callback) {
return [...this.kvs.entries()].map(([key, value]) => {
const cells = key.split(SPLIT);
return callback({
key: cells.map(cell => {
const [, type, unit] = cell.match(/^([^:]*):(.*)$/);
return type === 'number' ? Number(unit) : unit;
}),
value
});
});
}
toJSON() {
const json = {};
this.map(({
key,
value
}) => {
json[key.join('.')] = value;
return null;
});
return json;
}
}
export default NameMap;
@@ -0,0 +1,2 @@
import type { FieldError } from '../interface';
export declare function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>;
+26
View File
@@ -0,0 +1,26 @@
export function allPromiseFinish(promiseList) {
let hasError = false;
let count = promiseList.length;
const results = [];
if (!promiseList.length) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
promiseList.forEach((promise, index) => {
promise.catch(e => {
hasError = true;
return e;
}).then(result => {
count -= 1;
results[index] = result;
if (count > 0) {
return;
}
if (hasError) {
reject(results);
}
resolve(results);
});
});
});
}
@@ -0,0 +1 @@
export default function delayFrame(): Promise<void>;
+11
View File
@@ -0,0 +1,11 @@
import { macroTask } from "../hooks/useNotifyWatch";
import raf from "@rc-component/util/es/raf";
export default async function delayFrame() {
return new Promise(resolve => {
macroTask(() => {
raf(() => {
resolve();
});
});
});
}
+48
View File
@@ -0,0 +1,48 @@
export declare const defaultValidateMessages: {
default: string;
required: string;
enum: string;
whitespace: string;
date: {
format: string;
parse: string;
invalid: string;
};
types: {
string: string;
method: string;
array: string;
object: string;
number: string;
date: string;
boolean: string;
integer: string;
float: string;
regexp: string;
email: string;
tel: string;
url: string;
hex: string;
};
string: {
len: string;
min: string;
max: string;
range: string;
};
number: {
len: string;
min: string;
max: string;
range: string;
};
array: {
len: string;
min: string;
max: string;
range: string;
};
pattern: {
mismatch: string;
};
};
+49
View File
@@ -0,0 +1,49 @@
const typeTemplate = "'${name}' is not a valid ${type}";
export const defaultValidateMessages = {
default: "Validation error on field '${name}'",
required: "'${name}' is required",
enum: "'${name}' must be one of [${enum}]",
whitespace: "'${name}' cannot be empty",
date: {
format: "'${name}' is invalid for format date",
parse: "'${name}' could not be parsed as date",
invalid: "'${name}' is invalid date"
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
tel: typeTemplate,
url: typeTemplate,
hex: typeTemplate
},
string: {
len: "'${name}' must be exactly ${len} characters",
min: "'${name}' must be at least ${min} characters",
max: "'${name}' cannot be longer than ${max} characters",
range: "'${name}' must be between ${min} and ${max} characters"
},
number: {
len: "'${name}' must equal ${len}",
min: "'${name}' cannot be less than ${min}",
max: "'${name}' cannot be greater than ${max}",
range: "'${name}' must be between ${min} and ${max}"
},
array: {
len: "'${name}' must be exactly ${len} in length",
min: "'${name}' cannot be less than ${min} in length",
max: "'${name}' cannot be greater than ${max} in length",
range: "'${name}' must be between ${min} and ${max} in length"
},
pattern: {
mismatch: "'${name}' does not match pattern ${pattern}"
}
};
@@ -0,0 +1,3 @@
import type { FormInstance } from '../interface';
export declare function toArray<T>(value?: T | T[] | null): T[];
export declare function isFormInstance<T>(form: T | FormInstance): form is FormInstance;
+9
View File
@@ -0,0 +1,9 @@
export function toArray(value) {
if (value === undefined || value === null) {
return [];
}
return Array.isArray(value) ? value : [value];
}
export function isFormInstance(form) {
return form && !!form._init;
}
@@ -0,0 +1,6 @@
import type { InternalNamePath, InternalValidateOptions, RuleObject, StoreValue, RuleError } from '../interface';
/**
* We use `async-validator` to validate the value.
* But only check one value in a time to avoid namePath validate issue.
*/
export declare function validateRules(namePath: InternalNamePath, value: StoreValue, rules: RuleObject[], options: InternalValidateOptions, validateFirst: boolean | 'parallel', messageVariables?: Record<string, string>): Promise<RuleError[]>;
@@ -0,0 +1,227 @@
import RawAsyncValidator from '@rc-component/async-validator';
import * as React from 'react';
import warning from "@rc-component/util/es/warning";
import { defaultValidateMessages } from "./messages";
import { merge } from "@rc-component/util/es/utils/set";
// Remove incorrect original ts define
const AsyncValidator = RawAsyncValidator;
/**
* Replace with template.
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
*/
function replaceMessage(template, kv) {
return template.replace(/\\?\$\{\w+\}/g, str => {
if (str.startsWith('\\')) {
return str.slice(1);
}
const key = str.slice(2, -1);
return kv[key];
});
}
const CODE_LOGIC_ERROR = 'CODE_LOGIC_ERROR';
async function validateRule(name, value, rule, options, messageVariables) {
const cloneRule = {
...rule
};
// Bug of `async-validator`
// https://github.com/react-component/field-form/issues/316
// https://github.com/react-component/field-form/issues/313
delete cloneRule.ruleIndex;
// https://github.com/ant-design/ant-design/issues/40497#issuecomment-1422282378
AsyncValidator.warning = () => void 0;
if (cloneRule.validator) {
const originValidator = cloneRule.validator;
cloneRule.validator = (...args) => {
try {
return originValidator(...args);
} catch (error) {
console.error(error);
return Promise.reject(CODE_LOGIC_ERROR);
}
};
}
// We should special handle array validate
let subRuleField = null;
if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
subRuleField = cloneRule.defaultField;
delete cloneRule.defaultField;
}
const validator = new AsyncValidator({
[name]: [cloneRule]
});
const messages = merge(defaultValidateMessages, options.validateMessages);
validator.messages(messages);
let result = [];
try {
await Promise.resolve(validator.validate({
[name]: value
}, {
...options
}));
} catch (errObj) {
if (errObj.errors) {
result = errObj.errors.map(({
message
}, index) => {
const mergedMessage = message === CODE_LOGIC_ERROR ? messages.default : message;
return /*#__PURE__*/React.isValidElement(mergedMessage) ?
/*#__PURE__*/
// Wrap ReactNode with `key`
React.cloneElement(mergedMessage, {
key: `error_${index}`
}) : mergedMessage;
});
}
}
if (!result.length && subRuleField && Array.isArray(value) && value.length > 0) {
const subResults = await Promise.all(value.map((subValue, i) => validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables)));
return subResults.reduce((prev, errors) => [...prev, ...errors], []);
}
// Replace message with variables
const kv = {
...rule,
name,
enum: (rule.enum || []).join(', '),
...messageVariables
};
const fillVariableResult = result.map(error => {
if (typeof error === 'string') {
return replaceMessage(error, kv);
}
return error;
});
return fillVariableResult;
}
/**
* We use `async-validator` to validate the value.
* But only check one value in a time to avoid namePath validate issue.
*/
export function validateRules(namePath, value, rules, options, validateFirst, messageVariables) {
const name = namePath.join('.');
// Fill rule with context
const filledRules = rules.map((currentRule, ruleIndex) => {
const originValidatorFunc = currentRule.validator;
const cloneRule = {
...currentRule,
ruleIndex
};
// Replace validator if needed
if (originValidatorFunc) {
cloneRule.validator = (rule, val, callback) => {
let hasPromise = false;
// Wrap callback only accept when promise not provided
const wrappedCallback = (...args) => {
// Wait a tick to make sure return type is a promise
Promise.resolve().then(() => {
warning(!hasPromise, 'Your validator function has already return a promise. `callback` will be ignored.');
if (!hasPromise) {
callback(...args);
}
});
};
// Get promise
const promise = originValidatorFunc(rule, val, wrappedCallback);
hasPromise = promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
/**
* 1. Use promise as the first priority.
* 2. If promise not exist, use callback with warning instead
*/
warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
if (hasPromise) {
promise.then(() => {
callback();
}).catch(err => {
callback(err || ' ');
});
}
};
}
return cloneRule;
}).sort(({
warningOnly: w1,
ruleIndex: i1
}, {
warningOnly: w2,
ruleIndex: i2
}) => {
if (!!w1 === !!w2) {
// Let keep origin order
return i1 - i2;
}
if (w1) {
return 1;
}
return -1;
});
// Do validate rules
let summaryPromise;
if (validateFirst === true) {
// >>>>> Validate by serialization
summaryPromise = new Promise(async (resolve, reject) => {
/* eslint-disable no-await-in-loop */
for (let i = 0; i < filledRules.length; i += 1) {
const rule = filledRules[i];
const errors = await validateRule(name, value, rule, options, messageVariables);
if (errors.length) {
reject([{
errors,
rule
}]);
return;
}
}
/* eslint-enable */
resolve([]);
});
} else {
// >>>>> Validate by parallel
const rulePromises = filledRules.map(rule => validateRule(name, value, rule, options, messageVariables).then(errors => ({
errors,
rule
})));
summaryPromise = (validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)).then(errors => {
// Always change to rejection for Field to catch
return Promise.reject(errors);
});
}
// Internal catch error to avoid console error log.
summaryPromise.catch(e => e);
return summaryPromise;
}
async function finishOnAllFailed(rulePromises) {
return Promise.all(rulePromises).then(errorsList => {
const errors = [].concat(...errorsList);
return errors;
});
}
async function finishOnFirstFailed(rulePromises) {
let count = 0;
return new Promise(resolve => {
rulePromises.forEach(promise => {
promise.then(ruleError => {
if (ruleError.errors.length) {
resolve([ruleError]);
}
count += 1;
if (count === rulePromises.length) {
resolve([]);
}
});
});
});
}
@@ -0,0 +1,45 @@
import getValue from '@rc-component/util/lib/utils/get';
import setValue from '@rc-component/util/lib/utils/set';
import type { InternalNamePath, NamePath, Store, EventArgs } from '../interface';
export { getValue, setValue };
/**
* Convert name to internal supported format.
* This function should keep since we still thinking if need support like `a.b.c` format.
* 'a' => ['a']
* 123 => [123]
* ['a', 123] => ['a', 123]
*/
export declare function getNamePath(path: NamePath | null): InternalNamePath;
/**
* Create a new store object that contains only the values referenced by
* the provided list of name paths.
*/
export declare function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store;
/**
* Check if `namePathList` includes `namePath`.
* @param namePathList A list of `InternalNamePath[]`
* @param namePath Compare `InternalNamePath`
* @param partialMatch True will make `[a, b]` match `[a, b, c]`
*/
export declare function containsNamePath(namePathList: InternalNamePath[], namePath: InternalNamePath, partialMatch?: boolean): boolean;
/**
* Check if `namePath` is super set or equal of `subNamePath`.
* @param namePath A list of `InternalNamePath[]`
* @param subNamePath Compare `InternalNamePath`
* @param partialMatch Default false. True will make `[a, b]` match `[a, b, c]`
*/
export declare function matchNamePath(namePath: InternalNamePath, subNamePath: InternalNamePath | null, partialMatch?: boolean): boolean;
type SimilarObject = string | number | object;
export declare function isSimilar(source: SimilarObject, target: SimilarObject): boolean;
export declare function defaultGetValueFromEvent(valuePropName: string, ...args: EventArgs): any;
/**
* Moves an array item from one position in an array to another.
*
* Note: This is a pure function so a new array will be returned, instead
* of altering the array argument.
*
* @param array Array in which to move an item. (required)
* @param moveIndex The index of the item to move. (required)
* @param toIndex The index to move item at moveIndex to. (required)
*/
export declare function move<T>(array: T[], moveIndex: number, toIndex: number): T[];
+116
View File
@@ -0,0 +1,116 @@
import getValue from "@rc-component/util/es/utils/get";
import setValue from "@rc-component/util/es/utils/set";
import { toArray } from "./typeUtil";
export { getValue, setValue };
/**
* Convert name to internal supported format.
* This function should keep since we still thinking if need support like `a.b.c` format.
* 'a' => ['a']
* 123 => [123]
* ['a', 123] => ['a', 123]
*/
export function getNamePath(path) {
return toArray(path);
}
/**
* Create a new store object that contains only the values referenced by
* the provided list of name paths.
*/
export function cloneByNamePathList(store, namePathList) {
let newStore = {};
namePathList.forEach(namePath => {
const value = getValue(store, namePath);
newStore = setValue(newStore, namePath, value);
});
return newStore;
}
/**
* Check if `namePathList` includes `namePath`.
* @param namePathList A list of `InternalNamePath[]`
* @param namePath Compare `InternalNamePath`
* @param partialMatch True will make `[a, b]` match `[a, b, c]`
*/
export function containsNamePath(namePathList, namePath, partialMatch = false) {
return namePathList && namePathList.some(path => matchNamePath(namePath, path, partialMatch));
}
/**
* Check if `namePath` is super set or equal of `subNamePath`.
* @param namePath A list of `InternalNamePath[]`
* @param subNamePath Compare `InternalNamePath`
* @param partialMatch Default false. True will make `[a, b]` match `[a, b, c]`
*/
export function matchNamePath(namePath, subNamePath, partialMatch = false) {
if (!namePath || !subNamePath) {
return false;
}
if (!partialMatch && namePath.length !== subNamePath.length) {
return false;
}
return subNamePath.every((nameUnit, i) => namePath[i] === nameUnit);
}
// Like `shallowEqual`, but we not check the data which may cause re-render
export function isSimilar(source, target) {
if (source === target) {
return true;
}
if (!source && target || source && !target) {
return false;
}
if (!source || !target || typeof source !== 'object' || typeof target !== 'object') {
return false;
}
const sourceKeys = Object.keys(source);
const targetKeys = Object.keys(target);
const keys = new Set([...sourceKeys, ...targetKeys]);
return [...keys].every(key => {
const sourceValue = source[key];
const targetValue = target[key];
if (typeof sourceValue === 'function' && typeof targetValue === 'function') {
return true;
}
return sourceValue === targetValue;
});
}
export function defaultGetValueFromEvent(valuePropName, ...args) {
const event = args[0];
if (event && event.target && typeof event.target === 'object' && valuePropName in event.target) {
return event.target[valuePropName];
}
return event;
}
/**
* Moves an array item from one position in an array to another.
*
* Note: This is a pure function so a new array will be returned, instead
* of altering the array argument.
*
* @param array Array in which to move an item. (required)
* @param moveIndex The index of the item to move. (required)
* @param toIndex The index to move item at moveIndex to. (required)
*/
export function move(array, moveIndex, toIndex) {
const {
length
} = array;
if (moveIndex < 0 || moveIndex >= length || toIndex < 0 || toIndex >= length) {
return array;
}
const item = array[moveIndex];
const diff = moveIndex - toIndex;
if (diff > 0) {
// move left
return [...array.slice(0, toIndex), item, ...array.slice(toIndex, moveIndex), ...array.slice(moveIndex + 1, length)];
}
if (diff < 0) {
// move right
return [...array.slice(0, moveIndex), ...array.slice(moveIndex + 1, toIndex + 1), item, ...array.slice(toIndex + 1, length)];
}
return array;
}