1
This commit is contained in:
+19
@@ -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
@@ -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;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import type { FieldError } from '../interface';
|
||||
export declare function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>;
|
||||
+26
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function delayFrame(): Promise<void>;
|
||||
+11
@@ -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
@@ -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
@@ -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}"
|
||||
}
|
||||
};
|
||||
+3
@@ -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
@@ -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;
|
||||
}
|
||||
+6
@@ -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[]>;
|
||||
+227
@@ -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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+45
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user