├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── karma.conf.js ├── webpack.config.js └── webpack.manual-test.config.js ├── img ├── gallery-initial.png └── gallery-with-files.png ├── package-lock.json ├── package.json └── src ├── cancel-button.jsx ├── delete-button.jsx ├── dropzone.jsx ├── file-input ├── index.jsx └── styleable-element.jsx ├── filename.jsx ├── filesize.jsx ├── gallery ├── gallery.css ├── index.jsx ├── pause-icon.jsx ├── play-icon.jsx ├── upload-failed-icon.jsx ├── upload-icon.jsx ├── upload-success-icon.jsx └── x-icon.jsx ├── pause-resume-button.jsx ├── progress-bar.jsx ├── retry-button.jsx ├── status.jsx ├── test ├── manual │ ├── composer.json │ ├── composer.lock │ ├── index.html │ ├── index.jsx │ ├── php.ini │ └── tester.jsx └── unit │ ├── cancel-button.spec.jsx │ ├── delete-button.spec.jsx │ ├── file-input │ ├── file-input.spec.jsx │ └── styleable-element.spec.jsx │ ├── filename.spec.jsx │ ├── filesize.spec.jsx │ ├── gallery.spec.jsx │ ├── pause-resume-button.spec.jsx │ ├── progress-bar.spec.jsx │ ├── retry-button.spec.jsx │ ├── sanity.spec.js │ ├── status.spec.jsx │ ├── tests.bundle.js │ ├── thumbnail │ └── index.spec.jsx │ └── utils.jsx └── thumbnail ├── index.jsx ├── not-available-placeholder.jsx ├── placeholder.jsx └── waiting-placeholder.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "syntax-class-properties", 4 | "transform-class-properties", 5 | "transform-object-rest-spread" 6 | ], 7 | "presets": [ 8 | "es2015", 9 | "react" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | # Matches multiple files with brace expansion notation 10 | # Set default charset 11 | [{*.js}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 4 15 | 16 | # Tab indentation (no size specified) 17 | [Makefile] 18 | indent_style = tab 19 | 20 | # Matches the exact files either package.json or .travis.yml 21 | [{package.json,.travis.yml}] 22 | indent_style = space 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/test/manual/**/** 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "afterAll": true, 10 | "afterEach": true, 11 | "beforeAll": true, 12 | "beforeEach": true, 13 | "describe": true, 14 | "expect": true, 15 | "it": true, 16 | "jasmine": true, 17 | "spyOn": true 18 | }, 19 | "parser": "babel-eslint", 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "jsx": true, 23 | "modules": true 24 | }, 25 | "sourceType": "module" 26 | }, 27 | "plugins": [ 28 | "react" 29 | ], 30 | "rules": { 31 | "indent": [ 32 | "error", 33 | 4, 34 | { "SwitchCase": 1 } 35 | ], 36 | "linebreak-style": [ 37 | "error", 38 | "unix" 39 | ], 40 | "no-console": 0, 41 | "quotes": [ 42 | "error", 43 | "single" 44 | ], 45 | "react/jsx-uses-react": "error", 46 | "react/jsx-uses-vars": "error", 47 | "semi": [ 48 | "error", 49 | "never" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | .idea 3 | composer.phar 4 | dist 5 | lib 6 | node_modules 7 | s3keys.sh 8 | src/test/manual/bundle 9 | uploads/ 10 | vendor/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '5.0' 5 | env: 6 | global: 7 | - DISPLAY=:99.0 8 | - SAUCE_USERNAME: react-fine-uploader2 9 | - secure: QW6G2+DfOiH01dRd8qlj/Xc58maGsj7NKge7fIOqBF2SO42RyGAzRCLyeI2A7Sj0lcytIP2X1ILLhNMVsAMa0ldTnBa3hDmonBOvgGmLxbe13EsM+2sfsqAigOiIoUwk5yUk/v4pOCFmEf3BCE9jEp5S0q197Ii8tuH99AO9vKz1wFJ21fZZf9+N8tJcXaBpR7nfeN80G0Fs+ddQktYCq+644CQveqghwx1Vbi2wztkTZKMcSdCYh8pQaieZf2PnIGi/StcOry6jBOlfl8H243vpdmHJJssztHiylomjtPjsxnB5PdwtlZZNIgy/+/cGYcE9RRNwEpOe1Z10NvukSjW/hmYEo8It3xakdYhV3jQU8GTCSxu2D8QyR/pW0JjxDyybmGhE5ElQaGMSUdJ/5+FF/aTuzmfly8eAZX1+64XOZSEwbUi+A2hDZ2HIJU+jaGwXouzUqYCvsjn+tAn2mlURdGCaooqWoCVxpnP2MBFKhRuyL2aX6MgqRkZ1GOHH5SAo/ufPd5j8tRQ7BwEcyX1zit/EiutPhkhsO3fdW2EBs5EjrNxK5x+tFATml1iawdvy2m+h8kvwQ3oA2ja8gGBuIcdYKft+iHGk7ozUxOHvzTVQ1tNlFoM05drNDGn2Ld23j8ih7jtaHb1lAprqUBNodiStBxUIqmsRMO/zwMs= 10 | before_script: 11 | - sh -e /etc/init.d/xvfb start 12 | addons: 13 | firefox: latest 14 | jwt: 15 | secure: adRnCigFL/pHJdZzRtb4cEw2vGsrvqyWKVfR77nhTTPrgk9TiIvPd1/w9ufQJhhYkYtq2TqdWVmuw7wzBYiu2RiTWHeTEDmACUkfJbWPBt9xfUiOOgcNvLKGMICoeKEeiX+yK0FRGv+V6vInyOVCl+yEJ3nH4Rf9a2UbCLZaB/HTnhu8ejjwz7mnklbsGnGHyUi9VRIz24u02lxXVOZrrAQNsNdxDz8Lyu2RGAbFGdN+iw8alRVLhErv0n8TgWm5CUBoon3e1g6gEoXswLqNWrpJXagQeamHaflpehGJ0zubqXohf3OP4hxRD7apeeAvXfT7/O0uQCGnLQAlgH3bHWCx6O9xSshfpfoevnsKlTxMHyIc4hx76yA3picCKwRdtnnJbBOqSIT9/8VAhp3WJxOWbGwp0J44u5E4PjHuL5h5kt0NOFADyysS+/RLG3GZN4VJ9qzFb1uQ4qeJpS5/6iRFiwbS09h/TP8F4XjuY478xuSR4fhUH1qz0EA3R6PRtjRP1nFSOdqH3SZd10rFphUij+uOoDpUaAI5FFqld0+iBO2/hiQEdJSCm+/gk0n+hyqnHJfNIvTMC7OFOKLR2+yWZC4saRNKu/BcIdY9BA/VurQAUykrlwP+bp3GvqTnWCHuRNpNuAoz6h3ITeCq8Rm+ZW5O3JFB4H1EQUf+6L8= 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ray Nicholus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [![npm](https://img.shields.io/npm/v/react-fine-uploader.svg)](https://www.npmjs.com/package/react-fine-uploader) 7 | [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 8 | [![Build Status](https://travis-ci.org/FineUploader/react-fine-uploader.svg?branch=master)](https://travis-ci.org/FineUploader/react-fine-uploader) 9 | 10 | Makes using [Fine Uploader](http://fineuploader.com) in a React app simple. Drop-in high-level components for a turn-key UI. Use small focused components to build a more custom UI. 11 | 12 | ## Docs 13 | 14 | ### Supported browsers 15 | 16 | - Chrome (desktop & mobile) 17 | - Firefox 18 | - Internet Explorer 11 19 | - Microsoft Edge 13+ 20 | - Safari 9+ (desktop & mobile) 21 | 22 | ### Overview 23 | 24 | React Fine Uploader makes using Fine Uploader and all of its unique features very simple in a React-based project. This library provides useful resources that can be divided into three sections: 25 | 26 | #### Individual focused components (like `` and ``). 27 | 28 | These allow you to easily build a highly customizable and powerful UI for your upload widget, backed by Fine Uploader's core feature set. Most of these components are unstyled (i.e. ready to be styled by you). Focused component-specific stylesheets may be provided at a later date. 29 | 30 | 31 | #### Higher-level components (like ``) 32 | 33 | These combine many focused components that provide style (which can be adjusted via your own stylesheet) and enhanced UI-specific features. These components are essentially "turn-key", which means that you can get a fully functional upload widget up and running in your project with a few lines of code. Keep in mind that of course you still need a server to handle the requests sent by Fine Uploader and to server up the JavaScript and CSS files. 34 | 35 | #### Wrapper classes 36 | 37 | These wrap a Fine Uploader instance for use in React Fine Uploader. They provide additional features such as the ability to dynamically register multiple event/callback listeners. All individual and high-level/focused components require you to pass a constructed wrapper class instance. 38 | 39 | More information, such as examples and API documentation, can be found in the README of the [fine-uploader-wrappers project](https://github.com/FineUploader/fine-uploader-wrappers). 40 | 41 | ### Quick Reference 42 | 43 | - [Installing](#installing) 44 | - [High-level Components](#high-level-components) 45 | - [``](#gallery-) 46 | - [Low-level Components](#low-level-components) 47 | - [``](#cancelbutton-) 48 | - [``](#deletebutton-) 49 | - [``](#dropzone-) 50 | - [``](#fileinput-) 51 | - [``](#filename-) 52 | - [``](#filesize-) 53 | - [``](#pauseresumebutton-) 54 | - [``](#progressbar-) 55 | - [``](#retrybutton-) 56 | - [``](#status-) 57 | - [``](#thumbnail-) 58 | 59 | ### Installing 60 | 61 | Two dependencies that you will need to install yourself: an A+/Promise spec compliant polyfill (for IE11) and React (which is a peer dependency). Simply `npm install react-fine-uploader` and see the documentation for your specific integration instructions (based on your needs). You will also need to install [Fine Uploader](https://github.com/FineUploader/fine-uploader) as well, which is also [available on npm](https://www.npmjs.com/package/fine-uploader). 62 | 63 | ### High-level Components 64 | 65 | #### `` 66 | 67 | Similar to the Fine Uploader UI gallery template, the `` component lays out an uploader using all of the available [low-level components](#low-level-components). Appealing styles are provided, which can be easily overriden in your own style sheet. 68 | 69 | In the `` component, each file is rendered as a "card". CSS transitions are used to fade a card in when a file is submitted and then fade it out again when the file is either canceled during uploading or deleted after uploading. By default, a file input element is rendered and styled to allow access to the file chooser. And, if supported by the device, a drop zone is rendered as well. 70 | 71 | ##### Properties 72 | 73 | The only required property is `uploader`, which must be a Fine Uploader [wrapper class](#wrapper-classes) instance. But you can pass any property supported by any low-level component through `` by following this simple convention: `{lowerCamelCaseComponentName}-{propertyName}: {value}`. For example, if you want to specify custom child elements for the [`` element](#fileinput-), you would initialize the component like so: 74 | 75 | ```js 76 | const fileInputChildren = Choose files 77 | 78 | render() { 79 | return ( 80 | 81 | ) 82 | } 83 | ``` 84 | 85 | And if you wanted to instead change the rendered text for the [`` element](#status-) when the file uploads successfully, you would initialize your component like this: 86 | 87 | ```js 88 | const statusTextOverride = { 89 | upload_successful: 'Success!' 90 | } 91 | 92 | render() { 93 | return ( 94 | 95 | ) 96 | } 97 | ``` 98 | 99 | Note that you can also disable some components by passing a `disabled` property. Currently, this is limited to the `` component and the `` component. For example, if you wanted to prevent file dropping, your code would look similar to this: 100 | 101 | ```js 102 | render() { 103 | return ( 104 | 105 | ) 106 | } 107 | ``` 108 | 109 | Finally, you may disable the add/remove file animations by setting the `animationsDisabled` property to `true`. 110 | 111 | ##### A simple example 112 | 113 | For example, if you render a `` component using the following code: 114 | 115 | ```js 116 | import React, { Component } from 'react' 117 | 118 | import FineUploaderTraditional from 'fine-uploader-wrappers' 119 | import Gallery from 'react-fine-uploader' 120 | 121 | // ...or load this specific CSS file using a tag in your document 122 | import 'react-fine-uploader/gallery/gallery.css' 123 | 124 | const uploader = new FineUploaderTraditional({ 125 | options: { 126 | chunking: { 127 | enabled: true 128 | }, 129 | deleteFile: { 130 | enabled: true, 131 | endpoint: '/uploads' 132 | }, 133 | request: { 134 | endpoint: '/uploads' 135 | }, 136 | retry: { 137 | enableAuto: true 138 | } 139 | } 140 | }) 141 | 142 | class UploadComponent extends Component { 143 | render() { 144 | return ( 145 | 146 | ) 147 | } 148 | } 149 | 150 | export default UploadComponent 151 | ``` 152 | 153 | ...you will see this initial UI on page load: 154 | 155 | 156 | 157 | After setting up a [server to handle the upload and delete requests](http://docs.fineuploader.com/branch/master/quickstart/03-setting_up_server.html), and dropping a few files, you will see a modern-looking upload user interface with your files represented like so: 158 | 159 | 160 | 161 | ### Low-level Components 162 | 163 | #### `` 164 | 165 | The `` component allows you to easily render a useable cancel button for a submitted file. An file can be "canceled" at any time, except after it has uploaded successfully, and before it has passed validation (and of course after it has already been canceled). 166 | 167 | By default, the `` will be rendered and clickable only when the associated file is eligible for cancelation. Otherwise, the component will _not_ render a button. In other words, once, for example, the associated file has been canceled or has uploaded successfully, the button will essentially disappear. You can change this behavior by setting appropriate options. 168 | 169 | ##### Properties 170 | 171 | - `children` - (child elements/components of ``. Use this for any text of graphics that you would like to display inside the rendered button. If the component is childless, the button will be rendered with a simple text node of "Cancel". 172 | 173 | - `id` - The Fine Uploader ID of the submitted file. (required) 174 | 175 | - `onlyRenderIfCancelable` - Defaults to `true`. If set to `false`, the element will be rendered as a disabled button if the associated file is not cancelable. 176 | 177 | - `uploader` - A Fine Uploader [wrapper class](#wrapper-classes). (required) 178 | 179 | The example below will include a cancel button for each submitted file along with a [``](#thumbnail-), and will ensure the elements representing a file are removed if the file is canceled. 180 | 181 | ```javascript 182 | import React, { Component } from 'react' 183 | import ReactDOM from 'react-dom' 184 | 185 | import CancelButton from 'react-fine-uploader/cancel-button' 186 | import FineUploaderTraditional from 'fine-uploader-wrappers' 187 | import Thumbnail from 'react-fine-uploader/thumbnail' 188 | 189 | const uploader = new FineUploaderTraditional({ 190 | options: { 191 | request: { 192 | endpoint: 'my/upload/endpoint' 193 | } 194 | } 195 | }) 196 | 197 | export default class FileListener extends Component { 198 | constructor() { 199 | super() 200 | 201 | this.state = { 202 | submittedFiles: [] 203 | } 204 | } 205 | 206 | componentDidMount() { 207 | uploader.on('statusChange', (id, oldStatus, newStatus) => { 208 | if (newStatus === 'submitted') { 209 | const submittedFiles = this.state.submittedFiles 210 | 211 | submittedFiles.push(id) 212 | this.setState({ submittedFiles }) 213 | } 214 | else if (isFileGone(newStatus)) { 215 | const submittedFiles = this.state.submittedFiles 216 | const indexToRemove = submittedFiles.indexOf(id) 217 | 218 | submittedFiles.splice(indexToRemove, 1) 219 | this.setState({ submittedFiles }) 220 | } 221 | }) 222 | } 223 | 224 | render() { 225 | return ( 226 |
227 | { 228 | this.state.submittedFiles.map(id => { 229 |
230 | 231 | 232 |
233 | }) 234 | } 235 |
236 | ) 237 | } 238 | } 239 | 240 | const isFileGone = status => { 241 | return [ 242 | 'canceled', 243 | 'deleted', 244 | ].indexOf(status) >= 0 245 | } 246 | ``` 247 | 248 | You may pass _any_ standard [` 64 | ) 65 | } 66 | 67 | return null 68 | } 69 | 70 | _unregisterStatusChangeHandler() { 71 | this.props.uploader.off('statusChange', this._onStatusChange) 72 | } 73 | } 74 | 75 | const isCancelable = (statusToCheck, statusEnum) => { 76 | return [ 77 | statusEnum.DELETE_FAILED, 78 | statusEnum.PAUSED, 79 | statusEnum.QUEUED, 80 | statusEnum.UPLOAD_RETRYING, 81 | statusEnum.SUBMITTED, 82 | statusEnum.UPLOADING, 83 | statusEnum.UPLOAD_FAILED 84 | ].indexOf(statusToCheck) >= 0 85 | } 86 | 87 | export default CancelButton 88 | -------------------------------------------------------------------------------- /src/delete-button.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class DeleteButton extends Component { 5 | static propTypes = { 6 | children: PropTypes.node, 7 | id: PropTypes.number.isRequired, 8 | onlyRenderIfDeletable: PropTypes.bool, 9 | uploader: PropTypes.object.isRequired 10 | }; 11 | 12 | static defaultProps = { 13 | onlyRenderIfDeletable: true 14 | }; 15 | 16 | constructor(props) { 17 | super(props) 18 | 19 | this.state = { 20 | deletable: false, 21 | deleting: false 22 | } 23 | 24 | const statusEnum = props.uploader.qq.status 25 | 26 | this._onStatusChange = (id, oldStatus, newStatus) => { 27 | if (id === this.props.id && !this._unmounted) { 28 | if (!isDeletable(newStatus, statusEnum) && newStatus !== statusEnum.DELETING && this.state.deletable) { 29 | !this._unmounted && this.setState({ 30 | deletable: false, 31 | deleting: false 32 | }) 33 | this._unregisterStatusChangeHandler() 34 | } 35 | else if (isDeletable(newStatus, statusEnum) && !this.state.deletable) { 36 | this.setState({ 37 | deletable: true, 38 | deleting: false 39 | }) 40 | } 41 | else if (newStatus === statusEnum.DELETING && !this.state.deleting) { 42 | this.setState({ deleting: true }) 43 | } 44 | } 45 | } 46 | 47 | this._onClick = () => this.props.uploader.methods.deleteFile(this.props.id) 48 | } 49 | 50 | componentDidMount() { 51 | this.props.uploader.on('statusChange', this._onStatusChange) 52 | } 53 | 54 | componentWillUnmount() { 55 | this._unmounted = true 56 | this._unregisterStatusChangeHandler() 57 | } 58 | 59 | render() { 60 | const { children, onlyRenderIfDeletable, id, uploader, ...elementProps } = this.props // eslint-disable-line no-unused-vars 61 | const content = children || 'Delete' 62 | 63 | if (this.state.deletable || this.state.deleting || !onlyRenderIfDeletable) { 64 | return ( 65 | 74 | ) 75 | } 76 | 77 | return null 78 | } 79 | 80 | _unregisterStatusChangeHandler() { 81 | this.props.uploader.off('statusChange', this._onStatusChange) 82 | } 83 | } 84 | 85 | const isDeletable = (statusToCheck, statusEnum) => { 86 | return [ 87 | statusEnum.DELETE_FAILED, 88 | statusEnum.UPLOAD_SUCCESSFUL 89 | ].indexOf(statusToCheck) >= 0 90 | } 91 | 92 | export default DeleteButton 93 | -------------------------------------------------------------------------------- /src/dropzone.jsx: -------------------------------------------------------------------------------- 1 | import qq from 'fine-uploader/lib/dnd' 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | class DropzoneElement extends Component { 6 | static propTypes = { 7 | children: PropTypes.node, 8 | dropActiveClassName: PropTypes.string, 9 | element: PropTypes.object, 10 | multiple: PropTypes.bool, 11 | onDropError: PropTypes.func, 12 | onProcessingDroppedFiles: PropTypes.func, 13 | onProcessingDroppedFilesComplete: PropTypes.func, 14 | uploader: PropTypes.object.isRequired 15 | }; 16 | 17 | static defaultProps = { 18 | dropActiveClassName: 'react-fine-uploader-dropzone-active' 19 | } 20 | 21 | componentDidMount() { 22 | this._registerDropzone() 23 | } 24 | 25 | componentDidUpdate() { 26 | this._registerDropzone() 27 | } 28 | 29 | componentWillUnmount() { 30 | this._qqDropzone && this._qqDropzone.dispose() 31 | } 32 | 33 | render() { 34 | const { uploader, ...elementProps } = this.props // eslint-disable-line no-unused-vars 35 | 36 | return ( 37 |
41 | { this.props.children } 42 |
43 | ) 44 | } 45 | 46 | _onDropError(errorCode, errorData) { 47 | console.error(errorCode, errorData) 48 | 49 | this.props.onDropError && this.props.onDropError(errorCode, errorData) 50 | } 51 | 52 | _onProcessingDroppedFilesComplete(files) { 53 | this.props.uploader.methods.addFiles(files) 54 | 55 | if (this.props.onProcessingDroppedFilesComplete) { 56 | this.props.onProcessingDroppedFilesComplete(files) 57 | } 58 | } 59 | 60 | _registerDropzone() { 61 | this._qqDropzone && this._qqDropzone.dispose() 62 | 63 | const dropzoneEl = this.props.element || this.refs.dropZone 64 | 65 | this._qqDropzone = new qq.DragAndDrop({ 66 | allowMultipleItems: !!this.props.multiple, 67 | callbacks: { 68 | dropError: this._onDropError.bind(this), 69 | processingDroppedFiles: this.props.onProcessingDroppedFiles || function() {}, 70 | processingDroppedFilesComplete: this._onProcessingDroppedFilesComplete.bind(this) 71 | }, 72 | classes: { 73 | dropActive: this.props.dropActiveClassName || '' 74 | }, 75 | dropZoneElements: [dropzoneEl] 76 | }) 77 | } 78 | } 79 | 80 | const getElementProps = actualProps => { 81 | const actualPropsCopy = { ...actualProps } 82 | const expectedPropNames = Object.keys(DropzoneElement.propTypes) 83 | 84 | expectedPropNames.forEach(expectedPropName => delete actualPropsCopy[expectedPropName]) 85 | return actualPropsCopy 86 | } 87 | 88 | export default DropzoneElement 89 | -------------------------------------------------------------------------------- /src/file-input/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import StyleableElement from './styleable-element' 5 | 6 | class FileInput extends Component { 7 | static propTypes = { 8 | text: PropTypes.shape({ 9 | selectFile: PropTypes.string, 10 | selectFiles: PropTypes.string, 11 | }), 12 | uploader: PropTypes.object.isRequired 13 | }; 14 | 15 | static defaultProps = { 16 | text: { 17 | selectFile: 'Select a File', 18 | selectFiles: 'Select Files', 19 | } 20 | } 21 | 22 | constructor() { 23 | super() 24 | 25 | this.state = { key: newKey() } 26 | this._onFilesSelected = onFilesSelected.bind(this) 27 | } 28 | 29 | render() { 30 | const { text, uploader, ...elementProps } = this.props // eslint-disable-line no-unused-vars 31 | 32 | return ( 33 | 37 | { 38 | this.props.children 39 | ? this.props.children 40 | : { elementProps.multiple ? text.selectFiles : text.selectFile } 41 | } 42 | 43 | ) 44 | } 45 | 46 | _resetInput() { 47 | this.setState({ key: newKey() }) 48 | } 49 | } 50 | 51 | const onFilesSelected = function(onChangeEvent) { 52 | this.props.uploader.methods.addFiles(onChangeEvent.target) 53 | this._resetInput() 54 | } 55 | 56 | const newKey = () => Date.now() 57 | 58 | export default FileInput 59 | -------------------------------------------------------------------------------- /src/file-input/styleable-element.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const containerStyle = { 4 | display: 'inline-block', 5 | position: 'relative' 6 | } 7 | 8 | const inputStyle = { 9 | bottom: 0, 10 | height: '100%', 11 | left: 0, 12 | margin: 0, 13 | opacity: 0, 14 | padding: 0, 15 | position: 'absolute', 16 | right: 0, 17 | top: 0, 18 | width: '100%' 19 | } 20 | 21 | const StyleableFileInput = ({ children, className, onChange, ...params }) => ( 22 |
25 | { children } 26 | 32 |
33 | ) 34 | 35 | export default StyleableFileInput 36 | -------------------------------------------------------------------------------- /src/filename.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Filename extends Component { 5 | static propTypes = { 6 | id: PropTypes.number.isRequired, 7 | uploader: PropTypes.object.isRequired 8 | }; 9 | 10 | constructor(props) { 11 | super(props) 12 | 13 | this.state = { 14 | filename: props.uploader.methods.getName(props.id) 15 | } 16 | 17 | this._interceptSetName() 18 | } 19 | 20 | shouldComponentUpdate(nextProps, nextState) { 21 | return nextState.filename !== this.state.filename 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | { this.state.filename } 28 | 29 | ) 30 | } 31 | 32 | _interceptSetName() { 33 | const oldSetName = this.props.uploader.methods.setName 34 | 35 | this.props.uploader.methods.setName = (id, newName) => { 36 | oldSetName.call(this.props.uploader.methods, id, newName) 37 | 38 | if (id === this.props.id) { 39 | this.setState({ 40 | filename: newName 41 | }) 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default Filename 48 | -------------------------------------------------------------------------------- /src/filesize.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Filesize extends Component { 5 | static propTypes = { 6 | id: PropTypes.number.isRequired, 7 | units: PropTypes.shape({ 8 | byte: PropTypes.string, 9 | kilobyte: PropTypes.string, 10 | megabyte: PropTypes.string, 11 | gigabyte: PropTypes.string, 12 | terabyte: PropTypes.string 13 | }), 14 | uploader: PropTypes.object.isRequired 15 | }; 16 | 17 | static defaultProps = { 18 | units: { 19 | byte: 'B', 20 | kilobyte: 'KB', 21 | megabyte: 'MB', 22 | gigabyte: 'GB', 23 | terabyte: 'TB' 24 | } 25 | } 26 | 27 | constructor(props) { 28 | super(props) 29 | 30 | this.state = { 31 | size: props.uploader.methods.getSize(props.id) 32 | } 33 | 34 | // Don't bother to check size at upload time if scaling feature is not enabled. 35 | const scalingOption = this.props.uploader.options.scaling 36 | if (scalingOption && scalingOption.sizes.length) { 37 | // If this is a scaled image, the size won't be known until upload time. 38 | this._onUploadHandler = id => { 39 | if (id === this.props.id) { 40 | this.setState({ 41 | size: this.props.uploader.methods.getSize(id) 42 | }) 43 | } 44 | } 45 | } 46 | } 47 | 48 | componentDidMount() { 49 | this._onUploadHandler && this.props.uploader.on('upload', this._onUploadHandler) 50 | } 51 | 52 | componentWillUnmount() { 53 | this._onUploadHandler && this.props.uploader.off('upload', this._onUploadHandler) 54 | } 55 | 56 | shouldComponentUpdate(nextProps, nextState) { 57 | return nextState.size !== this.state.size || !areUnitsEqual(nextProps.units, this.props.units) 58 | } 59 | 60 | render() { 61 | const size = this.state.size 62 | 63 | if (size == null || size < 0) { 64 | return ( 65 | 66 | ) 67 | } 68 | 69 | const units = this.props.units 70 | const { formattedSize, formattedUnits } = formatSizeAndUnits({ size, units }) 71 | 72 | return ( 73 | 74 | 75 | { formattedSize } 76 | 77 | 78 | 79 | { formattedUnits } 80 | 81 | 82 | ) 83 | } 84 | } 85 | 86 | const formatSizeAndUnits = ({ size, units }) => { 87 | let formattedSize, 88 | formattedUnits 89 | 90 | if (size < 1e+3) { 91 | formattedSize = size 92 | formattedUnits = units.byte 93 | } 94 | else if (size >= 1e+3 && size < 1e+6) { 95 | formattedSize = (size / 1e+3).toFixed(2) 96 | formattedUnits = units.kilobyte 97 | } 98 | else if (size >= 1e+6 && size < 1e+9) { 99 | formattedSize = (size / 1e+6).toFixed(2) 100 | formattedUnits = units.megabyte 101 | } 102 | else if (size >= 1e+9 && size < 1e+12) { 103 | formattedSize = (size / 1e+9).toFixed(2) 104 | formattedUnits = units.gigabyte 105 | } 106 | else { 107 | formattedSize = (size / 1e+12).toFixed(2) 108 | formattedUnits = units.terabyte 109 | } 110 | 111 | return { formattedSize, formattedUnits } 112 | } 113 | 114 | const areUnitsEqual = (units1, units2) => { 115 | const keys1 = Object.keys(units1) 116 | 117 | if (keys1.length === Object.keys(units2).length) { 118 | return keys1.every(key1 => units1[key1] === units2[key1]) 119 | } 120 | 121 | return false 122 | } 123 | 124 | export default Filesize 125 | -------------------------------------------------------------------------------- /src/gallery/gallery.css: -------------------------------------------------------------------------------- 1 | [hidden] { 2 | display: none !important; 3 | } 4 | 5 | .react-fine-uploader-gallery-nodrop-container, 6 | .react-fine-uploader-gallery-dropzone { 7 | border-radius: 6px; 8 | background-color: #FAFAFA; 9 | max-height: 490px; 10 | min-height: 310px; 11 | overflow-y: hidden; 12 | padding: 15px 15px 15px 5px; 13 | position: relative; 14 | } 15 | 16 | .react-fine-uploader-gallery-dropzone { 17 | border: 2px dashed #00ABC7; 18 | } 19 | .react-fine-uploader-gallery-dropzone-upload-icon { 20 | height: 36px; 21 | margin-bottom: -6px; 22 | margin-right: 10px; 23 | width: 36px; 24 | } 25 | 26 | .react-fine-uploader-gallery-nodrop-container { 27 | border: 2px solid #00ABC7; 28 | } 29 | 30 | .react-fine-uploader-gallery-dropzone-active { 31 | background: #FDFDFD; 32 | border: 2px solid #00ABC7; 33 | } 34 | 35 | .react-fine-uploader-gallery-dropzone-content, 36 | .react-fine-uploader-gallery-nodrop-content { 37 | font-size: 36px; 38 | left: 0; 39 | opacity: 0.25; 40 | position: absolute; 41 | text-align: center; 42 | top: 38%; 43 | width: 100%; 44 | } 45 | 46 | .react-fine-uploader-gallery-file-input-container { 47 | background: #00ABC7; 48 | border: 1px solid #37B7CC; 49 | border-radius: 3px; 50 | color: #FFFFFF; 51 | display: inline; 52 | float: left; 53 | margin-left: 10px; 54 | padding-bottom: 7px; 55 | padding-left: 10px; 56 | padding-right: 10px; 57 | padding-top: 7px; 58 | text-align: center; 59 | width: 105px; 60 | } 61 | .react-fine-uploader-gallery-file-input-container:hover { 62 | background: #33B6CC; 63 | } 64 | .react-fine-uploader-gallery-file-input-container:focus { 65 | outline: 1px dotted #000000; 66 | } 67 | .react-fine-uploader-gallery-file-input-content { 68 | display: inline-block; 69 | margin-top: -2px; 70 | } 71 | .react-fine-uploader-gallery-file-input-upload-icon { 72 | fill: white; 73 | height: 24px; 74 | margin-bottom: -6px; 75 | margin-right: 5px; 76 | width: 24px; 77 | } 78 | 79 | .react-fine-uploader-gallery-progress-bar, 80 | .react-fine-uploader-gallery-total-progress-bar { 81 | border-radius: 3px; 82 | } 83 | .react-fine-uploader-gallery-progress-bar-container, 84 | .react-fine-uploader-gallery-total-progress-bar-container { 85 | background: #F2F2F2; 86 | border-radius: 3px; 87 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) inset; 88 | position: absolute; 89 | } 90 | .react-fine-uploader-gallery-total-progress-bar-container { 91 | display: inline-block; 92 | height: 25px; 93 | margin-left: 10px; 94 | margin-right: 10px; 95 | margin-top: 4px; 96 | width: 70%; 97 | } 98 | .react-fine-uploader-gallery-progress-bar, 99 | .react-fine-uploader-gallery-total-progress-bar { 100 | background: #00ABC7; 101 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) inset; 102 | height: inherit; 103 | } 104 | .react-fine-uploader-gallery-progress-bar-container { 105 | height: 15px; 106 | left: 50%; 107 | opacity: 0.9; 108 | top: 60px; 109 | transform: translateX(-50%); 110 | width: 90%; 111 | z-index: 1; 112 | } 113 | 114 | .react-fine-uploader-gallery-files { 115 | clear: both; 116 | list-style: none; 117 | max-height: 450px; 118 | overflow-y: auto; 119 | padding-left: 0; 120 | padding-top: 15px; 121 | } 122 | .react-fine-uploader-gallery-files-enter { 123 | opacity: 0.01; 124 | } 125 | .react-fine-uploader-gallery-files-enter.react-fine-uploader-gallery-files-enter-active { 126 | opacity: 1; 127 | transition: opacity 500ms ease-in; 128 | } 129 | .react-fine-uploader-gallery-files-exit { 130 | opacity: 1; 131 | } 132 | .react-fine-uploader-gallery-files-exit.react-fine-uploader-gallery-files-exit-active { 133 | opacity: 0.01; 134 | transition: opacity 300ms ease-in; 135 | } 136 | 137 | .react-fine-uploader-gallery-file { 138 | background-color: #FFFFFF; 139 | border-radius: 9px; 140 | box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.22); 141 | display: inline-block; 142 | font-size: 13px; 143 | height: 165px; 144 | line-height: 16px; 145 | margin: 0 25px 25px 10px; 146 | position: relative; 147 | vertical-align: top; 148 | width: 130px; 149 | } 150 | 151 | .react-fine-uploader-gallery-thumbnail-container { 152 | display: block; 153 | height: 130px; 154 | text-align: center; 155 | } 156 | .react-fine-uploader-gallery-thumbnail { 157 | position: relative; 158 | top: 50%; 159 | transform: translateY(-50%); 160 | } 161 | 162 | .react-fine-uploader-gallery-thumbnail-icon-backdrop, 163 | .react-fine-uploader-gallery-upload-failed-icon, 164 | .react-fine-uploader-gallery-upload-success-icon { 165 | left: 50%; 166 | opacity: 0.5; 167 | position: absolute; 168 | top: 39%; 169 | transform: translate(-50%, -50%); 170 | } 171 | .react-fine-uploader-gallery-upload-failed-icon, 172 | .react-fine-uploader-gallery-upload-success-icon { 173 | height: 60px; 174 | width: 60px; 175 | z-index: 1; 176 | } 177 | .react-fine-uploader-gallery-upload-success-icon { 178 | fill: green; 179 | } 180 | .react-fine-uploader-gallery-upload-failed-icon { 181 | fill: red; 182 | } 183 | .react-fine-uploader-gallery-thumbnail-icon-backdrop { 184 | background-color: white; 185 | border-radius: 30px; 186 | height: 50px; 187 | width: 50px; 188 | } 189 | 190 | .react-fine-uploader-gallery-file-footer { 191 | padding-left: 5px; 192 | padding-right: 5px; 193 | } 194 | 195 | .react-fine-uploader-gallery-filename { 196 | display: block; 197 | font-weight: bold; 198 | overflow: hidden; 199 | text-overflow: ellipsis; 200 | white-space: nowrap; 201 | } 202 | 203 | .react-fine-uploader-gallery-filesize { 204 | display: block; 205 | float: right; 206 | } 207 | 208 | .react-fine-uploader-gallery-status { 209 | font-style: italic; 210 | } 211 | 212 | .react-fine-uploader-gallery-cancel-button:hover svg, 213 | .react-fine-uploader-gallery-delete-button:hover svg, 214 | .react-fine-uploader-gallery-pause-resume-button:hover svg, 215 | .react-fine-uploader-gallery-retry-button:hover svg { 216 | fill: grey; 217 | } 218 | .react-fine-uploader-gallery-cancel-button:focus, 219 | .react-fine-uploader-gallery-delete-button:focus, 220 | .react-fine-uploader-gallery-pause-resume-button:focus, 221 | .react-fine-uploader-gallery-retry-button:focus { 222 | outline: none; 223 | } 224 | .react-fine-uploader-gallery-cancel-button:focus svg, 225 | .react-fine-uploader-gallery-delete-button:focus svg, 226 | .react-fine-uploader-gallery-pause-resume-button:focus svg, 227 | .react-fine-uploader-gallery-retry-button:focus svg { 228 | fill: grey; 229 | } 230 | .react-fine-uploader-gallery-cancel-button, 231 | .react-fine-uploader-gallery-delete-button, 232 | .react-fine-uploader-gallery-pause-resume-button, 233 | .react-fine-uploader-gallery-retry-button { 234 | background: transparent; 235 | border: 0; 236 | position: absolute; 237 | } 238 | .react-fine-uploader-gallery-cancel-button, 239 | .react-fine-uploader-gallery-delete-button { 240 | right: -18px; 241 | top: -12px; 242 | } 243 | .react-fine-uploader-gallery-pause-resume-button, 244 | .react-fine-uploader-gallery-retry-button { 245 | left: -18px; 246 | top: -12px 247 | } 248 | -------------------------------------------------------------------------------- /src/gallery/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import {CSSTransition, TransitionGroup} from 'react-transition-group' 4 | 5 | import CancelButton from '../cancel-button' 6 | import DeleteButton from '../delete-button' 7 | import Dropzone from '../dropzone' 8 | import FileInput from '../file-input' 9 | import Filename from '../filename' 10 | import Filesize from '../filesize' 11 | import RetryButton from '../retry-button' 12 | import PauseResumeButton from '../pause-resume-button' 13 | import ProgressBar from '../progress-bar' 14 | import Status from '../status' 15 | import Thumbnail from '../thumbnail' 16 | 17 | import PauseIcon from './pause-icon' 18 | import PlayIcon from './play-icon' 19 | import UploadIcon from './upload-icon' 20 | import UploadFailedIcon from './upload-failed-icon' 21 | import UploadSuccessIcon from './upload-success-icon' 22 | import XIcon from './x-icon' 23 | 24 | class Gallery extends Component { 25 | static propTypes = { 26 | className: PropTypes.string, 27 | uploader: PropTypes.object.isRequired 28 | }; 29 | 30 | static defaultProps = { 31 | className: '', 32 | 'cancelButton-children': , 33 | 'deleteButton-children': , 34 | 'dropzone-disabled': false, 35 | 'dropzone-dropActiveClassName': 'react-fine-uploader-gallery-dropzone-active', 36 | 'dropzone-multiple': true, 37 | 'fileInput-multiple': true, 38 | 'pauseResumeButton-pauseChildren': , 39 | 'pauseResumeButton-resumeChildren': , 40 | 'retryButton-children': , 41 | 'thumbnail-maxSize': 130 42 | } 43 | 44 | constructor(props) { 45 | super(props) 46 | 47 | this.state = { 48 | visibleFiles: [] 49 | } 50 | 51 | const statusEnum = props.uploader.qq.status 52 | 53 | this._onStatusChange = (id, oldStatus, status) => { 54 | const visibleFiles = this.state.visibleFiles 55 | 56 | if (status === statusEnum.SUBMITTED) { 57 | visibleFiles.push({ id }) 58 | this.setState({ visibleFiles }) 59 | } 60 | else if (isFileGone(status, statusEnum)) { 61 | this._removeVisibleFile(id) 62 | } 63 | else if (status === statusEnum.UPLOAD_SUCCESSFUL|| status === statusEnum.UPLOAD_FAILED) { 64 | if (status === statusEnum.UPLOAD_SUCCESSFUL) { 65 | const visibleFileIndex = this._findFileIndex(id) 66 | if (visibleFileIndex < 0) { 67 | visibleFiles.push({ id, fromServer: true }) 68 | } 69 | } 70 | this._updateVisibleFileStatus(id, status) 71 | } 72 | } 73 | } 74 | 75 | componentDidMount() { 76 | this.props.uploader.on('statusChange', this._onStatusChange) 77 | } 78 | 79 | componentWillUnmount() { 80 | this.props.uploader.off('statusChange', this._onStatusChange) 81 | } 82 | 83 | render() { 84 | const cancelButtonProps = getComponentProps('cancelButton', this.props) 85 | const dropzoneProps = getComponentProps('dropzone', this.props) 86 | const fileInputProps = getComponentProps('fileInput', this.props) 87 | const filenameProps = getComponentProps('filename', this.props) 88 | const filesizeProps = getComponentProps('filesize', this.props) 89 | const progressBarProps = getComponentProps('progressBar', this.props) 90 | const retryButtonProps = getComponentProps('retryButton', this.props) 91 | const statusProps = getComponentProps('status', this.props) 92 | const thumbnailProps = getComponentProps('thumbnail', this.props) 93 | const uploader = this.props.uploader 94 | 95 | const chunkingEnabled = uploader.options.chunking && uploader.options.chunking.enabled 96 | const deleteEnabled = uploader.options.deleteFile && uploader.options.deleteFile.enabled 97 | const deleteButtonProps = deleteEnabled && getComponentProps('deleteButton', this.props) 98 | const pauseResumeButtonProps = chunkingEnabled && getComponentProps('pauseResumeButton', this.props) 99 | 100 | return ( 101 | 0 } 103 | uploader={ uploader } 104 | { ...dropzoneProps } 105 | > 106 | { 107 | !fileInputProps.disabled && 108 | 109 | } 110 | 114 | 120 | { 121 | this.state.visibleFiles.map(({ id, status, fromServer }) => ( 122 | 127 |
  • 130 | 135 | 141 | { 142 | status === 'upload successful' && 143 | 144 | 145 |
    146 | 147 | } 148 | { 149 | status === 'upload failed' && 150 | 151 | 152 |
    153 | 154 | } 155 |
    156 | 161 | 166 | 171 |
    172 | 177 | 182 | { 183 | deleteEnabled && 184 | 189 | } 190 | { 191 | chunkingEnabled && 192 | 197 | } 198 |
  • 199 |
    200 | )) 201 | } 202 |
    203 |
    204 | ) 205 | } 206 | 207 | _removeVisibleFile(id) { 208 | const visibleFileIndex = this._findFileIndex(id) 209 | 210 | if (visibleFileIndex >= 0) { 211 | const visibleFiles = this.state.visibleFiles 212 | 213 | visibleFiles.splice(visibleFileIndex, 1) 214 | this.setState({ visibleFiles }) 215 | } 216 | } 217 | 218 | _updateVisibleFileStatus(id, status) { 219 | this.state.visibleFiles.some(file => { 220 | if (file.id === id) { 221 | file.status = status 222 | this.setState({ visibleFiles: this.state.visibleFiles }) 223 | return true 224 | } 225 | }) 226 | } 227 | 228 | _findFileIndex(id) { 229 | let visibleFileIndex = -1 230 | 231 | this.state.visibleFiles.some((file, index) => { 232 | if (file.id === id) { 233 | visibleFileIndex = index 234 | return true 235 | } 236 | }) 237 | 238 | return visibleFileIndex 239 | } 240 | } 241 | 242 | const MaybeDropzone = ({ children, content, hasVisibleFiles, uploader, ...props }) => { 243 | const { disabled, ...dropzoneProps } = props 244 | 245 | let dropzoneDisabled = disabled 246 | if (!dropzoneDisabled) { 247 | dropzoneDisabled = !uploader.qq.supportedFeatures.fileDrop 248 | } 249 | 250 | if (hasVisibleFiles) { 251 | content = 252 | } 253 | else { 254 | content = content || getDefaultMaybeDropzoneContent({ content, disabled: dropzoneDisabled }) 255 | } 256 | 257 | if (dropzoneDisabled) { 258 | return ( 259 |
    260 | { content } 261 | { children } 262 |
    263 | ) 264 | } 265 | 266 | return ( 267 | 271 | { content } 272 | { children } 273 | 274 | ) 275 | } 276 | 277 | const FileInputComponent = ({ uploader, ...props }) => { 278 | const { children, ...fileInputProps } = props 279 | const content = children || ( 280 | 281 | 282 | Select Files 283 | 284 | ) 285 | 286 | return ( 287 | 291 | 292 | { content } 293 | 294 | 295 | ) 296 | } 297 | 298 | const getComponentProps = (componentName, allProps) => { 299 | const componentProps = {} 300 | 301 | Object.keys(allProps).forEach(propName => { 302 | if (propName.indexOf(componentName + '-') === 0) { 303 | const componentPropName = propName.substr(componentName.length + 1) 304 | componentProps[componentPropName] = allProps[propName] 305 | } 306 | }) 307 | 308 | return componentProps 309 | } 310 | 311 | const getDefaultMaybeDropzoneContent = ({ content, disabled }) => { 312 | const className = disabled 313 | ? 'react-fine-uploader-gallery-nodrop-content' 314 | : 'react-fine-uploader-gallery-dropzone-content' 315 | 316 | if (disabled && !content) { 317 | return ( 318 | 319 | Upload files 320 | 321 | ) 322 | } 323 | else if (content) { 324 | return { content } 325 | } 326 | else if (!disabled) { 327 | return ( 328 | 329 | 330 | Drop files here 331 | 332 | ) 333 | } 334 | } 335 | 336 | const isFileGone = (statusToCheck, statusEnum) => { 337 | return [ 338 | statusEnum.CANCELED, 339 | statusEnum.DELETED, 340 | ].indexOf(statusToCheck) >= 0 341 | } 342 | 343 | export default Gallery 344 | -------------------------------------------------------------------------------- /src/gallery/pause-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PauseIcon = ({...props}) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default PauseIcon 13 | -------------------------------------------------------------------------------- /src/gallery/play-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PlayIcon = ({...props}) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default PlayIcon 13 | -------------------------------------------------------------------------------- /src/gallery/upload-failed-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const UploadFailIcon = ({...props}) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default UploadFailIcon 13 | -------------------------------------------------------------------------------- /src/gallery/upload-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const UploadIcon = ({...props}) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default UploadIcon 13 | -------------------------------------------------------------------------------- /src/gallery/upload-success-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const UploadSuccessIcon = ({...props}) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default UploadSuccessIcon 13 | -------------------------------------------------------------------------------- /src/gallery/x-icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const XIcon = ({...props}) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default XIcon 13 | -------------------------------------------------------------------------------- /src/pause-resume-button.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class PauseResumeButton extends Component { 5 | static propTypes = { 6 | id: PropTypes.number.isRequired, 7 | onlyRenderIfEnabled: PropTypes.bool, 8 | pauseChildren: PropTypes.node, 9 | resumeChildren: PropTypes.node, 10 | uploader: PropTypes.object.isRequired 11 | }; 12 | 13 | static defaultProps = { 14 | onlyRenderIfEnabled: true 15 | }; 16 | 17 | constructor(props) { 18 | super(props) 19 | 20 | this.state = { 21 | atLeastOneChunkUploaded: false, 22 | pausable: false, 23 | resumable: false 24 | } 25 | 26 | const statusEnum = props.uploader.qq.status 27 | 28 | this._onStatusChange = (id, oldStatus, newStatus) => { 29 | if (id === this.props.id && !this._unmounted) { 30 | const pausable = newStatus === statusEnum.UPLOADING && this.state.atLeastOneChunkUploaded 31 | const resumable = newStatus === statusEnum.PAUSED 32 | 33 | if (pausable !== this.state.pausable) { 34 | this.setState({ pausable }) 35 | } 36 | if (resumable !== this.state.resumable) { 37 | this.setState({ resumable }) 38 | } 39 | 40 | if ( 41 | newStatus === statusEnum.DELETED 42 | || newStatus === statusEnum.CANCELED 43 | || newStatus === statusEnum.UPLOAD_SUCCESSFUL 44 | ) { 45 | this._unregisterOnResumeHandler() 46 | this._unregisterOnStatusChangeHandler() 47 | this._unregisterOnUploadChunkSuccessHandler() 48 | } 49 | } 50 | } 51 | 52 | this._onClick = () => { 53 | if (this.state.pausable) { 54 | this.props.uploader.methods.pauseUpload(this.props.id) 55 | } 56 | else if (this.state.resumable) { 57 | this.props.uploader.methods.continueUpload(this.props.id) 58 | } 59 | } 60 | 61 | this._onResume = id => { 62 | if (id === this.props.id 63 | && !this._unmounted 64 | && !this.state.atLeastOneChunkUploaded) { 65 | 66 | this.setState({ 67 | atLeastOneChunkUploaded: true, 68 | pausable: true, 69 | resumable: false 70 | }) 71 | } 72 | } 73 | 74 | this._onUploadChunkSuccess = id => { 75 | if (id === this.props.id 76 | && !this._unmounted 77 | && !this.state.atLeastOneChunkUploaded) { 78 | 79 | this.setState({ 80 | atLeastOneChunkUploaded: true, 81 | pausable: true, 82 | resumable: false 83 | }) 84 | } 85 | } 86 | } 87 | 88 | 89 | componentDidMount() { 90 | this.props.uploader.on('resume', this._onResume) 91 | this.props.uploader.on('statusChange', this._onStatusChange) 92 | this.props.uploader.on('uploadChunkSuccess', this._onUploadChunkSuccess) 93 | } 94 | 95 | componentWillUnmount() { 96 | this._unmounted = true 97 | this._unregisterOnResumeHandler() 98 | this._unregisterOnStatusChangeHandler() 99 | this._unregisterOnUploadChunkSuccessHandler() 100 | } 101 | 102 | render() { 103 | const { onlyRenderIfEnabled, id, pauseChildren, resumeChildren, uploader, ...elementProps } = this.props // eslint-disable-line no-unused-vars 104 | 105 | if (this.state.pausable || this.state.resumable || !onlyRenderIfEnabled) { 106 | return ( 107 | 116 | ) 117 | } 118 | 119 | return null 120 | } 121 | 122 | _unregisterOnResumeHandler() { 123 | this.props.uploader.off('resume', this._onResume) 124 | } 125 | 126 | _unregisterOnStatusChangeHandler() { 127 | this.props.uploader.off('statusChange', this._onStatusChange) 128 | } 129 | 130 | _unregisterOnUploadChunkSuccessHandler() { 131 | this.props.uploader.off('uploadChunkSuccess', this._onUploadChunkSuccess) 132 | } 133 | } 134 | 135 | const getButtonClassName = state => { 136 | const { resumable } = state 137 | 138 | return resumable ? 'react-fine-uploader-resume-button' : 'react-fine-uploader-pause-button' 139 | } 140 | 141 | const getButtonContent = (state, props) => { 142 | const { resumable } = state 143 | const { pauseChildren, resumeChildren } = props 144 | 145 | if (resumable) { 146 | return resumeChildren || 'Resume' 147 | } 148 | 149 | return pauseChildren || 'Pause' 150 | } 151 | 152 | const getButtonLabel = state => { 153 | const { resumable } = state 154 | 155 | return resumable ? 'resume' : 'pause' 156 | } 157 | 158 | export default PauseResumeButton 159 | -------------------------------------------------------------------------------- /src/progress-bar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class ProgressBar extends Component { 5 | static propTypes = { 6 | id: PropTypes.number, 7 | hideBeforeStart: PropTypes.bool, 8 | hideOnComplete: PropTypes.bool, 9 | uploader: PropTypes.object.isRequired 10 | }; 11 | 12 | static defaultProps = { 13 | hideBeforeStart: true, 14 | hideOnComplete: true 15 | }; 16 | 17 | constructor(props) { 18 | super(props) 19 | 20 | this.state = { 21 | bytesUploaded: null, 22 | hidden: props.hideBeforeStart, 23 | totalSize: null 24 | } 25 | 26 | this._createEventHandlers() 27 | } 28 | 29 | componentDidMount() { 30 | if (this._isTotalProgress) { 31 | this.props.uploader.on('totalProgress', this._trackProgressEventHandler) 32 | } 33 | else { 34 | this.props.uploader.on('progress', this._trackProgressEventHandler) 35 | } 36 | 37 | this.props.uploader.on('statusChange', this._trackStatusEventHandler) 38 | } 39 | 40 | componentWillUnmount() { 41 | this._unmounted = true 42 | this._unregisterEventHandlers() 43 | } 44 | 45 | render() { 46 | const className = this._isTotalProgress ? 'react-fine-uploader-total-progress-bar' : 'react-fine-uploader-file-progress-bar' 47 | const customContainerClassName = this.props.className ? this.props.className + '-container' : '' 48 | const percentWidth = this.state.bytesUploaded / this.state.totalSize * 100 || 0 49 | 50 | return ( 51 |