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
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-present react-component
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+114
View File
@@ -0,0 +1,114 @@
# @rc-component/upload
React Upload
[![NPM version][npm-image]][npm-url] [![npm download][download-image]][download-url] [![build status][github-actions-image]][github-actions-url] [![Codecov][codecov-image]][codecov-url] [![bundle size][bundlephobia-image]][bundlephobia-url] [![dumi][dumi-image]][dumi-url]
[npm-image]: http://img.shields.io/npm/v/@rc-component/upload.svg?style=flat-square
[npm-url]: http://npmjs.org/package/@rc-component/upload
[travis-image]: https://img.shields.io/travis/react-component/upload/master?style=flat-square
[travis-url]: https://travis-ci.com/react-component/upload
[github-actions-image]: https://github.com/react-component/upload/actions/workflows/react-component-ci.yml/badge.svg
[github-actions-url]: https://github.com/react-component/upload/actions/workflows/react-component-ci.yml
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/upload/master.svg?style=flat-square
[codecov-url]: https://app.codecov.io/gh/react-component/upload
[download-image]: https://img.shields.io/npm/dm/@rc-component/upload.svg?style=flat-square
[download-url]: https://npmjs.org/package/@rc-component/upload
[bundlephobia-url]: https://bundlephobia.com/package/@rc-component/upload
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/upload
[dumi-url]: https://github.com/umijs/dumi
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
## Development
```
npm install
npm start
```
## Example
http://localhost:8000/
online example: https://upload.react-component.vercel.app/
## Feature
- support IE11+, Chrome, Firefox, Safari
## install
[![@rc-component/upload](https://nodei.co/npm/@rc-component/upload.png)](https://npmjs.org/package/@rc-component/upload)
## Usage
```js
var Upload = require('@rc-component/upload');
var React = require('react');
React.render(<Upload />, container);
```
## API
### props
| name | type | default | description |
| --- | --- | --- | --- |
| name | string | file | file param post to server |
| style | object | {} | root component inline style |
| className | string | - | root component className |
| disabled | boolean | false | whether disabled |
| component | "div" \| "span" | "span" | wrap component name |
| action | string &#124; function(file): string &#124; Promise&lt;string&gt; | | form action url |
| method | string | post | request method |
| directory | boolean | false | support upload whole directory |
| data | object/function(file) | | other data object to post or a function which returns a data object(a promise object which resolve a data object) |
| headers | object | {} | http headers to post, available in modern browsers |
| accept | string | | input accept attribute |
| capture | string | | input capture attribute |
| multiple | boolean | false | only support ie10+ |
| onStart | function | | start upload file |
| onError | function | | error callback |
| onSuccess | function | | success callback |
| onProgress | function | | progress callback, only for modern browsers |
| beforeUpload | function | null | before upload check, return false or a rejected Promise will stop upload, only for modern browsers |
| customRequest | function | null | provide an override for the default xhr behavior for additional customization |
| withCredentials | boolean | false | ajax upload with cookie send |
| openFileDialogOnClick | boolean | true | useful for drag only upload as it does not trigger on enter key or click event |
| pastable | boolean | false | support paste upload |
#### onError arguments
1. `err`: request error message
2. `response`: request response, not support on iframeUpload
3. `file`: upload file
### onSuccess arguments
1. `result`: response body
2. `file`: upload file
3. `xhr`: xhr header, only for modern browsers which support AJAX upload. since 2.4.0
### customRequest
Allows for advanced customization by overriding default behavior in AjaxUploader. Provide your own XMLHttpRequest calls to interface with custom backend processes or interact with AWS S3 service through the aws-sdk-js package.
customRequest callback is passed an object with:
- `onProgress: (event: { percent: number }): void`
- `onError: (event: Error, body?: Object): void`
- `onSuccess: (body: Object): void`
- `data: Object`
- `filename: String`
- `file: File`
- `withCredentials: Boolean`
- `action: String`
- `headers: Object`
### methods
abort(file?: File) => void: abort the uploading file
## License
@rc-component/upload is released under the MIT license.
+38
View File
@@ -0,0 +1,38 @@
import React, { Component } from 'react';
import type { RcFile, UploadProps } from './interface';
interface ParsedFileInfo {
origin: RcFile;
action: string;
data: Record<string, unknown>;
parsedFile: RcFile;
}
declare class AjaxUploader extends Component<UploadProps> {
state: {
uid: string;
};
reqs: Record<string, any>;
private fileInput;
private _isMounted;
private filterFile;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClick: (event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
onDataTransferFiles: (dataTransfer: DataTransfer, existFileCallback?: () => void) => Promise<void>;
onFilePaste: (e: ClipboardEvent) => Promise<void>;
onFileDragOver: (e: React.DragEvent<HTMLDivElement>) => void;
onFileDrop: (e: React.DragEvent<HTMLDivElement>) => Promise<void>;
componentDidMount(): void;
componentWillUnmount(): void;
componentDidUpdate(prevProps: UploadProps): void;
uploadFiles: (files: File[]) => void;
/**
* Process file before upload. When all the file is ready, we start upload.
*/
processFile: (file: RcFile, fileList: RcFile[]) => Promise<ParsedFileInfo>;
post({ data, origin, action, parsedFile }: ParsedFileInfo): void;
reset(): void;
abort(file?: any): void;
saveFileInput: (node: HTMLInputElement) => void;
render(): React.JSX.Element;
}
export default AjaxUploader;
+394
View File
@@ -0,0 +1,394 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */
import pickAttrs from "@rc-component/util/es/pickAttrs";
import { clsx } from 'clsx';
import React, { Component } from 'react';
import attrAccept from "./attr-accept";
import defaultRequest from "./request";
import traverseFileTree from "./traverseFileTree";
import getUid from "./uid";
class AjaxUploader extends Component {
state = {
uid: getUid()
};
reqs = {};
fileInput;
_isMounted;
filterFile = (file, force = false) => {
const {
accept,
directory
} = this.props;
let filterFn;
let acceptFormat;
if (typeof accept === 'string') {
acceptFormat = accept;
} else {
const {
filter,
format
} = accept || {};
acceptFormat = format;
if (filter === 'native') {
filterFn = () => true;
} else {
filterFn = filter;
}
}
const mergedFilter = filterFn || (directory || force ? currentFile => attrAccept(currentFile, acceptFormat) : () => true);
return mergedFilter(file);
};
onChange = e => {
const {
files
} = e.target;
const acceptedFiles = [...files].filter(file => this.filterFile(file));
this.uploadFiles(acceptedFiles);
this.reset();
};
onClick = event => {
const el = this.fileInput;
if (!el) {
return;
}
const target = event.target;
const {
onClick
} = this.props;
if (target && target.tagName === 'BUTTON') {
const parent = el.parentNode;
parent.focus();
target.blur();
}
el.click();
if (onClick) {
onClick(event);
}
};
onKeyDown = e => {
if (e.key === 'Enter') {
this.onClick(e);
}
};
onDataTransferFiles = async (dataTransfer, existFileCallback) => {
const {
multiple,
directory
} = this.props;
const items = [...(dataTransfer.items || [])];
let files = [...(dataTransfer.files || [])];
if (files.length > 0 || items.some(item => item.kind === 'file')) {
existFileCallback?.();
}
if (directory) {
files = await traverseFileTree(Array.prototype.slice.call(items), this.filterFile);
this.uploadFiles(files);
} else {
let acceptFiles = [...files].filter(file => this.filterFile(file, true));
if (multiple === false) {
acceptFiles = files.slice(0, 1);
}
this.uploadFiles(acceptFiles);
}
};
onFilePaste = async e => {
const {
pastable
} = this.props;
if (!pastable) {
return;
}
if (e.type === 'paste') {
const clipboardData = e.clipboardData;
return this.onDataTransferFiles(clipboardData, () => {
e.preventDefault();
});
}
};
onFileDragOver = e => {
e.preventDefault();
};
onFileDrop = async e => {
e.preventDefault();
if (e.type === 'drop') {
const dataTransfer = e.dataTransfer;
return this.onDataTransferFiles(dataTransfer);
}
};
componentDidMount() {
this._isMounted = true;
const {
pastable
} = this.props;
if (pastable) {
document.addEventListener('paste', this.onFilePaste);
}
}
componentWillUnmount() {
this._isMounted = false;
this.abort();
document.removeEventListener('paste', this.onFilePaste);
}
componentDidUpdate(prevProps) {
const {
pastable
} = this.props;
if (pastable && !prevProps.pastable) {
document.addEventListener('paste', this.onFilePaste);
} else if (!pastable && prevProps.pastable) {
document.removeEventListener('paste', this.onFilePaste);
}
}
uploadFiles = files => {
const originFiles = [...files];
const postFiles = originFiles.map(file => {
// eslint-disable-next-line no-param-reassign
file.uid = getUid();
return this.processFile(file, originFiles);
});
// Batch upload files
Promise.all(postFiles).then(fileList => {
const {
onBatchStart
} = this.props;
onBatchStart?.(fileList.map(({
origin,
parsedFile
}) => ({
file: origin,
parsedFile
})));
fileList.filter(file => file.parsedFile !== null).forEach(file => {
this.post(file);
});
});
};
/**
* Process file before upload. When all the file is ready, we start upload.
*/
processFile = async (file, fileList) => {
const {
beforeUpload
} = this.props;
let transformedFile = file;
if (beforeUpload) {
try {
transformedFile = await beforeUpload(file, fileList);
} catch (e) {
// Rejection will also trade as false
transformedFile = false;
}
if (transformedFile === false) {
return {
origin: file,
parsedFile: null,
action: null,
data: null
};
}
}
// Get latest action
const {
action
} = this.props;
let mergedAction;
if (typeof action === 'function') {
mergedAction = await action(file);
} else {
mergedAction = action;
}
// Get latest data
const {
data
} = this.props;
let mergedData;
if (typeof data === 'function') {
mergedData = await data(file);
} else {
mergedData = data;
}
const parsedData =
// string type is from legacy `transformFile`.
// Not sure if this will work since no related test case works with it
(typeof transformedFile === 'object' || typeof transformedFile === 'string') && transformedFile ? transformedFile : file;
let parsedFile;
if (parsedData instanceof File) {
parsedFile = parsedData;
} else {
parsedFile = new File([parsedData], file.name, {
type: file.type
});
}
const mergedParsedFile = parsedFile;
mergedParsedFile.uid = file.uid;
return {
origin: file,
data: mergedData,
parsedFile: mergedParsedFile,
action: mergedAction
};
};
post({
data,
origin,
action,
parsedFile
}) {
if (!this._isMounted) {
return;
}
const {
onStart,
customRequest,
name,
headers,
withCredentials,
method
} = this.props;
const {
uid
} = origin;
const request = customRequest || defaultRequest;
const requestOption = {
action,
filename: name,
data,
file: parsedFile,
headers,
withCredentials,
method: method || 'post',
onProgress: e => {
const {
onProgress
} = this.props;
onProgress?.(e, parsedFile);
},
onSuccess: (ret, xhr) => {
const {
onSuccess
} = this.props;
onSuccess?.(ret, parsedFile, xhr);
delete this.reqs[uid];
},
onError: (err, ret) => {
const {
onError
} = this.props;
onError?.(err, ret, parsedFile);
delete this.reqs[uid];
}
};
onStart(origin);
this.reqs[uid] = request(requestOption, {
defaultRequest
});
}
reset() {
this.setState({
uid: getUid()
});
}
abort(file) {
const {
reqs
} = this;
if (file) {
const uid = file.uid ? file.uid : file;
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
delete reqs[uid];
} else {
Object.keys(reqs).forEach(uid => {
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
delete reqs[uid];
});
}
}
saveFileInput = node => {
this.fileInput = node;
};
render() {
const {
component: Tag,
prefixCls,
className,
classNames = {},
disabled,
id,
name,
style,
styles = {},
multiple,
accept,
capture,
children,
directory,
openFileDialogOnClick,
onMouseEnter,
onMouseLeave,
hasControlInside,
...otherProps
} = this.props;
// Extract accept format for input element
const acceptFormat = typeof accept === 'string' ? accept : accept?.format;
const cls = clsx(prefixCls, {
[`${prefixCls}-disabled`]: disabled,
[className]: className
});
// because input don't have directory/webkitdirectory type declaration
const dirProps = directory ? {
directory: 'directory',
webkitdirectory: 'webkitdirectory'
} : {};
const events = disabled ? {} : {
onClick: openFileDialogOnClick ? this.onClick : () => {},
onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => {},
onMouseEnter,
onMouseLeave,
onDrop: this.onFileDrop,
onDragOver: this.onFileDragOver,
tabIndex: hasControlInside ? undefined : '0'
};
return /*#__PURE__*/React.createElement(Tag, _extends({}, events, {
className: cls,
role: hasControlInside ? undefined : 'button',
style: style
}), /*#__PURE__*/React.createElement("input", _extends({}, pickAttrs(otherProps, {
aria: true,
data: true
}), {
id: id
/**
* https://github.com/ant-design/ant-design/issues/50643,
* https://github.com/react-component/upload/pull/575#issuecomment-2320646552
*/,
name: name,
disabled: disabled,
type: "file",
ref: this.saveFileInput,
onClick: e => e.stopPropagation() // https://github.com/ant-design/ant-design/issues/19948
,
key: this.state.uid,
style: {
display: 'none',
...styles.input
},
className: classNames.input,
accept: acceptFormat
}, dirProps, {
multiple: multiple,
onChange: this.onChange
}, capture != null ? {
capture
} : {})), children);
}
}
export default AjaxUploader;
+28
View File
@@ -0,0 +1,28 @@
import React, { Component } from 'react';
import AjaxUpload from './AjaxUploader';
import type { UploadProps, RcFile } from './interface';
declare function empty(): void;
declare class Upload extends Component<UploadProps> {
static defaultProps: {
component: string;
prefixCls: string;
data: {};
headers: {};
name: string;
multipart: boolean;
onStart: typeof empty;
onError: typeof empty;
onSuccess: typeof empty;
multiple: boolean;
beforeUpload: any;
customRequest: any;
withCredentials: boolean;
openFileDialogOnClick: boolean;
hasControlInside: boolean;
};
private uploader;
abort(file: RcFile): void;
saveUploader: (node: AjaxUpload) => void;
render(): React.JSX.Element;
}
export default Upload;
+37
View File
@@ -0,0 +1,37 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* eslint react/prop-types:0 */
import React, { Component } from 'react';
import AjaxUpload from "./AjaxUploader";
function empty() {}
class Upload extends Component {
static defaultProps = {
component: 'span',
prefixCls: 'rc-upload',
data: {},
headers: {},
name: 'file',
multipart: false,
onStart: empty,
onError: empty,
onSuccess: empty,
multiple: false,
beforeUpload: null,
customRequest: null,
withCredentials: false,
openFileDialogOnClick: true,
hasControlInside: false
};
uploader;
abort(file) {
this.uploader.abort(file);
}
saveUploader = node => {
this.uploader = node;
};
render() {
return /*#__PURE__*/React.createElement(AjaxUpload, _extends({}, this.props, {
ref: this.saveUploader
}));
}
}
export default Upload;
@@ -0,0 +1,3 @@
import type { RcFile } from './interface';
declare const _default: (file: RcFile, acceptedFiles: string | string[]) => boolean;
export default _default;
+45
View File
@@ -0,0 +1,45 @@
import { warning } from '@rc-component/util';
export default ((file, acceptedFiles) => {
if (file && acceptedFiles) {
const acceptedFilesArray = Array.isArray(acceptedFiles) ? acceptedFiles : acceptedFiles.split(',');
const fileName = file.name || '';
const mimeType = file.type || '';
const baseMimeType = mimeType.replace(/\/.*$/, '');
return acceptedFilesArray.some(type => {
const validType = type.trim();
// This is something like */*,* allow all files
if (/^\*(\/\*)?$/.test(type)) {
return true;
}
// like .jpg, .png
if (validType.charAt(0) === '.') {
const lowerFileName = fileName.toLowerCase();
const lowerType = validType.toLowerCase();
let affixList = [lowerType];
if (lowerType === '.jpg' || lowerType === '.jpeg') {
affixList = ['.jpg', '.jpeg'];
}
return affixList.some(affix => lowerFileName.endsWith(affix));
}
// This is something like a image/* mime type
if (/\/\*$/.test(validType)) {
return baseMimeType === validType.replace(/\/.*$/, '');
}
// Full match
if (mimeType === validType) {
return true;
}
// Invalidate type should skip
if (/^\w+$/.test(validType)) {
warning(false, `Upload takes an invalidate 'accept' type '${validType}'.Skip for check.`);
return true;
}
return false;
});
}
return true;
});
+4
View File
@@ -0,0 +1,4 @@
import type { UploadProps } from './interface';
import Upload from './Upload';
export type { UploadProps };
export default Upload;
+2
View File
@@ -0,0 +1,2 @@
import Upload from "./Upload";
export default Upload;
+79
View File
@@ -0,0 +1,79 @@
import type * as React from 'react';
export type BeforeUploadFileType = File | Blob | boolean | string;
export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
export type AcceptConfig = {
format: string;
filter?: 'native' | ((file: RcFile) => boolean);
};
export interface UploadProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onError' | 'onProgress' | 'accept'> {
name?: string;
style?: React.CSSProperties;
className?: string;
disabled?: boolean;
component?: React.ComponentType<any> | string;
action?: Action;
method?: UploadRequestMethod;
directory?: boolean;
data?: Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>);
headers?: UploadRequestHeader;
accept?: string | AcceptConfig;
multiple?: boolean;
onBatchStart?: (fileList: {
file: RcFile;
parsedFile: Exclude<BeforeUploadFileType, boolean>;
}[]) => void;
onStart?: (file: RcFile) => void;
onError?: (error: Error, ret: Record<string, unknown>, file: RcFile) => void;
onSuccess?: (response: Record<string, unknown>, file: RcFile, xhr: XMLHttpRequest) => void;
onProgress?: (event: UploadProgressEvent, file: RcFile) => void;
beforeUpload?: (file: RcFile, FileList: RcFile[]) => BeforeUploadFileType | Promise<void | BeforeUploadFileType> | void;
customRequest?: CustomUploadRequestOption;
withCredentials?: boolean;
openFileDialogOnClick?: boolean;
prefixCls?: string;
id?: string;
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void;
classNames?: {
input?: string;
};
styles?: {
input?: React.CSSProperties;
};
hasControlInside?: boolean;
pastable?: boolean;
}
export interface UploadProgressEvent extends Partial<ProgressEvent> {
percent?: number;
}
export type UploadRequestMethod = 'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch';
export type UploadRequestHeader = Record<string, string>;
export type UploadRequestFile = Exclude<BeforeUploadFileType, File | boolean> | RcFile;
export interface UploadRequestError extends Error {
status?: number;
method?: UploadRequestMethod;
url?: string;
}
export interface UploadRequestOption<T = any> {
onProgress?: (event: UploadProgressEvent, file?: UploadRequestFile) => void;
onError?: (event: UploadRequestError | ProgressEvent, body?: T) => void;
onSuccess?: (body: T, fileOrXhr?: UploadRequestFile | XMLHttpRequest) => void;
data?: Record<string, unknown>;
filename?: string;
file: UploadRequestFile;
withCredentials?: boolean;
action: string;
headers?: UploadRequestHeader;
method: UploadRequestMethod;
}
export type CustomUploadRequestOption = (option: UploadRequestOption, info: {
defaultRequest: (option: UploadRequestOption) => {
abort: () => void;
} | void;
}) => void | {
abort: () => void;
};
export interface RcFile extends File {
uid: string;
}
+1
View File
@@ -0,0 +1 @@
export {};
+4
View File
@@ -0,0 +1,4 @@
import type { UploadRequestOption } from './interface';
export default function upload(option: UploadRequestOption): {
abort(): void;
};
+91
View File
@@ -0,0 +1,91 @@
function getError(option, xhr) {
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
const err = new Error(msg);
err.status = xhr.status;
err.method = option.method;
err.url = option.action;
return err;
}
function getBody(xhr) {
const text = xhr.responseText || xhr.response;
if (!text) {
return text;
}
try {
return JSON.parse(text);
} catch (e) {
return text;
}
}
export default function upload(option) {
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest();
if (option.onProgress && xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
option.onProgress(e);
};
}
// eslint-disable-next-line no-undef
const formData = new FormData();
if (option.data) {
Object.keys(option.data).forEach(key => {
const value = option.data[key];
// support key-value array data
if (Array.isArray(value)) {
value.forEach(item => {
// { list: [ 11, 22 ] }
// formData.append('list[]', 11);
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, value);
});
}
// eslint-disable-next-line no-undef
if (option.file instanceof Blob) {
formData.append(option.filename, option.file, option.file.name);
} else {
formData.append(option.filename, option.file);
}
xhr.onerror = function error(e) {
option.onError(e);
};
xhr.onload = function onload() {
// allow success when 2xx status
// see https://github.com/react-component/upload/issues/34
if (xhr.status < 200 || xhr.status >= 300) {
return option.onError(getError(option, xhr), getBody(xhr));
}
return option.onSuccess(getBody(xhr), xhr);
};
xhr.open(option.method, option.action, true);
// Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true;
}
const headers = option.headers || {};
// when set headers['X-Requested-With'] = null , can close default XHR header
// see https://github.com/react-component/upload/issues/33
if (headers['X-Requested-With'] !== null) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
Object.keys(headers).forEach(h => {
if (headers[h] !== null) {
xhr.setRequestHeader(h, headers[h]);
}
});
xhr.send(formData);
return {
abort() {
xhr.abort();
}
};
}
@@ -0,0 +1,14 @@
import type { RcFile } from './interface';
interface InternalDataTransferItem extends DataTransferItem {
isFile: boolean;
file: (cd: (file: RcFile & {
webkitRelativePath?: string;
}) => void) => void;
createReader: () => any;
fullPath: string;
isDirectory: boolean;
name: string;
path: string;
}
declare const traverseFileTree: (files: InternalDataTransferItem[], isAccepted: any) => Promise<any[]>;
export default traverseFileTree;
@@ -0,0 +1,74 @@
// https://github.com/ant-design/ant-design/issues/50080
const traverseFileTree = async (files, isAccepted) => {
const flattenFileList = [];
const progressFileList = [];
files.forEach(file => progressFileList.push(file.webkitGetAsEntry()));
async function readDirectory(directory) {
const dirReader = directory.createReader();
const entries = [];
while (true) {
const results = await new Promise(resolve => {
dirReader.readEntries(resolve, () => resolve([]));
});
const n = results.length;
if (!n) {
break;
}
for (let i = 0; i < n; i++) {
entries.push(results[i]);
}
}
return entries;
}
async function readFile(item) {
return new Promise(reslove => {
item.file(file => {
if (isAccepted(file)) {
// https://github.com/ant-design/ant-design/issues/16426
if (item.fullPath && !file.webkitRelativePath) {
Object.defineProperties(file, {
webkitRelativePath: {
writable: true
}
});
// eslint-disable-next-line no-param-reassign
file.webkitRelativePath = item.fullPath.replace(/^\//, '');
Object.defineProperties(file, {
webkitRelativePath: {
writable: false
}
});
}
reslove(file);
} else {
reslove(null);
}
});
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
const _traverseFileTree = async (item, path) => {
if (!item) {
return;
}
// eslint-disable-next-line no-param-reassign
item.path = path || '';
if (item.isFile) {
const file = await readFile(item);
if (file) {
flattenFileList.push(file);
}
} else if (item.isDirectory) {
const entries = await readDirectory(item);
progressFileList.push(...entries);
}
};
let wipIndex = 0;
while (wipIndex < progressFileList.length) {
await _traverseFileTree(progressFileList[wipIndex]);
wipIndex++;
}
return flattenFileList;
};
export default traverseFileTree;
+1
View File
@@ -0,0 +1 @@
export default function uid(): string;
+6
View File
@@ -0,0 +1,6 @@
const now = +new Date();
let index = 0;
export default function uid() {
// eslint-disable-next-line no-plusplus
return `rc-upload-${now}-${++index}`;
}
@@ -0,0 +1,38 @@
import React, { Component } from 'react';
import type { RcFile, UploadProps } from './interface';
interface ParsedFileInfo {
origin: RcFile;
action: string;
data: Record<string, unknown>;
parsedFile: RcFile;
}
declare class AjaxUploader extends Component<UploadProps> {
state: {
uid: string;
};
reqs: Record<string, any>;
private fileInput;
private _isMounted;
private filterFile;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClick: (event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
onDataTransferFiles: (dataTransfer: DataTransfer, existFileCallback?: () => void) => Promise<void>;
onFilePaste: (e: ClipboardEvent) => Promise<void>;
onFileDragOver: (e: React.DragEvent<HTMLDivElement>) => void;
onFileDrop: (e: React.DragEvent<HTMLDivElement>) => Promise<void>;
componentDidMount(): void;
componentWillUnmount(): void;
componentDidUpdate(prevProps: UploadProps): void;
uploadFiles: (files: File[]) => void;
/**
* Process file before upload. When all the file is ready, we start upload.
*/
processFile: (file: RcFile, fileList: RcFile[]) => Promise<ParsedFileInfo>;
post({ data, origin, action, parsedFile }: ParsedFileInfo): void;
reset(): void;
abort(file?: any): void;
saveFileInput: (node: HTMLInputElement) => void;
render(): React.JSX.Element;
}
export default AjaxUploader;
+402
View File
@@ -0,0 +1,402 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _pickAttrs = _interopRequireDefault(require("@rc-component/util/lib/pickAttrs"));
var _clsx = require("clsx");
var _react = _interopRequireWildcard(require("react"));
var _attrAccept = _interopRequireDefault(require("./attr-accept"));
var _request = _interopRequireDefault(require("./request"));
var _traverseFileTree = _interopRequireDefault(require("./traverseFileTree"));
var _uid = _interopRequireDefault(require("./uid"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */
class AjaxUploader extends _react.Component {
state = {
uid: (0, _uid.default)()
};
reqs = {};
fileInput;
_isMounted;
filterFile = (file, force = false) => {
const {
accept,
directory
} = this.props;
let filterFn;
let acceptFormat;
if (typeof accept === 'string') {
acceptFormat = accept;
} else {
const {
filter,
format
} = accept || {};
acceptFormat = format;
if (filter === 'native') {
filterFn = () => true;
} else {
filterFn = filter;
}
}
const mergedFilter = filterFn || (directory || force ? currentFile => (0, _attrAccept.default)(currentFile, acceptFormat) : () => true);
return mergedFilter(file);
};
onChange = e => {
const {
files
} = e.target;
const acceptedFiles = [...files].filter(file => this.filterFile(file));
this.uploadFiles(acceptedFiles);
this.reset();
};
onClick = event => {
const el = this.fileInput;
if (!el) {
return;
}
const target = event.target;
const {
onClick
} = this.props;
if (target && target.tagName === 'BUTTON') {
const parent = el.parentNode;
parent.focus();
target.blur();
}
el.click();
if (onClick) {
onClick(event);
}
};
onKeyDown = e => {
if (e.key === 'Enter') {
this.onClick(e);
}
};
onDataTransferFiles = async (dataTransfer, existFileCallback) => {
const {
multiple,
directory
} = this.props;
const items = [...(dataTransfer.items || [])];
let files = [...(dataTransfer.files || [])];
if (files.length > 0 || items.some(item => item.kind === 'file')) {
existFileCallback?.();
}
if (directory) {
files = await (0, _traverseFileTree.default)(Array.prototype.slice.call(items), this.filterFile);
this.uploadFiles(files);
} else {
let acceptFiles = [...files].filter(file => this.filterFile(file, true));
if (multiple === false) {
acceptFiles = files.slice(0, 1);
}
this.uploadFiles(acceptFiles);
}
};
onFilePaste = async e => {
const {
pastable
} = this.props;
if (!pastable) {
return;
}
if (e.type === 'paste') {
const clipboardData = e.clipboardData;
return this.onDataTransferFiles(clipboardData, () => {
e.preventDefault();
});
}
};
onFileDragOver = e => {
e.preventDefault();
};
onFileDrop = async e => {
e.preventDefault();
if (e.type === 'drop') {
const dataTransfer = e.dataTransfer;
return this.onDataTransferFiles(dataTransfer);
}
};
componentDidMount() {
this._isMounted = true;
const {
pastable
} = this.props;
if (pastable) {
document.addEventListener('paste', this.onFilePaste);
}
}
componentWillUnmount() {
this._isMounted = false;
this.abort();
document.removeEventListener('paste', this.onFilePaste);
}
componentDidUpdate(prevProps) {
const {
pastable
} = this.props;
if (pastable && !prevProps.pastable) {
document.addEventListener('paste', this.onFilePaste);
} else if (!pastable && prevProps.pastable) {
document.removeEventListener('paste', this.onFilePaste);
}
}
uploadFiles = files => {
const originFiles = [...files];
const postFiles = originFiles.map(file => {
// eslint-disable-next-line no-param-reassign
file.uid = (0, _uid.default)();
return this.processFile(file, originFiles);
});
// Batch upload files
Promise.all(postFiles).then(fileList => {
const {
onBatchStart
} = this.props;
onBatchStart?.(fileList.map(({
origin,
parsedFile
}) => ({
file: origin,
parsedFile
})));
fileList.filter(file => file.parsedFile !== null).forEach(file => {
this.post(file);
});
});
};
/**
* Process file before upload. When all the file is ready, we start upload.
*/
processFile = async (file, fileList) => {
const {
beforeUpload
} = this.props;
let transformedFile = file;
if (beforeUpload) {
try {
transformedFile = await beforeUpload(file, fileList);
} catch (e) {
// Rejection will also trade as false
transformedFile = false;
}
if (transformedFile === false) {
return {
origin: file,
parsedFile: null,
action: null,
data: null
};
}
}
// Get latest action
const {
action
} = this.props;
let mergedAction;
if (typeof action === 'function') {
mergedAction = await action(file);
} else {
mergedAction = action;
}
// Get latest data
const {
data
} = this.props;
let mergedData;
if (typeof data === 'function') {
mergedData = await data(file);
} else {
mergedData = data;
}
const parsedData =
// string type is from legacy `transformFile`.
// Not sure if this will work since no related test case works with it
(typeof transformedFile === 'object' || typeof transformedFile === 'string') && transformedFile ? transformedFile : file;
let parsedFile;
if (parsedData instanceof File) {
parsedFile = parsedData;
} else {
parsedFile = new File([parsedData], file.name, {
type: file.type
});
}
const mergedParsedFile = parsedFile;
mergedParsedFile.uid = file.uid;
return {
origin: file,
data: mergedData,
parsedFile: mergedParsedFile,
action: mergedAction
};
};
post({
data,
origin,
action,
parsedFile
}) {
if (!this._isMounted) {
return;
}
const {
onStart,
customRequest,
name,
headers,
withCredentials,
method
} = this.props;
const {
uid
} = origin;
const request = customRequest || _request.default;
const requestOption = {
action,
filename: name,
data,
file: parsedFile,
headers,
withCredentials,
method: method || 'post',
onProgress: e => {
const {
onProgress
} = this.props;
onProgress?.(e, parsedFile);
},
onSuccess: (ret, xhr) => {
const {
onSuccess
} = this.props;
onSuccess?.(ret, parsedFile, xhr);
delete this.reqs[uid];
},
onError: (err, ret) => {
const {
onError
} = this.props;
onError?.(err, ret, parsedFile);
delete this.reqs[uid];
}
};
onStart(origin);
this.reqs[uid] = request(requestOption, {
defaultRequest: _request.default
});
}
reset() {
this.setState({
uid: (0, _uid.default)()
});
}
abort(file) {
const {
reqs
} = this;
if (file) {
const uid = file.uid ? file.uid : file;
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
delete reqs[uid];
} else {
Object.keys(reqs).forEach(uid => {
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
delete reqs[uid];
});
}
}
saveFileInput = node => {
this.fileInput = node;
};
render() {
const {
component: Tag,
prefixCls,
className,
classNames = {},
disabled,
id,
name,
style,
styles = {},
multiple,
accept,
capture,
children,
directory,
openFileDialogOnClick,
onMouseEnter,
onMouseLeave,
hasControlInside,
...otherProps
} = this.props;
// Extract accept format for input element
const acceptFormat = typeof accept === 'string' ? accept : accept?.format;
const cls = (0, _clsx.clsx)(prefixCls, {
[`${prefixCls}-disabled`]: disabled,
[className]: className
});
// because input don't have directory/webkitdirectory type declaration
const dirProps = directory ? {
directory: 'directory',
webkitdirectory: 'webkitdirectory'
} : {};
const events = disabled ? {} : {
onClick: openFileDialogOnClick ? this.onClick : () => {},
onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => {},
onMouseEnter,
onMouseLeave,
onDrop: this.onFileDrop,
onDragOver: this.onFileDragOver,
tabIndex: hasControlInside ? undefined : '0'
};
return /*#__PURE__*/_react.default.createElement(Tag, _extends({}, events, {
className: cls,
role: hasControlInside ? undefined : 'button',
style: style
}), /*#__PURE__*/_react.default.createElement("input", _extends({}, (0, _pickAttrs.default)(otherProps, {
aria: true,
data: true
}), {
id: id
/**
* https://github.com/ant-design/ant-design/issues/50643,
* https://github.com/react-component/upload/pull/575#issuecomment-2320646552
*/,
name: name,
disabled: disabled,
type: "file",
ref: this.saveFileInput,
onClick: e => e.stopPropagation() // https://github.com/ant-design/ant-design/issues/19948
,
key: this.state.uid,
style: {
display: 'none',
...styles.input
},
className: classNames.input,
accept: acceptFormat
}, dirProps, {
multiple: multiple,
onChange: this.onChange
}, capture != null ? {
capture
} : {})), children);
}
}
var _default = exports.default = AjaxUploader;
+28
View File
@@ -0,0 +1,28 @@
import React, { Component } from 'react';
import AjaxUpload from './AjaxUploader';
import type { UploadProps, RcFile } from './interface';
declare function empty(): void;
declare class Upload extends Component<UploadProps> {
static defaultProps: {
component: string;
prefixCls: string;
data: {};
headers: {};
name: string;
multipart: boolean;
onStart: typeof empty;
onError: typeof empty;
onSuccess: typeof empty;
multiple: boolean;
beforeUpload: any;
customRequest: any;
withCredentials: boolean;
openFileDialogOnClick: boolean;
hasControlInside: boolean;
};
private uploader;
abort(file: RcFile): void;
saveUploader: (node: AjaxUpload) => void;
render(): React.JSX.Element;
}
export default Upload;
+45
View File
@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _AjaxUploader = _interopRequireDefault(require("./AjaxUploader"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /* eslint react/prop-types:0 */
function empty() {}
class Upload extends _react.Component {
static defaultProps = {
component: 'span',
prefixCls: 'rc-upload',
data: {},
headers: {},
name: 'file',
multipart: false,
onStart: empty,
onError: empty,
onSuccess: empty,
multiple: false,
beforeUpload: null,
customRequest: null,
withCredentials: false,
openFileDialogOnClick: true,
hasControlInside: false
};
uploader;
abort(file) {
this.uploader.abort(file);
}
saveUploader = node => {
this.uploader = node;
};
render() {
return /*#__PURE__*/_react.default.createElement(_AjaxUploader.default, _extends({}, this.props, {
ref: this.saveUploader
}));
}
}
var _default = exports.default = Upload;
@@ -0,0 +1,3 @@
import type { RcFile } from './interface';
declare const _default: (file: RcFile, acceptedFiles: string | string[]) => boolean;
export default _default;
+52
View File
@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _util = require("@rc-component/util");
var _default = (file, acceptedFiles) => {
if (file && acceptedFiles) {
const acceptedFilesArray = Array.isArray(acceptedFiles) ? acceptedFiles : acceptedFiles.split(',');
const fileName = file.name || '';
const mimeType = file.type || '';
const baseMimeType = mimeType.replace(/\/.*$/, '');
return acceptedFilesArray.some(type => {
const validType = type.trim();
// This is something like */*,* allow all files
if (/^\*(\/\*)?$/.test(type)) {
return true;
}
// like .jpg, .png
if (validType.charAt(0) === '.') {
const lowerFileName = fileName.toLowerCase();
const lowerType = validType.toLowerCase();
let affixList = [lowerType];
if (lowerType === '.jpg' || lowerType === '.jpeg') {
affixList = ['.jpg', '.jpeg'];
}
return affixList.some(affix => lowerFileName.endsWith(affix));
}
// This is something like a image/* mime type
if (/\/\*$/.test(validType)) {
return baseMimeType === validType.replace(/\/.*$/, '');
}
// Full match
if (mimeType === validType) {
return true;
}
// Invalidate type should skip
if (/^\w+$/.test(validType)) {
(0, _util.warning)(false, `Upload takes an invalidate 'accept' type '${validType}'.Skip for check.`);
return true;
}
return false;
});
}
return true;
};
exports.default = _default;
+4
View File
@@ -0,0 +1,4 @@
import type { UploadProps } from './interface';
import Upload from './Upload';
export type { UploadProps };
export default Upload;
+9
View File
@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _Upload = _interopRequireDefault(require("./Upload"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _default = exports.default = _Upload.default;
+79
View File
@@ -0,0 +1,79 @@
import type * as React from 'react';
export type BeforeUploadFileType = File | Blob | boolean | string;
export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
export type AcceptConfig = {
format: string;
filter?: 'native' | ((file: RcFile) => boolean);
};
export interface UploadProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onError' | 'onProgress' | 'accept'> {
name?: string;
style?: React.CSSProperties;
className?: string;
disabled?: boolean;
component?: React.ComponentType<any> | string;
action?: Action;
method?: UploadRequestMethod;
directory?: boolean;
data?: Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>);
headers?: UploadRequestHeader;
accept?: string | AcceptConfig;
multiple?: boolean;
onBatchStart?: (fileList: {
file: RcFile;
parsedFile: Exclude<BeforeUploadFileType, boolean>;
}[]) => void;
onStart?: (file: RcFile) => void;
onError?: (error: Error, ret: Record<string, unknown>, file: RcFile) => void;
onSuccess?: (response: Record<string, unknown>, file: RcFile, xhr: XMLHttpRequest) => void;
onProgress?: (event: UploadProgressEvent, file: RcFile) => void;
beforeUpload?: (file: RcFile, FileList: RcFile[]) => BeforeUploadFileType | Promise<void | BeforeUploadFileType> | void;
customRequest?: CustomUploadRequestOption;
withCredentials?: boolean;
openFileDialogOnClick?: boolean;
prefixCls?: string;
id?: string;
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void;
classNames?: {
input?: string;
};
styles?: {
input?: React.CSSProperties;
};
hasControlInside?: boolean;
pastable?: boolean;
}
export interface UploadProgressEvent extends Partial<ProgressEvent> {
percent?: number;
}
export type UploadRequestMethod = 'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch';
export type UploadRequestHeader = Record<string, string>;
export type UploadRequestFile = Exclude<BeforeUploadFileType, File | boolean> | RcFile;
export interface UploadRequestError extends Error {
status?: number;
method?: UploadRequestMethod;
url?: string;
}
export interface UploadRequestOption<T = any> {
onProgress?: (event: UploadProgressEvent, file?: UploadRequestFile) => void;
onError?: (event: UploadRequestError | ProgressEvent, body?: T) => void;
onSuccess?: (body: T, fileOrXhr?: UploadRequestFile | XMLHttpRequest) => void;
data?: Record<string, unknown>;
filename?: string;
file: UploadRequestFile;
withCredentials?: boolean;
action: string;
headers?: UploadRequestHeader;
method: UploadRequestMethod;
}
export type CustomUploadRequestOption = (option: UploadRequestOption, info: {
defaultRequest: (option: UploadRequestOption) => {
abort: () => void;
} | void;
}) => void | {
abort: () => void;
};
export interface RcFile extends File {
uid: string;
}
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
+4
View File
@@ -0,0 +1,4 @@
import type { UploadRequestOption } from './interface';
export default function upload(option: UploadRequestOption): {
abort(): void;
};
+97
View File
@@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = upload;
function getError(option, xhr) {
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
const err = new Error(msg);
err.status = xhr.status;
err.method = option.method;
err.url = option.action;
return err;
}
function getBody(xhr) {
const text = xhr.responseText || xhr.response;
if (!text) {
return text;
}
try {
return JSON.parse(text);
} catch (e) {
return text;
}
}
function upload(option) {
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest();
if (option.onProgress && xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
option.onProgress(e);
};
}
// eslint-disable-next-line no-undef
const formData = new FormData();
if (option.data) {
Object.keys(option.data).forEach(key => {
const value = option.data[key];
// support key-value array data
if (Array.isArray(value)) {
value.forEach(item => {
// { list: [ 11, 22 ] }
// formData.append('list[]', 11);
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, value);
});
}
// eslint-disable-next-line no-undef
if (option.file instanceof Blob) {
formData.append(option.filename, option.file, option.file.name);
} else {
formData.append(option.filename, option.file);
}
xhr.onerror = function error(e) {
option.onError(e);
};
xhr.onload = function onload() {
// allow success when 2xx status
// see https://github.com/react-component/upload/issues/34
if (xhr.status < 200 || xhr.status >= 300) {
return option.onError(getError(option, xhr), getBody(xhr));
}
return option.onSuccess(getBody(xhr), xhr);
};
xhr.open(option.method, option.action, true);
// Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true;
}
const headers = option.headers || {};
// when set headers['X-Requested-With'] = null , can close default XHR header
// see https://github.com/react-component/upload/issues/33
if (headers['X-Requested-With'] !== null) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
Object.keys(headers).forEach(h => {
if (headers[h] !== null) {
xhr.setRequestHeader(h, headers[h]);
}
});
xhr.send(formData);
return {
abort() {
xhr.abort();
}
};
}
@@ -0,0 +1,14 @@
import type { RcFile } from './interface';
interface InternalDataTransferItem extends DataTransferItem {
isFile: boolean;
file: (cd: (file: RcFile & {
webkitRelativePath?: string;
}) => void) => void;
createReader: () => any;
fullPath: string;
isDirectory: boolean;
name: string;
path: string;
}
declare const traverseFileTree: (files: InternalDataTransferItem[], isAccepted: any) => Promise<any[]>;
export default traverseFileTree;
@@ -0,0 +1,80 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
// https://github.com/ant-design/ant-design/issues/50080
const traverseFileTree = async (files, isAccepted) => {
const flattenFileList = [];
const progressFileList = [];
files.forEach(file => progressFileList.push(file.webkitGetAsEntry()));
async function readDirectory(directory) {
const dirReader = directory.createReader();
const entries = [];
while (true) {
const results = await new Promise(resolve => {
dirReader.readEntries(resolve, () => resolve([]));
});
const n = results.length;
if (!n) {
break;
}
for (let i = 0; i < n; i++) {
entries.push(results[i]);
}
}
return entries;
}
async function readFile(item) {
return new Promise(reslove => {
item.file(file => {
if (isAccepted(file)) {
// https://github.com/ant-design/ant-design/issues/16426
if (item.fullPath && !file.webkitRelativePath) {
Object.defineProperties(file, {
webkitRelativePath: {
writable: true
}
});
// eslint-disable-next-line no-param-reassign
file.webkitRelativePath = item.fullPath.replace(/^\//, '');
Object.defineProperties(file, {
webkitRelativePath: {
writable: false
}
});
}
reslove(file);
} else {
reslove(null);
}
});
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
const _traverseFileTree = async (item, path) => {
if (!item) {
return;
}
// eslint-disable-next-line no-param-reassign
item.path = path || '';
if (item.isFile) {
const file = await readFile(item);
if (file) {
flattenFileList.push(file);
}
} else if (item.isDirectory) {
const entries = await readDirectory(item);
progressFileList.push(...entries);
}
};
let wipIndex = 0;
while (wipIndex < progressFileList.length) {
await _traverseFileTree(progressFileList[wipIndex]);
wipIndex++;
}
return flattenFileList;
};
var _default = exports.default = traverseFileTree;
+1
View File
@@ -0,0 +1 @@
export default function uid(): string;
+12
View File
@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = uid;
const now = +new Date();
let index = 0;
function uid() {
// eslint-disable-next-line no-plusplus
return `rc-upload-${now}-${++index}`;
}
+74
View File
@@ -0,0 +1,74 @@
{
"name": "@rc-component/upload",
"version": "1.1.0",
"description": "upload ui component for react",
"keywords": [
"react",
"react-component",
"react-upload",
"upload"
],
"homepage": "http://github.com/react-component/upload",
"bugs": {
"url": "http://github.com/react-component/upload/issues"
},
"repository": {
"type": "git",
"url": "git@github.com:react-component/upload.git"
},
"license": "MIT",
"main": "./lib/index",
"module": "./es/index",
"files": [
"lib",
"es"
],
"scripts": {
"compile": "father build",
"coverage": "rc-test --coverage",
"docs:build": "dumi build",
"docs:deploy": "npm run docs:build && gh-pages -d dist",
"lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md",
"now-build": "npm run docs:build",
"prepublishOnly": "npm run compile && rc-np",
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"postpublish": "npm run docs:deploy",
"start": "dumi dev",
"test": "rc-test"
},
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.2",
"@rc-component/np": "^1.0.4",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^16.2.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.5.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@umijs/fabric": "^4.0.1",
"axios": "^1.9.0",
"co-busboy": "^2.0.2",
"coveralls": "^3.0.3",
"cross-env": "^10.1.0",
"dumi": "^2.1.0",
"eslint": "^8.0.0",
"father": "^4.0.0",
"fs-extra": "^11.2.0",
"gh-pages": "^6.1.1",
"rc-test": "^7.0.13",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"regenerator-runtime": "^0.14.1",
"sinon": "^9.0.2",
"typescript": "^5.3.3",
"vinyl-fs": "^4.0.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
}