807 | {
808 | this.state.submittedFiles.map(id => (
809 |
810 |
811 | ))
812 | }
813 |
814 | )
815 | }
816 | }
817 | ```
818 |
819 | #### `` element containing the thumbnail preview. If thumbnail generation fails, a "not available" SVG graphic will be rendered instead.
822 |
823 | ##### Properties
824 |
825 | - `customResizer(resizeInfo)` - An optional function that allows you to use a custom/3rd-library to resize thumbnail images. Documented further in [Fine Uploader's `drawThumbnail` API method documentation](https://docs.fineuploader.com/api/methods.html#drawThumbnail). See the second code example below for details.
826 |
827 | - `fromServer` - Specify whether the current file was set from [initial file](https://docs.fineuploader.com/branch/master/features/session.html)
828 |
829 | - `id` - The Fine Uploader ID of the submitted file. (required)
830 |
831 | - `maxSize` - Maps directly to the [`maxSize` parameter](http://docs.fineuploader.com/branch/master/api/methods.html#drawThumbnail) of the Fine Uploader `drawThumbnail` API method. If not supplied a default value is used, which is exported as a named constant.
832 |
833 | - `notAvailablePlaceholder` - A custom element to display if the thumbnail is not available.
834 |
835 | - `uploader` - A Fine Uploader [wrapper class](#wrapper-classes). (required)
836 |
837 | - `waitingPlaceholder` - A custom element to display while waiting for the thumbnail.
838 |
839 | Suppose you wanted to render a thumbnail for each file as new files are submitted to Fine Uploader. Your React component may look like this:
840 |
841 | Note: This assumes you have additional components or code to allow files to actually be submitted to Fine Uploader.
842 |
843 | ```javascript
844 | import React, { Component } from 'react'
845 |
846 | import FineUploaderTraditional from 'fine-uploader-wrappers'
847 | import Thumbnail from 'react-fine-uploader/thumbnail'
848 |
849 | const uploader = new FineUploaderTraditional({
850 | options: {
851 | request: {
852 | endpoint: 'my/upload/endpoint'
853 | }
854 | }
855 | })
856 |
857 | export default class FileListener extends Component {
858 | constructor() {
859 | super()
860 |
861 | this.state = {
862 | submittedFiles: []
863 | }
864 | }
865 |
866 | componentDidMount() {
867 | uploader.on('submitted', id => {
868 | const submittedFiles = this.state.submittedFiles
869 |
870 | submittedFiles.push(id)
871 | this.setState({ submittedFiles })
872 | })
873 | }
874 |
875 | render() {
876 | return (
877 |
878 | {
879 | this.state.submittedFiles.map(id => (
880 |
881 | ))
882 | }
883 |
884 | )
885 | }
886 | }
887 | ```
888 |
889 | Suppose you want to override React Fine Uploader's thumbnail generation code (especially since it trades quality for speed). The below example uses the [Pica canvas resizing library](https://github.com/nodeca/pica) to generate much higher quality thumbnail images:
890 |
891 | ```javascript
892 | import pica from 'pica/dist/pica'
893 | import React, { Component } from 'react'
894 |
895 | import FineUploaderTraditional from 'fine-uploader-wrappers'
896 | import Thumbnail from 'react-fine-uploader/thumbnail'
897 |
898 | const customResizer = resizeInfo => {
899 | return new Promise(resolve => {
900 | pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, resolve)
901 | })
902 | }
903 |
904 | const uploader = new FineUploader({
905 | options: {
906 | request: {
907 | endpoint: 'my/upload/endpoint'
908 | }
909 | }
910 | })
911 |
912 | export default class FileListener extends Component {
913 | constructor() {
914 | super()
915 |
916 | this.state = {
917 | submittedFiles: []
918 | }
919 | }
920 |
921 | componentDidMount() {
922 | uploader.on('submitted', id => {
923 | const submittedFiles = this.state.submittedFiles
924 |
925 | submittedFiles.push(id)
926 | this.setState({ submittedFiles })
927 | })
928 | }
929 |
930 | render() {
931 | return (
932 |
933 | {
934 | this.state.submittedFiles.map(id => (
935 |
939 | ))
940 | }
941 |
942 | )
943 | }
944 | }
945 | ```
946 |
--------------------------------------------------------------------------------
/config/karma.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | webpackConfig = require('./webpack.config');
3 |
4 | webpackConfig.module.rules[0].query = { plugins: ['rewire'] }
5 | webpackConfig.devtool = 'inline-source-map'
6 |
7 | module.exports = function (config) {
8 | config.set({
9 | basePath: 'src',
10 | files: [
11 | path.resolve('src/test/unit/tests.bundle.js')
12 | ],
13 | frameworks: ['jasmine'],
14 | preprocessors: (function() {
15 | var preprocessors = {}
16 | preprocessors[path.resolve('src/test/unit/tests.bundle.js')] = ['webpack', 'sourcemap']
17 | return preprocessors
18 | }()),
19 | browsers: ['Firefox'],
20 | browserNoActivityTimeout: 30000,
21 | captureTimeout: 120000,
22 | concurrency: 5,
23 | reporters: ['spec'],
24 | singleRun: true,
25 | webpack: webpackConfig,
26 | webpackMiddleware: {
27 | noInfo: true
28 | }
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
2 | const isProduction = process.env.NODE_ENV === 'production';
3 | const path = require('path')
4 |
5 | module.exports = {
6 | output: {
7 | path: path.resolve('lib'),
8 | filename: `[name].${isProduction ? 'min.js' : '.js'}`
9 | },
10 | resolve: {
11 | alias: {
12 | lib: path.resolve('lib'),
13 | src: path.resolve('src'),
14 | test: path.resolve('src/test/unit')
15 | },
16 | extensions: ['.js', '.jsx']
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js/,
22 | loader: 'babel-loader',
23 | exclude: /node_modules/
24 | },
25 | {
26 | test: /\.css$/,
27 | loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' })
28 | }
29 | ]
30 | },
31 | plugins: [new ExtractTextPlugin('[name].css')]
32 | }
33 |
--------------------------------------------------------------------------------
/config/webpack.manual-test.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpackConfig = require('./webpack.config')
3 |
4 | module.exports = Object.assign({}, webpackConfig, {
5 | devtool: 'source-map',
6 | entry: {
7 | index: [path.resolve('src/test/manual/index.jsx')]
8 | },
9 | output: {
10 | path: path.resolve('src/test/manual/bundle'),
11 | filename: '[name].js'
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/img/gallery-initial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineUploader/react-fine-uploader/d8f21116ed7e215d0702ef50e2c2b2ad0291b37d/img/gallery-initial.png
--------------------------------------------------------------------------------
/img/gallery-with-files.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FineUploader/react-fine-uploader/d8f21116ed7e215d0702ef50e2c2b2ad0291b37d/img/gallery-with-files.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fine-uploader",
3 | "version": "1.1.1",
4 | "license": "MIT",
5 | "description": "React UI components for using Fine Uploader in a React-based project.",
6 | "author": {
7 | "name": "Ray Nicholus",
8 | "url": "http://raynicholus.com"
9 | },
10 | "homepage": "https://github.com/FineUploader/react-fine-uploader#readme",
11 | "repository": "https://github.com/FineUploader/react-fine-uploader",
12 | "keywords": [
13 | "file",
14 | "blob",
15 | "file-uploader",
16 | "fine-uploader",
17 | "jsx",
18 | "react",
19 | "upload",
20 | "widget"
21 | ],
22 | "main": "gallery/index.js",
23 | "peerDependencies": {
24 | "react": "0.14.x - 16.x",
25 | "prop-types": "15.x"
26 | },
27 | "dependencies": {
28 | "fine-uploader-wrappers": "^1.0.1",
29 | "object-assign": "4.1.1",
30 | "react-transition-group": "2.x"
31 | },
32 | "devDependencies": {
33 | "babel-cli": "6.23.0",
34 | "babel-core": "6.24.0",
35 | "babel-eslint": "7.2.0",
36 | "babel-loader": "6.4.1",
37 | "babel-plugin-rewire": "1.0.0",
38 | "babel-preset-es2015": "6.24.0",
39 | "babel-preset-react": "6.23.0",
40 | "babel-plugin-syntax-class-properties": "6.13.0",
41 | "babel-plugin-transform-class-properties": "6.23.0",
42 | "babel-plugin-transform-object-rest-spread": "6.23.0",
43 | "css-loader": "0.27.3",
44 | "es6-promise": "4.1.0",
45 | "eslint": "3.18.0",
46 | "eslint-plugin-react": "6.10.3",
47 | "extract-text-webpack-plugin": "2.1.0",
48 | "fine-uploader": "5.14.2",
49 | "jasmine": "2.5.3",
50 | "jasmine-core": "2.5.2",
51 | "karma": "1.5.0",
52 | "karma-firefox-launcher": "1.0.1",
53 | "karma-jasmine": "1.1.0",
54 | "karma-sourcemap-loader": "0.3.7",
55 | "karma-spec-reporter": "0.0.30",
56 | "karma-webpack": "2.0.3",
57 | "pica": "2.0.8",
58 | "react": "16.4.1",
59 | "react-dom": "16.4.1",
60 | "prop-types": "15.5.8",
61 | "style-loader": "0.16.0",
62 | "webpack": "2.3.2",
63 | "webpack-node-externals": "1.5.4"
64 | },
65 | "engines": {
66 | "node": ">=5.0.0"
67 | },
68 | "scripts": {
69 | "build": "rm -rf lib && mkdir -p lib && rsync -av --exclude='test' src/ lib && babel lib --out-dir lib && find lib -type f -name '*.jsx' -delete",
70 | "lint": "eslint src/. --ext .js,.jsx --cache",
71 | "manual-test": "webpack --config config/webpack.manual-test.config.js --watch",
72 | "push-to-npm": "cp package.json README.md LICENSE lib && (cd lib ; npm publish)",
73 | "release": "npm run test && npm run build && npm run push-to-npm",
74 | "start": "(php -S 0.0.0.0:9090 -t src/test/manual -c src/test/manual/php.ini)",
75 | "start-no-s3": "php -S 0.0.0.0:9090 -t src/test/manual -c src/test/manual/php.ini",
76 | "test": "npm run lint && karma start config/karma.conf"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/cancel-button.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class CancelButton extends Component {
5 | static propTypes = {
6 | children: PropTypes.node,
7 | id: PropTypes.number.isRequired,
8 | onlyRenderIfCancelable: PropTypes.bool,
9 | uploader: PropTypes.object.isRequired
10 | };
11 |
12 | static defaultProps = {
13 | onlyRenderIfCancelable: true
14 | };
15 |
16 | constructor(props) {
17 | super(props)
18 |
19 | this.state = { cancelable: true }
20 |
21 | const statusEnum = props.uploader.qq.status
22 |
23 | this._onStatusChange = (id, oldStatus, newStatus) => {
24 | if (id === this.props.id && !this._unmounted) {
25 | if (!isCancelable(newStatus, statusEnum) && this.state.cancelable) {
26 | this.setState({ cancelable: false })
27 | }
28 | else if (isCancelable(newStatus, statusEnum) && !this.state.cancelable) {
29 | this.setState({ cancelable: true })
30 | }
31 | else if (newStatus === statusEnum.DELETED || newStatus === statusEnum.CANCELED) {
32 | this._unregisterStatusChangeHandler()
33 | }
34 | }
35 | }
36 |
37 | this._onClick = () => this.props.uploader.methods.cancel(this.props.id)
38 | }
39 |
40 | componentDidMount() {
41 | this.props.uploader.on('statusChange', this._onStatusChange)
42 | }
43 |
44 | componentWillUnmount() {
45 | this._unmounted = true
46 | this._unregisterStatusChangeHandler()
47 | }
48 |
49 | render() {
50 | const { children, onlyRenderIfCancelable, id, uploader, ...elementProps } = this.props // eslint-disable-line no-unused-vars
51 | const content = children || 'Cancel'
52 |
53 | if (this.state.cancelable || !onlyRenderIfCancelable) {
54 | return (
55 |
62 | { content }
63 |
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 |
72 | { content }
73 |
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 |
114 | { getButtonContent(this.state, this.props) }
115 |
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 |
62 | )
63 | }
64 |
65 | _createEventHandlers() {
66 | if (this._isTotalProgress) {
67 | this._trackProgressEventHandler = (bytesUploaded, totalSize) => {
68 | this.setState({ bytesUploaded, totalSize })
69 | }
70 | }
71 | else {
72 | this._trackProgressEventHandler = (id, name, bytesUploaded, totalSize) => {
73 | if (id === this.props.id) {
74 | this.setState({ bytesUploaded, totalSize })
75 | }
76 | }
77 | }
78 |
79 | const statusEnum = this.props.uploader.qq.status
80 |
81 | this._trackStatusEventHandler = (id, oldStatus, newStatus) => {
82 | if (!this._unmounted) {
83 | if (this._isTotalProgress) {
84 | if (!this.state.hidden
85 | && this.props.hideOnComplete
86 | && isUploadComplete(newStatus, statusEnum)
87 | && !this.props.uploader.methods.getInProgress()) {
88 |
89 | this.setState({ hidden: true })
90 | }
91 | else if (this.state.hidden && this.props.uploader.methods.getInProgress()) {
92 | this.setState({ hidden: false })
93 | }
94 | }
95 | else if (id === this.props.id) {
96 | if (this.state.hidden && newStatus === statusEnum.UPLOADING) {
97 | this.setState({ hidden: false })
98 | }
99 | else if (!this.state.hidden && this.props.hideOnComplete && isUploadComplete(newStatus, statusEnum)) {
100 | this.setState({ hidden: true })
101 | }
102 | }
103 | }
104 | }
105 | }
106 |
107 | get _isTotalProgress() {
108 | return this.props.id == null
109 | }
110 |
111 | _unregisterEventHandlers() {
112 | if (this._isTotalProgress) {
113 | this.props.uploader.off('totalProgress', this._trackProgressEventHandler)
114 | }
115 | else {
116 | this.props.uploader.off('progress', this._trackProgressEventHandler)
117 | }
118 |
119 | this.props.uploader.off('statusChange', this._trackStatusEventHandler)
120 | }
121 | }
122 |
123 | const isUploadComplete = (statusToCheck, statusEnum) => (
124 | statusToCheck === statusEnum.UPLOAD_FAILED
125 | || statusToCheck === statusEnum.UPLOAD_SUCCESSFUL
126 | || statusToCheck === statusEnum.CANCELED
127 | )
128 |
129 | export default ProgressBar
130 |
--------------------------------------------------------------------------------
/src/retry-button.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class RetryButton extends Component {
5 | static propTypes = {
6 | children: PropTypes.node,
7 | id: PropTypes.number.isRequired,
8 | onlyRenderIfRetryable: PropTypes.bool,
9 | uploader: PropTypes.object.isRequired
10 | };
11 |
12 | static defaultProps = {
13 | onlyRenderIfRetryable: true
14 | };
15 |
16 | constructor(props) {
17 | super(props)
18 |
19 | this.state = { retryable: false }
20 |
21 | this._onComplete = (id, name, response) => {
22 | if (id === this.props.id && !this._unmounted) {
23 | const retryForbidden = isRetryForbidden(response, this.props.uploader)
24 |
25 | if (!response.success && !retryForbidden && !this.state.retryable) {
26 | this.setState({ retryable: true })
27 | }
28 | else if (response.success && this.state.retryable) {
29 | this.setState({ retryable: false })
30 | }
31 | else if (retryForbidden && this.state.retryable) {
32 | this.setState({ retryable: false })
33 | this._unregisterEventHandlers()
34 | }
35 | }
36 | }
37 |
38 | this._onStatusChange = (id, oldStatus, newStatus) => {
39 | if (
40 | id === this.props.id
41 | && !this._unmounted
42 | && newStatus === props.uploader.qq.status.UPLOAD_RETRYING
43 | ) {
44 | this.setState({ retryable: false })
45 | }
46 | }
47 |
48 | this._onClick = () => this.props.uploader.methods.retry(this.props.id)
49 | }
50 |
51 | componentDidMount() {
52 | this.props.uploader.on('complete', this._onComplete)
53 | this.props.uploader.on('statusChange', this._onStatusChange)
54 | }
55 |
56 | componentWillUnmount() {
57 | this._unmounted = true
58 | this._unregisterEventHandlers()
59 | }
60 |
61 | render() {
62 | const { children, onlyRenderIfRetryable, id, uploader, ...elementProps } = this.props // eslint-disable-line no-unused-vars
63 | const content = children || 'Retry'
64 |
65 | if (this.state.retryable || !onlyRenderIfRetryable) {
66 | return (
67 |
74 | { content }
75 |
76 | )
77 | }
78 |
79 | return null
80 | }
81 |
82 | _unregisterEventHandlers() {
83 | this.props.uploader.off('complete', this._onComplete)
84 | this.props.uploader.off('statusChange', this._onStatusChange)
85 | }
86 | }
87 |
88 | const isRetryForbidden = (response, uploader) => {
89 | const preventRetryResponseProperty =
90 | (uploader.options.retry && uploader.options.retry.preventRetryResponseProperty)
91 | || 'preventRetry'
92 |
93 | return !!response[preventRetryResponseProperty]
94 | }
95 |
96 | export default RetryButton
97 |
--------------------------------------------------------------------------------
/src/status.jsx:
--------------------------------------------------------------------------------
1 | import objectAssign from 'object-assign'
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | class Status extends Component {
6 | static propTypes = {
7 | id: PropTypes.number.isRequired,
8 | className: PropTypes.string,
9 | text: PropTypes.shape({
10 | canceled: PropTypes.string,
11 | deleted: PropTypes.string,
12 | deleting: PropTypes.string,
13 | paused: PropTypes.string,
14 | queued: PropTypes.string,
15 | retrying_upload: PropTypes.string,
16 | submitting: PropTypes.string,
17 | uploading: PropTypes.string,
18 | upload_failed: PropTypes.string,
19 | upload_successful: PropTypes.string
20 | }),
21 | uploader: PropTypes.object.isRequired
22 | };
23 |
24 | static defaultProps = {
25 | className: '',
26 | text: {
27 | canceled: 'Canceled',
28 | deleted: 'Deleted',
29 | deleting: 'Deleting...',
30 | paused: 'Paused',
31 | queued: 'Queued',
32 | retrying_upload: 'Retrying...',
33 | submitting: 'Submitting...',
34 | uploading: 'Uploading...',
35 | upload_failed: 'Failed',
36 | upload_successful: 'Completed'
37 | }
38 | }
39 |
40 | constructor(props) {
41 | super(props)
42 |
43 | this.state = {
44 | status: '',
45 | text: objectAssign({}, Status.defaultProps.text, props.text || {})
46 | }
47 |
48 | this._onStatusChange = (id, oldStatus, newStatus) => {
49 | if (id === this.props.id && !this._unmounted) {
50 | const newStatusToDisplay = getStatusToDisplay({
51 | displayMap: this.state.text,
52 | status: newStatus
53 | })
54 |
55 | newStatusToDisplay && this.setState({ status: newStatusToDisplay })
56 | }
57 | }
58 | }
59 |
60 | componentDidMount() {
61 | this.props.uploader.on('statusChange', this._onStatusChange)
62 | }
63 |
64 | componentWillReceiveProps(nextProps) {
65 | if (nextProps.text) {
66 | this.setState({
67 | text: objectAssign({}, this.state.text, nextProps.text)
68 | })
69 | }
70 | }
71 |
72 | componentWillUnmount() {
73 | this._unmounted = true
74 | this._unregisterStatusChangeHandler()
75 | }
76 |
77 | render() {
78 | return (
79 |
80 | { this.state.status }
81 |
82 | )
83 | }
84 |
85 | _unregisterStatusChangeHandler() {
86 | this.props.uploader.off('statusChange', this._onStatusChange)
87 | }
88 | }
89 |
90 | const getStatusToDisplay = ({ displayMap, status }) => {
91 | let key
92 |
93 | if (status.indexOf(' ') > 0) {
94 | const statusParts = status.split(' ')
95 |
96 | key = `${statusParts[0]}_${statusParts[1]}`
97 | }
98 | else {
99 | key = status
100 | }
101 |
102 | return displayMap[key]
103 | }
104 |
105 | export default Status
106 |
--------------------------------------------------------------------------------
/src/test/manual/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "fineuploader/php-traditional-server": "1.2.1",
4 | "fineuploader/php-s3-server": "1.1.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/test/manual/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 | "This file is @generated automatically"
6 | ],
7 | "hash": "7bb03d7a40b8148d69899e7233264f57",
8 | "content-hash": "4838269b5b529d816bc32765c9f0432c",
9 | "packages": [
10 | {
11 | "name": "aws/aws-sdk-php",
12 | "version": "2.8.31",
13 | "source": {
14 | "type": "git",
15 | "url": "https://github.com/aws/aws-sdk-php.git",
16 | "reference": "64fa4b07f056e338a5f0f29eece75babaa83af68"
17 | },
18 | "dist": {
19 | "type": "zip",
20 | "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/64fa4b07f056e338a5f0f29eece75babaa83af68",
21 | "reference": "64fa4b07f056e338a5f0f29eece75babaa83af68",
22 | "shasum": ""
23 | },
24 | "require": {
25 | "guzzle/guzzle": "~3.7",
26 | "php": ">=5.3.3"
27 | },
28 | "require-dev": {
29 | "doctrine/cache": "~1.0",
30 | "ext-openssl": "*",
31 | "monolog/monolog": "~1.4",
32 | "phpunit/phpunit": "~4.0",
33 | "phpunit/phpunit-mock-objects": "2.3.1",
34 | "symfony/yaml": "~2.1"
35 | },
36 | "suggest": {
37 | "doctrine/cache": "Adds support for caching of credentials and responses",
38 | "ext-apc": "Allows service description opcode caching, request and response caching, and credentials caching",
39 | "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
40 | "monolog/monolog": "Adds support for logging HTTP requests and responses",
41 | "symfony/yaml": "Eases the ability to write manifests for creating jobs in AWS Import/Export"
42 | },
43 | "type": "library",
44 | "autoload": {
45 | "psr-0": {
46 | "Aws": "src/"
47 | }
48 | },
49 | "notification-url": "https://packagist.org/downloads/",
50 | "license": [
51 | "Apache-2.0"
52 | ],
53 | "authors": [
54 | {
55 | "name": "Amazon Web Services",
56 | "homepage": "http://aws.amazon.com"
57 | }
58 | ],
59 | "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
60 | "homepage": "http://aws.amazon.com/sdkforphp",
61 | "keywords": [
62 | "amazon",
63 | "aws",
64 | "cloud",
65 | "dynamodb",
66 | "ec2",
67 | "glacier",
68 | "s3",
69 | "sdk"
70 | ],
71 | "time": "2016-07-25 18:03:20"
72 | },
73 | {
74 | "name": "fineuploader/php-s3-server",
75 | "version": "1.1.0",
76 | "source": {
77 | "type": "git",
78 | "url": "https://github.com/FineUploader/php-s3-server.git",
79 | "reference": "40bdde75790de874a54a8e6d0a599034cb440ec7"
80 | },
81 | "dist": {
82 | "type": "zip",
83 | "url": "https://api.github.com/repos/FineUploader/php-s3-server/zipball/40bdde75790de874a54a8e6d0a599034cb440ec7",
84 | "reference": "40bdde75790de874a54a8e6d0a599034cb440ec7",
85 | "shasum": ""
86 | },
87 | "require": {
88 | "aws/aws-sdk-php": "2.*"
89 | },
90 | "type": "library",
91 | "notification-url": "https://packagist.org/downloads/",
92 | "license": [
93 | "MIT"
94 | ],
95 | "description": "Endpoint handler for Fine Uploader S3's server requests.",
96 | "homepage": "http://fineuploader.com",
97 | "time": "2015-11-05 16:41:06"
98 | },
99 | {
100 | "name": "fineuploader/php-traditional-server",
101 | "version": "1.2.0",
102 | "source": {
103 | "type": "git",
104 | "url": "https://github.com/FineUploader/php-traditional-server.git",
105 | "reference": "da217891afd218f66c5cf51363abf8e25a7fd4f0"
106 | },
107 | "dist": {
108 | "type": "zip",
109 | "url": "https://api.github.com/repos/FineUploader/php-traditional-server/zipball/da217891afd218f66c5cf51363abf8e25a7fd4f0",
110 | "reference": "da217891afd218f66c5cf51363abf8e25a7fd4f0",
111 | "shasum": ""
112 | },
113 | "type": "library",
114 | "notification-url": "https://packagist.org/downloads/",
115 | "license": [
116 | "MIT"
117 | ],
118 | "description": "Endpoint handler for Fine Uploader's traditional server requests.",
119 | "homepage": "http://fineuploader.com",
120 | "time": "2016-10-19 21:31:43"
121 | },
122 | {
123 | "name": "guzzle/guzzle",
124 | "version": "v3.9.3",
125 | "source": {
126 | "type": "git",
127 | "url": "https://github.com/guzzle/guzzle3.git",
128 | "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9"
129 | },
130 | "dist": {
131 | "type": "zip",
132 | "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9",
133 | "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9",
134 | "shasum": ""
135 | },
136 | "require": {
137 | "ext-curl": "*",
138 | "php": ">=5.3.3",
139 | "symfony/event-dispatcher": "~2.1"
140 | },
141 | "replace": {
142 | "guzzle/batch": "self.version",
143 | "guzzle/cache": "self.version",
144 | "guzzle/common": "self.version",
145 | "guzzle/http": "self.version",
146 | "guzzle/inflection": "self.version",
147 | "guzzle/iterator": "self.version",
148 | "guzzle/log": "self.version",
149 | "guzzle/parser": "self.version",
150 | "guzzle/plugin": "self.version",
151 | "guzzle/plugin-async": "self.version",
152 | "guzzle/plugin-backoff": "self.version",
153 | "guzzle/plugin-cache": "self.version",
154 | "guzzle/plugin-cookie": "self.version",
155 | "guzzle/plugin-curlauth": "self.version",
156 | "guzzle/plugin-error-response": "self.version",
157 | "guzzle/plugin-history": "self.version",
158 | "guzzle/plugin-log": "self.version",
159 | "guzzle/plugin-md5": "self.version",
160 | "guzzle/plugin-mock": "self.version",
161 | "guzzle/plugin-oauth": "self.version",
162 | "guzzle/service": "self.version",
163 | "guzzle/stream": "self.version"
164 | },
165 | "require-dev": {
166 | "doctrine/cache": "~1.3",
167 | "monolog/monolog": "~1.0",
168 | "phpunit/phpunit": "3.7.*",
169 | "psr/log": "~1.0",
170 | "symfony/class-loader": "~2.1",
171 | "zendframework/zend-cache": "2.*,<2.3",
172 | "zendframework/zend-log": "2.*,<2.3"
173 | },
174 | "suggest": {
175 | "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated."
176 | },
177 | "type": "library",
178 | "extra": {
179 | "branch-alias": {
180 | "dev-master": "3.9-dev"
181 | }
182 | },
183 | "autoload": {
184 | "psr-0": {
185 | "Guzzle": "src/",
186 | "Guzzle\\Tests": "tests/"
187 | }
188 | },
189 | "notification-url": "https://packagist.org/downloads/",
190 | "license": [
191 | "MIT"
192 | ],
193 | "authors": [
194 | {
195 | "name": "Michael Dowling",
196 | "email": "mtdowling@gmail.com",
197 | "homepage": "https://github.com/mtdowling"
198 | },
199 | {
200 | "name": "Guzzle Community",
201 | "homepage": "https://github.com/guzzle/guzzle/contributors"
202 | }
203 | ],
204 | "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle",
205 | "homepage": "http://guzzlephp.org/",
206 | "keywords": [
207 | "client",
208 | "curl",
209 | "framework",
210 | "http",
211 | "http client",
212 | "rest",
213 | "web service"
214 | ],
215 | "abandoned": "guzzlehttp/guzzle",
216 | "time": "2015-03-18 18:23:50"
217 | },
218 | {
219 | "name": "symfony/event-dispatcher",
220 | "version": "v2.8.12",
221 | "source": {
222 | "type": "git",
223 | "url": "https://github.com/symfony/event-dispatcher.git",
224 | "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8"
225 | },
226 | "dist": {
227 | "type": "zip",
228 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/889983a79a043dfda68f38c38b6dba092dd49cd8",
229 | "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8",
230 | "shasum": ""
231 | },
232 | "require": {
233 | "php": ">=5.3.9"
234 | },
235 | "require-dev": {
236 | "psr/log": "~1.0",
237 | "symfony/config": "~2.0,>=2.0.5|~3.0.0",
238 | "symfony/dependency-injection": "~2.6|~3.0.0",
239 | "symfony/expression-language": "~2.6|~3.0.0",
240 | "symfony/stopwatch": "~2.3|~3.0.0"
241 | },
242 | "suggest": {
243 | "symfony/dependency-injection": "",
244 | "symfony/http-kernel": ""
245 | },
246 | "type": "library",
247 | "extra": {
248 | "branch-alias": {
249 | "dev-master": "2.8-dev"
250 | }
251 | },
252 | "autoload": {
253 | "psr-4": {
254 | "Symfony\\Component\\EventDispatcher\\": ""
255 | },
256 | "exclude-from-classmap": [
257 | "/Tests/"
258 | ]
259 | },
260 | "notification-url": "https://packagist.org/downloads/",
261 | "license": [
262 | "MIT"
263 | ],
264 | "authors": [
265 | {
266 | "name": "Fabien Potencier",
267 | "email": "fabien@symfony.com"
268 | },
269 | {
270 | "name": "Symfony Community",
271 | "homepage": "https://symfony.com/contributors"
272 | }
273 | ],
274 | "description": "Symfony EventDispatcher Component",
275 | "homepage": "https://symfony.com",
276 | "time": "2016-07-28 16:56:28"
277 | }
278 | ],
279 | "packages-dev": [],
280 | "aliases": [],
281 | "minimum-stability": "stable",
282 | "stability-flags": [],
283 | "prefer-stable": false,
284 | "prefer-lowest": false,
285 | "platform": [],
286 | "platform-dev": []
287 | }
288 |
--------------------------------------------------------------------------------
/src/test/manual/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Manual Tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/test/manual/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import Tester from './tester'
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('content')
9 | )
10 |
--------------------------------------------------------------------------------
/src/test/manual/php.ini:
--------------------------------------------------------------------------------
1 | upload_max_filesize = 10M
2 | post_max_size = 10M
3 |
--------------------------------------------------------------------------------
/src/test/manual/tester.jsx:
--------------------------------------------------------------------------------
1 | import pica from 'pica/dist/pica'
2 | import React, { Component } from 'react'
3 |
4 | import Gallery from 'lib/gallery'
5 | import S3FineUploader from 'fine-uploader-wrappers/s3'
6 | import TraditionalFineUploader from 'fine-uploader-wrappers'
7 |
8 | import 'lib/gallery/gallery.css'
9 |
10 | const traditionalUploader = new TraditionalFineUploader({
11 | options: {
12 | chunking: {
13 | enabled: true
14 | },
15 | debug: true,
16 | deleteFile: {
17 | enabled: true,
18 | endpoint: '/vendor/fineuploader/php-traditional-server/endpoint.php'
19 | },
20 | request: {
21 | endpoint: '/vendor/fineuploader/php-traditional-server/endpoint.php'
22 | },
23 | retry: {
24 | enableAuto: true
25 | }
26 | }
27 | })
28 |
29 | const s3Uploader = new S3FineUploader({
30 | options: {
31 | chunking: {
32 | enabled: true,
33 | concurrent: {
34 | enabled: true
35 | }
36 | },
37 | debug: true,
38 | deleteFile: {
39 | enabled: true,
40 | endpoint: "/vendor/fineuploader/php-s3-server/endpoint.php"
41 | },
42 | request: {
43 | endpoint: "http://fineuploadertest.s3.amazonaws.com",
44 | accessKey: "AKIAIXVR6TANOGNBGANQ"
45 | },
46 | retry: {
47 | enableAuto: true
48 | },
49 | signature: {
50 | endpoint: "/vendor/fineuploader/php-s3-server/endpoint.php"
51 | },
52 | uploadSuccess: {
53 | endpoint: "/vendor/fineuploader/php-s3-server/endpoint.php?success"
54 | }
55 | }
56 | })
57 |
58 | class Tester extends Component {
59 | render() {
60 | return (
61 |
62 |
Traditional
63 |
66 |
67 | S3
68 |
69 |
70 | )
71 | }
72 | }
73 |
74 | const customResizer = resizeInfo => {
75 | return new Promise(resolve => {
76 | pica.resizeCanvas(resizeInfo.sourceCanvas, resizeInfo.targetCanvas, {}, resolve)
77 | })
78 | }
79 |
80 | export default Tester
81 |
--------------------------------------------------------------------------------
/src/test/unit/cancel-button.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import FineUploaderTraditional from 'fine-uploader-wrappers'
5 | import CancelButton from 'src/cancel-button'
6 |
7 | const sampleBlob = new Blob(['hi!'], { type : 'text/plain' })
8 |
9 | describe(' ', () => {
10 | let button, CancelButtonComponent, uploader
11 |
12 | beforeEach(done => {
13 | uploader = new FineUploaderTraditional({options: { autoUpload: false }})
14 |
15 | uploader.on('submitted', done)
16 |
17 | uploader.methods.addFiles(sampleBlob)
18 |
19 | CancelButtonComponent = TestUtils.renderIntoDocument(
20 |
21 | )
22 |
23 | button = TestUtils.findRenderedDOMComponentWithClass(CancelButtonComponent, 'react-fine-uploader-cancel-button')
24 | })
25 |
26 | it('renders the button for a submitted file w/ default content', () => {
27 | expect(button.disabled).toBeFalsy()
28 | expect(button.textContent).toBe('Cancel')
29 | })
30 |
31 | it('renders the button for a submitted file w/ custom content', () => {
32 | const CancelButtonComponent = TestUtils.renderIntoDocument(
33 | foo
34 | )
35 |
36 | button = TestUtils.findRenderedDOMComponentWithClass(CancelButtonComponent, 'react-fine-uploader-cancel-button')
37 | expect(button.textContent).toBe('foo')
38 | })
39 |
40 | it('allows custom attributes to be attached to the button', () => {
41 | const CancelButtonComponent = TestUtils.renderIntoDocument(
42 |
43 | )
44 |
45 | button = TestUtils.findRenderedDOMComponentWithClass(CancelButtonComponent, 'react-fine-uploader-cancel-button')
46 | expect(button.getAttribute('data-foo')).toBe('bar')
47 | })
48 |
49 | it('cancels the upload if clicked', done => {
50 | uploader.on('statusChange', (id, oldStatus, newStatus) => {
51 | if (id === 0 && newStatus === 'canceled') {
52 | expect(uploader.methods.getUploads()[0].status).toBe('canceled')
53 | done()
54 | }
55 | })
56 |
57 | TestUtils.Simulate.click(button)
58 | })
59 |
60 | it('removes the button by default if the file can no longer be canceled', done => {
61 | uploader.on('statusChange', (id, oldStatus, newStatus) => {
62 | if (id === 0 && newStatus === 'canceled') {
63 | const buttons = TestUtils.scryRenderedDOMComponentsWithClass(CancelButtonComponent, 'react-fine-uploader-cancel-button')
64 | expect(buttons.length).toBe(0)
65 | done()
66 | }
67 | })
68 |
69 | uploader.methods.cancel(0)
70 | })
71 |
72 | it('disables the button if requested when the file can no longer be canceled', done => {
73 | uploader.on('statusChange', (id, oldStatus, newStatus) => {
74 | if (id === 0 && newStatus === 'canceled') {
75 | setTimeout(() => {
76 | const buttons = TestUtils.scryRenderedDOMComponentsWithClass(CancelButtonComponent, 'react-fine-uploader-cancel-button')
77 | expect(buttons.length).toBe(1)
78 | expect(buttons[0].disabled).toBe(true)
79 | done()
80 | })
81 | }
82 | })
83 |
84 | CancelButtonComponent = TestUtils.renderIntoDocument(
85 |
86 | )
87 |
88 | uploader.methods.cancel(0)
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/src/test/unit/delete-button.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import DeleteButton from 'src/delete-button'
5 | import FineUploaderTraditional from 'fine-uploader-wrappers'
6 |
7 | describe(' ', () => {
8 | const getButton = Component => (
9 | TestUtils.scryRenderedDOMComponentsWithClass(Component, 'react-fine-uploader-delete-button')[0]
10 | )
11 | let uploader, statusChangeCallback
12 |
13 | beforeEach(() => {
14 | uploader = new FineUploaderTraditional({ options: {} })
15 |
16 | spyOn(uploader, 'on').and.callFake((type, callback) => {
17 | if (type === 'statusChange') {
18 | statusChangeCallback = callback
19 | }
20 | })
21 | })
22 |
23 | it('renders the button for a successfully uploaded file w/ default content', () => {
24 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
25 |
26 | )
27 |
28 | statusChangeCallback(0, null, 'upload successful')
29 |
30 | const button = getButton(DeleteButtonComponent)
31 | expect(button.disabled).toBeFalsy()
32 | expect(button.textContent).toBe('Delete')
33 | })
34 |
35 | it('renders the button for a successfully uploaded file w/ custom content', () => {
36 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
37 |
38 | Delete me
39 |
40 | )
41 |
42 | statusChangeCallback(0, null, 'upload successful')
43 |
44 | const button = getButton(DeleteButtonComponent)
45 | expect(button.disabled).toBeFalsy()
46 | expect(button.textContent).toBe('Delete me')
47 | })
48 |
49 | it('allows custom attributes to be attached to the button', () => {
50 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
51 |
52 | )
53 |
54 | statusChangeCallback(0, null, 'upload successful')
55 |
56 | const button = getButton(DeleteButtonComponent)
57 | expect(button.getAttribute('data-foo')).toBe('bar')
58 | })
59 |
60 | it('deletes the file if clicked', () => {
61 | const deleteFileMethod = spyOn(uploader.methods, 'deleteFile')
62 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
63 |
64 | )
65 |
66 | statusChangeCallback(0, null, 'upload successful')
67 |
68 | const button = getButton(DeleteButtonComponent)
69 | TestUtils.Simulate.click(button)
70 | expect(deleteFileMethod).toHaveBeenCalled()
71 | })
72 |
73 | it('removes the button by default if the file can no longer be deleted', () => {
74 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
75 |
76 | )
77 |
78 | statusChangeCallback(0, null, 'deleted')
79 |
80 | const button = getButton(DeleteButtonComponent)
81 | expect(button).toBeFalsy()
82 | })
83 |
84 | it('disabled the button while the delete is in progress', () => {
85 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
86 |
87 | )
88 |
89 | statusChangeCallback(0, null, 'deleting')
90 |
91 | const button = getButton(DeleteButtonComponent)
92 | expect(button.disabled).toBe(true)
93 | })
94 |
95 | it('disables the button if requested when the file can no longer be deleted', () => {
96 | const DeleteButtonComponent = TestUtils.renderIntoDocument(
97 |
98 | )
99 |
100 | statusChangeCallback(0, null, 'deleted')
101 |
102 | const button = getButton(DeleteButtonComponent)
103 | expect(button).toBeTruthy()
104 | expect(button.disabled).toBe(true)
105 | })
106 | })
107 |
--------------------------------------------------------------------------------
/src/test/unit/file-input/file-input.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import FileInput from 'src/file-input'
5 |
6 | class DummyStylableElement extends React.Component {
7 | render() {
8 | return
11 | }
12 | }
13 |
14 | describe(' ', () => {
15 | beforeEach(() => {
16 | FileInput.__Rewire__('StyleableElement', DummyStylableElement)
17 | })
18 |
19 | afterEach(() => {
20 | FileInput.__ResetDependency__('StyleableInput')
21 | })
22 |
23 | it('adds files to Fine Uploader when files are selected', () => {
24 | const addFiles = jasmine.createSpy('addFiles')
25 | const uploader = {
26 | methods: { addFiles }
27 | }
28 | const FileInputComponent =
29 | TestUtils.renderIntoDocument(click me )
30 | const fileInputElement = TestUtils.findRenderedDOMComponentWithClass(FileInputComponent, 'file-input')
31 |
32 | TestUtils.Simulate.change(fileInputElement)
33 | expect(addFiles).toHaveBeenCalled()
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/src/test/unit/file-input/styleable-element.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import { wrapStatelessComponent } from 'test/utils'
5 | import StyleableElement from 'src/file-input/styleable-element'
6 |
7 | describe(' ', () => {
8 | const WrappedStyleableElement = wrapStatelessComponent(StyleableElement)
9 |
10 | it('renders the underlying input type="file" element', () => {
11 | const StyleableElementComponent =
12 | TestUtils.renderIntoDocument(click me )
13 |
14 | expect(TestUtils.findRenderedDOMComponentWithTag(StyleableElementComponent, 'input'))
15 | .toBeTruthy()
16 | })
17 |
18 | it('passes standard attributes to the underlying file input', () => {
19 | const StyleableElementComponent =
20 | TestUtils.renderIntoDocument(
21 |
22 | click me
23 |
24 | )
25 |
26 | const fileInput = TestUtils.findRenderedDOMComponentWithTag(StyleableElementComponent, 'input')
27 | expect(fileInput.hasAttribute('multiple')).toBeTruthy()
28 | expect(fileInput.getAttribute('name')).toBe('test')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/test/unit/filename.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import FineUploaderTraditional from 'fine-uploader-wrappers'
5 | import Filename from 'src/filename'
6 |
7 | const sampleBlob = new Blob(['hi!'], { type : 'text/plain' })
8 | const sampleBlobWrapper = { blob: sampleBlob, name: 'test' }
9 |
10 | describe(' ', () => {
11 | it('renders initial filename', () => {
12 | const uploader = new FineUploaderTraditional({
13 | options: {
14 | autoUpload: false
15 | }
16 | })
17 |
18 | uploader.methods.addFiles(sampleBlobWrapper)
19 |
20 | const FilenameComponent = TestUtils.renderIntoDocument( )
21 | const filenameEl = TestUtils.findRenderedDOMComponentWithClass(FilenameComponent, 'react-fine-uploader-filename')
22 |
23 | expect(filenameEl.textContent).toBe('test')
24 | })
25 |
26 | it('updates filename on setName', () => {
27 | const uploader = new FineUploaderTraditional({
28 | options: {
29 | autoUpload: false
30 | }
31 | })
32 |
33 | uploader.methods.addFiles(sampleBlobWrapper)
34 |
35 | const FilenameComponent = TestUtils.renderIntoDocument( )
36 |
37 | uploader.methods.setName(0, 'new-name')
38 | const filenameEl = TestUtils.findRenderedDOMComponentWithClass(FilenameComponent, 'react-fine-uploader-filename')
39 | expect(filenameEl.textContent).toBe('new-name')
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/test/unit/filesize.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import FineUploaderTraditional from 'fine-uploader-wrappers'
5 | import Filesize from 'src/filesize'
6 |
7 | const sampleBlob = new Blob(['hi!'], { type : 'text/plain' })
8 | const sampleBlobWrapper = { blob: sampleBlob, name: 'test' }
9 |
10 | describe(' ', () => {
11 | const nativeObjectToString = Object.prototype.toString
12 |
13 | beforeEach(() => {
14 | Object.prototype.toString = function() {
15 | if (this && this.type === 'fakeBlob') {
16 | return '[object Blob]'
17 | }
18 |
19 | return nativeObjectToString.apply(this, arguments)
20 | }
21 | })
22 |
23 | afterEach(() => {
24 | Object.prototype.toString = nativeObjectToString
25 | })
26 |
27 | it('renders file size for tiny file using default units/text', () => {
28 | const uploader = new FineUploaderTraditional({
29 | options: {
30 | autoUpload: false
31 | }
32 | })
33 |
34 | uploader.methods.addFiles(sampleBlobWrapper)
35 |
36 | const FilesizeComponent = TestUtils.renderIntoDocument( )
37 | const filesizeEl = TestUtils.findRenderedDOMComponentWithClass(FilesizeComponent, 'react-fine-uploader-filesize')
38 |
39 | expect(filesizeEl.textContent).toBe(`${sampleBlob.size} B`)
40 | })
41 |
42 | it('renders an empty filesize component if size is not known initially', () => {
43 | const uploader = new FineUploaderTraditional({
44 | options: {
45 | autoUpload: false
46 | }
47 | })
48 |
49 | uploader.methods.addFiles({ type: 'fakeBlob' })
50 |
51 | const FilesizeComponent = TestUtils.renderIntoDocument( )
52 | const filesizeEl = TestUtils.findRenderedDOMComponentWithClass(FilesizeComponent, 'react-fine-uploader-filesize')
53 |
54 | expect(filesizeEl.textContent).toBe('')
55 | })
56 |
57 | it('renders file size for various sized files using default units/text', () => {
58 | const uploader = new FineUploaderTraditional({
59 | options: {
60 | autoUpload: false
61 | }
62 | })
63 |
64 | uploader.methods.addFiles([
65 | { size: 1100, type: 'fakeBlob' },
66 | { size: 1100000, type: 'fakeBlob' },
67 | { size: 1100000000, type: 'fakeBlob' },
68 | { size: 1100000000000, type: 'fakeBlob' }
69 | ])
70 |
71 | const expectedSizes = [
72 | '1.10 KB',
73 | '1.10 MB',
74 | '1.10 GB',
75 | '1.10 TB'
76 | ]
77 |
78 | expectedSizes.forEach((expectedSize, id) => {
79 | const FilesizeComponent = TestUtils.renderIntoDocument( )
80 | const filesizeEl = TestUtils.findRenderedDOMComponentWithClass(FilesizeComponent, 'react-fine-uploader-filesize')
81 |
82 | expect(filesizeEl.textContent).toBe(expectedSize)
83 | })
84 | })
85 |
86 | it('renders file size for various sized files using custom units/text', () => {
87 | const uploader = new FineUploaderTraditional({
88 | options: {
89 | autoUpload: false
90 | }
91 | })
92 |
93 | const customUnits = {
94 | byte: 'bytes',
95 | kilobyte: 'kilobytes',
96 | megabyte: 'megabytes',
97 | gigabyte: 'gigabytes',
98 | terabyte: 'terabytes'
99 | }
100 |
101 | uploader.methods.addFiles([
102 | { size: 1100, type: 'fakeBlob' },
103 | { size: 1100000, type: 'fakeBlob' },
104 | { size: 1100000000, type: 'fakeBlob' },
105 | { size: 1100000000000, type: 'fakeBlob' }
106 | ])
107 |
108 | const expectedSizes = [
109 | '1.10 kilobytes',
110 | '1.10 megabytes',
111 | '1.10 gigabytes',
112 | '1.10 terabytes'
113 | ]
114 |
115 | expectedSizes.forEach((expectedSize, id) => {
116 | const FilesizeComponent = TestUtils.renderIntoDocument(
117 |
118 | )
119 | const filesizeEl = TestUtils.findRenderedDOMComponentWithClass(FilesizeComponent, 'react-fine-uploader-filesize')
120 |
121 | expect(filesizeEl.textContent).toBe(expectedSize)
122 | })
123 | })
124 |
125 | it('renders file size at upload time for scaled blobs', () => {
126 | const uploader = new FineUploaderTraditional({
127 | options: {
128 | autoUpload: false,
129 | scaling: {
130 | sizes: [
131 | { name: 'test', maxSize: 100 }
132 | ]
133 | }
134 | }
135 | })
136 |
137 | let onUploadCallback
138 | spyOn(uploader, 'on').and.callFake((type, callback) => {
139 | if (type === 'upload') {
140 | onUploadCallback = callback
141 | }
142 | })
143 |
144 |
145 | const fakeBlob = { type: 'fakeBlob' }
146 | uploader.methods.addFiles(fakeBlob)
147 |
148 | const FilesizeComponent = TestUtils.renderIntoDocument( )
149 | const filesizeEl = TestUtils.findRenderedDOMComponentWithClass(FilesizeComponent, 'react-fine-uploader-filesize')
150 |
151 | expect(filesizeEl.textContent).toBe('')
152 |
153 | spyOn(uploader.methods, 'getSize').and.returnValue(1)
154 | onUploadCallback(0)
155 |
156 | expect(uploader.methods.getSize).toHaveBeenCalledWith(0)
157 | expect(filesizeEl.textContent).toBe('1 B')
158 | })
159 | })
160 |
--------------------------------------------------------------------------------
/src/test/unit/gallery.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import Dropzone from 'src/dropzone'
5 | import FileInput from 'src/file-input'
6 | import FineUploaderTraditional from 'fine-uploader-wrappers'
7 | import Gallery from 'src/gallery'
8 |
9 | const isMobile = !!('ontouchstart' in window)
10 | const sampleBlob = new Blob(['hi!'], { type : 'text/plain' })
11 | const sampleBlobWrapper = { blob: sampleBlob, name: 'test' }
12 | const sampleCannedFile = { name: 'test', uuid: 'test uuid', thumbnailUrl: 'http://localhost/images/test.jpg' }
13 |
14 | describe(' ', () => {
15 | let uploader
16 |
17 | beforeEach(() => {
18 | uploader = new FineUploaderTraditional({
19 | options: {
20 | autoUpload: false
21 | }
22 | })
23 | })
24 |
25 | if (!isMobile) {
26 | it('renders a by default', () => {
27 | const GalleryComponent = TestUtils.renderIntoDocument( )
28 | const DropzoneComponent = TestUtils.scryRenderedComponentsWithType(GalleryComponent, Dropzone)[0]
29 |
30 | expect(DropzoneComponent).toBeTruthy()
31 | })
32 | }
33 |
34 | it('does not render a if disabled via dropzone-disabled', () => {
35 | const GalleryComponent = TestUtils.renderIntoDocument(
36 |
39 | )
40 | const DropzoneComponent = TestUtils.scryRenderedComponentsWithType(GalleryComponent, Dropzone)[0]
41 |
42 | expect(DropzoneComponent).toBeFalsy()
43 | })
44 |
45 | it('renders children inside ', () => {
46 | const GalleryComponent = TestUtils.renderIntoDocument(
47 |
48 | test 123
49 |
50 | )
51 | const maybeDropzoneChild = TestUtils.scryRenderedDOMComponentsWithClass(GalleryComponent, 'gallery-child')[0]
52 |
53 | expect(maybeDropzoneChild).toBeTruthy()
54 | expect(maybeDropzoneChild.textContent).toBe('test 123')
55 | })
56 |
57 | it('renders a by default', () => {
58 | const GalleryComponent = TestUtils.renderIntoDocument( )
59 | const FileInputComponent = TestUtils.scryRenderedComponentsWithType(GalleryComponent, FileInput)[0]
60 |
61 | expect(FileInputComponent).toBeTruthy()
62 | })
63 |
64 | it('does not render a if disabled via fileInput-disabled', () => {
65 | const GalleryComponent = TestUtils.renderIntoDocument(
66 |
69 | )
70 | const FileInputComponent = TestUtils.scryRenderedComponentsWithType(GalleryComponent, FileInput)[0]
71 |
72 | expect(FileInputComponent).toBeFalsy()
73 | })
74 |
75 | it('renders a tile for each submitted file', done => {
76 | const GalleryComponent = TestUtils.renderIntoDocument( )
77 |
78 | uploader.methods.addFiles([sampleBlobWrapper, sampleBlobWrapper])
79 |
80 | setTimeout(() => {
81 | const tiles = TestUtils.scryRenderedDOMComponentsWithClass(GalleryComponent, 'react-fine-uploader-gallery-file')
82 |
83 | expect(tiles.length).toBe(2)
84 | done()
85 | }, 100)
86 | })
87 |
88 | it('removes a tile when cancel is clicked', done => {
89 | const GalleryComponent = TestUtils.renderIntoDocument(
90 |
93 | )
94 |
95 | uploader.methods.addFiles([sampleBlobWrapper, sampleBlobWrapper])
96 |
97 | setTimeout(() => {
98 | const cancelButtons = TestUtils.scryRenderedDOMComponentsWithClass(GalleryComponent, 'react-fine-uploader-gallery-cancel-button')
99 |
100 | TestUtils.Simulate.click(cancelButtons[1])
101 |
102 | setTimeout(() => {
103 | const tiles = TestUtils.scryRenderedDOMComponentsWithClass(GalleryComponent, 'react-fine-uploader-gallery-file')
104 |
105 | expect(tiles.length).toBe(1)
106 | done()
107 | }, 100)
108 | }, 100)
109 | })
110 |
111 | it('renders a tile for each initial file', done => {
112 | const GalleryComponent = TestUtils.renderIntoDocument( )
113 |
114 | uploader.methods.addInitialFiles([sampleCannedFile])
115 |
116 | setTimeout(() => {
117 | const tiles = TestUtils.scryRenderedDOMComponentsWithClass(GalleryComponent, 'react-fine-uploader-gallery-file')
118 |
119 | expect(tiles.length).toBe(1)
120 | done()
121 | }, 100)
122 | })
123 | })
124 |
--------------------------------------------------------------------------------
/src/test/unit/pause-resume-button.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import FineUploaderTraditional from 'fine-uploader-wrappers'
5 | import PauseResumeButton from 'src/pause-resume-button'
6 |
7 | describe(' ', () => {
8 | let resumeCallback,
9 | statusChangeCallback,
10 | uploadChunkSuccessCallback,
11 | uploader
12 |
13 | beforeEach(() => {
14 | uploader = new FineUploaderTraditional({options: {}})
15 |
16 | spyOn(uploader, 'on').and.callFake((type, callback) => {
17 | if (type === 'statusChange') {
18 | statusChangeCallback = callback
19 | }
20 | else if (type === 'uploadChunkSuccess') {
21 | uploadChunkSuccessCallback = callback
22 | }
23 | else if (type === 'resume') {
24 | resumeCallback = callback
25 | }
26 | })
27 | })
28 |
29 | it('by default renders a disabled pause button until the first chunk has been uploaded', () => {
30 | const PauseResumeButtonComponent = TestUtils.renderIntoDocument(
31 |
32 | )
33 |
34 | let button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-pause-button')[0]
35 | expect(button).toBeFalsy()
36 |
37 | uploadChunkSuccessCallback(0, { partIndex: 3 })
38 | button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-pause-button')[0]
39 | expect(button).toBeTruthy()
40 | })
41 |
42 | it('by default disables the pause button again when the upload is no longer actionable', () => {
43 | const PauseResumeButtonComponent = TestUtils.renderIntoDocument(
44 |
45 | )
46 |
47 | uploadChunkSuccessCallback(0, { partIndex: 1 })
48 | statusChangeCallback(0, null, 'deleted')
49 | const button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-pause-button')[0]
50 | expect(button).toBeFalsy()
51 | })
52 |
53 | it('allows a paused upload to be resumed and then paused again', () => {
54 | const PauseResumeButtonComponent = TestUtils.renderIntoDocument(
55 |
56 | )
57 |
58 | uploadChunkSuccessCallback(0, { partIndex: 7 })
59 |
60 | statusChangeCallback(0, null, 'paused')
61 | let button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-resume-button')[0]
62 | expect(button).toBeTruthy()
63 |
64 | const resumeUploadMethod = spyOn(uploader.methods, 'continueUpload')
65 | TestUtils.Simulate.click(button)
66 | expect(resumeUploadMethod).toHaveBeenCalledWith(0)
67 |
68 | statusChangeCallback(0, null, 'uploading')
69 | button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-pause-resume-button')[0]
70 | expect(button).toBeTruthy()
71 | expect(button.className.indexOf('react-fine-uploader-pause-button')).not.toBe(-1)
72 | expect(button.className.indexOf('react-fine-uploader-resume-button')).toBe(-1)
73 |
74 | const pauseUploadMethod = spyOn(uploader.methods, 'pauseUpload')
75 | TestUtils.Simulate.click(button)
76 | expect(pauseUploadMethod).toHaveBeenCalledWith(0)
77 | statusChangeCallback(0, null, 'paused')
78 | button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-pause-resume-button')[0]
79 | expect(button).toBeTruthy()
80 | expect(button.className.indexOf('react-fine-uploader-pause-button')).toBe(-1)
81 | expect(button.className.indexOf('react-fine-uploader-resume-button')).not.toBe(-1)
82 | })
83 |
84 | it('allows a resumed file to be paused immediately', () => {
85 | const PauseResumeButtonComponent = TestUtils.renderIntoDocument(
86 |
87 | )
88 |
89 | resumeCallback(0, { partIndex: 3 })
90 |
91 | let button = TestUtils.scryRenderedDOMComponentsWithClass(PauseResumeButtonComponent, 'react-fine-uploader-pause-button')[0]
92 | expect(button).toBeTruthy()
93 | expect(button.className.indexOf('react-fine-uploader-pause-button')).not.toBe(-1)
94 | expect(button.className.indexOf('react-fine-uploader-resume-button')).toBe(-1)
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/src/test/unit/progress-bar.spec.jsx:
--------------------------------------------------------------------------------
1 | import qq from 'fine-uploader/lib/core/all'
2 | import React from 'react'
3 | import TestUtils from 'react-dom/test-utils'
4 |
5 | import FineUploaderTraditional from 'fine-uploader-wrappers'
6 | import ProgressBar from 'src/progress-bar'
7 |
8 | describe(' ', () => {
9 | it('renders total progress bar when a file ID is not supplied & updates progress appropriately', () => {
10 | const uploader = new FineUploaderTraditional({options: {}})
11 | let totalProgressCallback
12 | spyOn(uploader, 'on').and.callFake((type, callback) => {
13 | if (type === 'totalProgress') {
14 | totalProgressCallback = callback
15 | }
16 | })
17 |
18 | const ProgressBarComponent = TestUtils.renderIntoDocument( )
19 |
20 | const fileProgressEls = TestUtils.scryRenderedDOMComponentsWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar')
21 | const totalProgressEls = TestUtils.scryRenderedDOMComponentsWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar')
22 |
23 | expect(fileProgressEls.length).toBe(0)
24 | expect(totalProgressEls.length).toBe(1)
25 |
26 | totalProgressCallback(100, 1000)
27 | const totalProgressEl = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar')
28 | expect(totalProgressEl.style.width).toBe('10%')
29 | })
30 |
31 | it('renders file progress bar when a file ID is supplied & updates progress appropriately', () => {
32 | const uploader = new FineUploaderTraditional({options: {}})
33 | let fileProgressCallback
34 | spyOn(uploader, 'on').and.callFake((type, callback) => {
35 | if (type === 'progress') {
36 | fileProgressCallback = callback
37 | }
38 | })
39 |
40 | const ProgressBarComponent = TestUtils.renderIntoDocument( )
41 |
42 | const fileProgressEls = TestUtils.scryRenderedDOMComponentsWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar')
43 | const totalProgressEls = TestUtils.scryRenderedDOMComponentsWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar')
44 |
45 | expect(fileProgressEls.length).toBe(1)
46 | expect(totalProgressEls.length).toBe(0)
47 |
48 | fileProgressCallback(3, 'foo.jpeg', 100, 1000)
49 | const fileProgressEl = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar')
50 | expect(fileProgressEl.style.width).toBe('10%')
51 | })
52 |
53 | it('hides total progress bar initially, by default', () => {
54 | const uploader = new FineUploaderTraditional({options: {}})
55 | const ProgressBarComponent = TestUtils.renderIntoDocument( )
56 | const totalProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar-container')
57 |
58 | expect(totalProgressElContainer.hasAttribute('hidden')).toBeTruthy()
59 | })
60 |
61 | it('does not hide total progress bar initially, if ordered to do so', () => {
62 | const uploader = new FineUploaderTraditional({options: {}})
63 | const ProgressBarComponent = TestUtils.renderIntoDocument(
64 |
65 | )
66 | const totalProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar-container')
67 |
68 | expect(totalProgressElContainer.hasAttribute('hidden')).toBeFalsy()
69 | })
70 |
71 | it('hides file progress bar initially, by default', () => {
72 | const uploader = new FineUploaderTraditional({options: {}})
73 | const ProgressBarComponent = TestUtils.renderIntoDocument( )
74 | const fileProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar-container')
75 |
76 | expect(fileProgressElContainer.hasAttribute('hidden')).toBeTruthy()
77 | })
78 |
79 | it('does not hide file progress bar initially, if ordered to do so', () => {
80 | const uploader = new FineUploaderTraditional({options: {}})
81 | const ProgressBarComponent = TestUtils.renderIntoDocument(
82 |
83 | )
84 | const fileProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar-container')
85 |
86 | expect(fileProgressElContainer.hasAttribute('hidden')).toBeFalsy()
87 | })
88 |
89 | it('hides total progress bar after all uploads are complete, by default', () => {
90 | const uploader = new FineUploaderTraditional({options: {}})
91 |
92 | let statusChangeCallback
93 | spyOn(uploader, 'on').and.callFake((type, callback) => {
94 | if (type === 'statusChange') {
95 | statusChangeCallback = callback
96 | }
97 | })
98 |
99 | const ProgressBarComponent = TestUtils.renderIntoDocument( )
100 |
101 | // uploading
102 | spyOn(uploader.methods, 'getInProgress').and.returnValue(2)
103 | statusChangeCallback(3, qq.status.QUEUED, qq.status.UPLOADING)
104 | statusChangeCallback(4, qq.status.QUEUED, qq.status.UPLOADING)
105 | let totalProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar-container')
106 | expect(totalProgressElContainer.hasAttribute('hidden')).toBeFalsy()
107 |
108 | // still uploading
109 | uploader.methods.getInProgress.and.returnValue(1)
110 | statusChangeCallback(3, qq.status.UPLOADING, qq.status.UPLOAD_SUCCESSFUL)
111 | totalProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar-container')
112 | expect(totalProgressElContainer.hasAttribute('hidden')).toBeFalsy()
113 |
114 | // done uploading
115 | uploader.methods.getInProgress.and.returnValue(0)
116 | statusChangeCallback(4, qq.status.UPLOADING, qq.status.UPLOAD_SUCCESSFUL)
117 | totalProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-total-progress-bar-container')
118 | expect(totalProgressElContainer.hasAttribute('hidden')).toBeTruthy()
119 | })
120 |
121 | it('hides file progress bar after upload is complete, by default', () => {
122 | const uploader = new FineUploaderTraditional({options: {}})
123 |
124 | let statusChangeCallback
125 | spyOn(uploader, 'on').and.callFake((type, callback) => {
126 | if (type === 'statusChange') {
127 | statusChangeCallback = callback
128 | }
129 | })
130 |
131 | const ProgressBarComponent = TestUtils.renderIntoDocument( )
132 |
133 | // uploading
134 | statusChangeCallback(3, qq.status.QUEUED, qq.status.UPLOADING)
135 | let fileProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar-container')
136 | expect(fileProgressElContainer.hasAttribute('hidden')).toBeFalsy()
137 |
138 | // done uploading
139 | statusChangeCallback(3, qq.status.UPLOADING, qq.status.UPLOAD_SUCCESSFUL)
140 | fileProgressElContainer = TestUtils.findRenderedDOMComponentWithClass(ProgressBarComponent, 'react-fine-uploader-file-progress-bar-container')
141 | expect(fileProgressElContainer.hasAttribute('hidden')).toBeTruthy()
142 | })
143 | })
144 |
--------------------------------------------------------------------------------
/src/test/unit/retry-button.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import RetryButton from 'src/retry-button'
5 | import FineUploaderTraditional from 'fine-uploader-wrappers'
6 |
7 | describe(' ', () => {
8 | const getButton = () => (
9 | TestUtils.scryRenderedDOMComponentsWithClass(
10 | RetryButtonComponent, 'react-fine-uploader-retry-button'
11 | )[0]
12 | )
13 |
14 | let onCompleteCallback, RetryButtonComponent, uploader
15 |
16 | const makeUploader = ({ options, onlyRenderIfRetryable }) => {
17 | uploader = new FineUploaderTraditional({ options })
18 |
19 | spyOn(uploader, 'on').and.callFake((type, callback) => {
20 | if (type === 'complete') {
21 | onCompleteCallback = callback
22 | }
23 | })
24 |
25 | if (onlyRenderIfRetryable != null) {
26 | RetryButtonComponent = TestUtils.renderIntoDocument(
27 |
31 | )
32 | }
33 | else {
34 | RetryButtonComponent = TestUtils.renderIntoDocument(
35 |
36 | )
37 | }
38 |
39 | }
40 |
41 | it('does not display retry button by default if upload has not failed', () => {
42 | makeUploader({ options: {} })
43 |
44 | onCompleteCallback(0, 'foo.bar', { success: true })
45 | expect(getButton()).toBeFalsy()
46 | })
47 |
48 | it('disables retry button if upload has not failed', () => {
49 | makeUploader({ options: {}, onlyRenderIfRetryable: false })
50 |
51 | onCompleteCallback(0, 'foo.bar', { success: true })
52 | expect(getButton().disabled).toBeTruthy()
53 | })
54 |
55 | it('displays retry button if upload has failed', () => {
56 | makeUploader({ options: {} })
57 |
58 | onCompleteCallback(0, 'foo.bar', { success: false })
59 | expect(getButton().disabled).toBeFalsy()
60 | })
61 |
62 | it('retries upload if button has been clicked', () => {
63 | makeUploader({ options: {} })
64 |
65 | onCompleteCallback(0, 'foo.bar', { success: false })
66 |
67 | spyOn(uploader.methods, 'retry')
68 | TestUtils.Simulate.click(getButton())
69 | expect(uploader.methods.retry).toHaveBeenCalledWith(0)
70 | })
71 |
72 | it('does not display retry button by default if upload has failed and retries are forbidden (default response property)', () => {
73 | makeUploader({ options: {} })
74 |
75 | onCompleteCallback(0, 'foo.bar', { success: false, preventRetry: true })
76 | expect(getButton()).toBeFalsy()
77 | })
78 |
79 | it('does not display retry button by default if upload has failed and retries are forbidden (custom response property)', () => {
80 | makeUploader({
81 | options: {
82 | retry: {
83 | preventRetryResponseProperty: 'dontDareRetry'
84 | }
85 | }
86 | })
87 |
88 | onCompleteCallback(0, 'foo.bar', { success: false, dontDareRetry: true })
89 | expect(getButton()).toBeFalsy()
90 | })
91 |
92 | it('disables retry button if upload has failed and retries are forbidden', () => {
93 | makeUploader({ options: {}, onlyRenderIfRetryable: false })
94 |
95 | onCompleteCallback(0, 'foo.bar', { success: false, preventRetry: true })
96 | expect(getButton().disabled).toBeTruthy()
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/test/unit/sanity.spec.js:
--------------------------------------------------------------------------------
1 | describe('sanity check', () => {
2 | it('makes sure test runner works', () => {
3 | expect(true).toBe(true)
4 | })
5 | })
6 |
--------------------------------------------------------------------------------
/src/test/unit/status.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestUtils from 'react-dom/test-utils'
3 |
4 | import Status from 'src/status'
5 | import FineUploaderTraditional from 'fine-uploader-wrappers'
6 |
7 | describe(' ', () => {
8 | const getStatus = () => (
9 | TestUtils.findRenderedDOMComponentWithClass(StatusComponent, 'react-fine-uploader-status').textContent
10 | )
11 | let StatusComponent, statusChangeCallback, uploader
12 |
13 | beforeEach(() => {
14 | uploader = new FineUploaderTraditional({ options: {} })
15 |
16 | spyOn(uploader, 'on').and.callFake((type, callback) => {
17 | if (type === 'statusChange') {
18 | statusChangeCallback = callback
19 | }
20 | })
21 |
22 | StatusComponent = TestUtils.renderIntoDocument(
23 |
24 | )
25 | })
26 |
27 | it('render nothing for a different file', () => {
28 | statusChangeCallback(1, 'upload successful', 'deleting')
29 |
30 | expect(getStatus()).toBe('')
31 | })
32 |
33 | it('render nothing for an untracked status value', () => {
34 | statusChangeCallback(0, 'deleting', 'delete failed')
35 |
36 | expect(getStatus()).toBe('')
37 | })
38 |
39 | it('renders correct default text for a single-word status value', () => {
40 | statusChangeCallback(0, 'upload successful', 'deleting')
41 |
42 | expect(getStatus()).toBe('Deleting...')
43 | })
44 |
45 | it('renders correct default text for a two-word status value', () => {
46 | statusChangeCallback(0, 'uploading', 'upload successful')
47 |
48 | expect(getStatus()).toBe('Completed')
49 | })
50 |
51 | it('renders custom text for a status value', () => {
52 | StatusComponent = TestUtils.renderIntoDocument(
53 |
54 | )
55 |
56 | statusChangeCallback(0, 'uploading', 'upload successful')
57 | expect(getStatus()).toBe('Success')
58 |
59 | statusChangeCallback(0, 'uploading', 'upload failed')
60 | expect(getStatus()).toBe('Failed')
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/src/test/unit/tests.bundle.js:
--------------------------------------------------------------------------------
1 | var context = require.context('.', true, /.+\.spec\.js(x?)?$/)
2 | context.keys().forEach(context)
3 | window.Promise = require('es6-promise').Promise
4 | module.exports = context
5 |
--------------------------------------------------------------------------------
/src/test/unit/thumbnail/index.spec.jsx:
--------------------------------------------------------------------------------
1 | import qq from 'fine-uploader/lib/core/all'
2 | import React from 'react'
3 | import TestUtils from 'react-dom/test-utils'
4 |
5 | import Thumbnail, { defaultMaxSize, notAvailableStatus, waitingStatus } from 'src/thumbnail'
6 |
7 | describe(' ', () => {
8 | let drawThumbnail, qqPromise, uploader
9 |
10 | beforeEach(() => {
11 | drawThumbnail = jasmine.createSpy('drawThumbnail')
12 |
13 | qqPromise = new qq.Promise()
14 |
15 | uploader = {
16 | methods: { drawThumbnail }
17 | }
18 | })
19 |
20 | it('renders thumbnail as canvas using default values', () => {
21 | qqPromise.success()
22 | drawThumbnail.and.returnValue(qqPromise)
23 |
24 | const ThumbnailComponent = TestUtils.renderIntoDocument(
25 |
26 | )
27 |
28 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeFalsy()
29 | expect(drawThumbnail).toHaveBeenCalledWith(3, ThumbnailComponent._canvas, defaultMaxSize, undefined, undefined)
30 | })
31 |
32 | it('renders thumbnail as canvas using passed size', () => {
33 | qqPromise.success()
34 | drawThumbnail.and.returnValue(qqPromise)
35 |
36 | const ThumbnailComponent = TestUtils.renderIntoDocument(
37 |
38 | )
39 |
40 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeFalsy()
41 | expect(drawThumbnail).toHaveBeenCalledWith(3, ThumbnailComponent._canvas, 333, undefined, undefined)
42 | })
43 |
44 | it('renders thumbnail as canvas using passed thumbnail origin', () => {
45 | qqPromise.success()
46 | drawThumbnail.and.returnValue(qqPromise)
47 |
48 | const ThumbnailComponent = TestUtils.renderIntoDocument(
49 |
50 | )
51 |
52 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeFalsy()
53 | expect(drawThumbnail).toHaveBeenCalledWith(3, ThumbnailComponent._canvas, 333, true, undefined)
54 | })
55 |
56 | it('renders default waiting placeholder until thumbnail generation is complete', () => {
57 | drawThumbnail.and.returnValue(qqPromise)
58 |
59 | const ThumbnailComponent = TestUtils.renderIntoDocument(
60 |
61 | )
62 | const placeholderEls = TestUtils.scryRenderedDOMComponentsWithClass(ThumbnailComponent, `react-fine-uploader-thumbnail-${waitingStatus}`)
63 |
64 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeTruthy()
65 | expect(placeholderEls.length).toBe(1)
66 | })
67 |
68 | it('renders custom waiting placeholder, if provided, until thumbnail generation is complete', () => {
69 | const customWaitingSvg = (
70 |
71 |
72 |
73 |
79 |
80 |
81 | )
82 | drawThumbnail.and.returnValue(qqPromise)
83 |
84 | const ThumbnailComponent = TestUtils.renderIntoDocument(
85 |
86 | )
87 | const customWaitingThumbnailEl = TestUtils.findRenderedDOMComponentWithClass(ThumbnailComponent, 'custom-waiting-thumbnail')
88 |
89 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeTruthy()
90 | expect(customWaitingThumbnailEl).toBeDefined()
91 | })
92 |
93 | it('renders default "not available" placeholder if thumbnail generation fails', () => {
94 | qqPromise.failure()
95 | drawThumbnail.and.returnValue(qqPromise)
96 |
97 | const ThumbnailComponent = TestUtils.renderIntoDocument(
98 |
99 | )
100 | const placeholderEls = TestUtils.scryRenderedDOMComponentsWithClass(ThumbnailComponent, `react-fine-uploader-thumbnail-${notAvailableStatus}`)
101 |
102 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeTruthy()
103 | expect(placeholderEls.length).toBe(1)
104 | })
105 |
106 | it('renders custom "not available" placeholder, if provided, if thumbnail generation fails', () => {
107 | const customNotAvailableSvg = (
108 |
109 |
110 |
123 | NO IMAGE
124 | AVAILABLE
125 |
126 |
127 | )
128 | qqPromise.failure()
129 | drawThumbnail.and.returnValue(qqPromise)
130 |
131 | const ThumbnailComponent = TestUtils.renderIntoDocument(
132 |
133 | )
134 | const notAvailableSvgEl = TestUtils.findRenderedDOMComponentWithClass(ThumbnailComponent, 'not-available-svg')
135 |
136 | expect(ThumbnailComponent._canvas.hasAttribute('hidden')).toBeTruthy()
137 | expect(notAvailableSvgEl).toBeDefined()
138 | })
139 | })
140 |
--------------------------------------------------------------------------------
/src/test/unit/utils.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const wrapStatelessComponent = StatelessComponent => (
4 | class Wrapper extends React.Component {
5 | render() {
6 | return StatelessComponent(this.props)
7 | }
8 | }
9 | )
10 |
--------------------------------------------------------------------------------
/src/thumbnail/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Placeholder from './placeholder'
5 |
6 | import NotAvailablePlaceholder from './not-available-placeholder'
7 | import WaitingPlaceholder from './waiting-placeholder'
8 |
9 | export const defaultMaxSize = 120
10 | export const notAvailableStatus = 'not-available'
11 | export const waitingStatus = 'waiting'
12 |
13 | class Thumbnail extends Component {
14 | static propTypes = {
15 | customResizer: PropTypes.func,
16 | fromServer: PropTypes.bool,
17 | id: PropTypes.number.isRequired,
18 | maxSize: PropTypes.number,
19 | notAvailablePlaceholder: PropTypes.element,
20 | uploader: PropTypes.object.isRequired,
21 | waitingPlaceholder: PropTypes.element
22 | };
23 |
24 | static defaultProps = {
25 | maxSize: defaultMaxSize
26 | };
27 |
28 | constructor() {
29 | super()
30 |
31 | this.state = {
32 | drawComplete: false
33 | }
34 | }
35 |
36 | componentDidMount() {
37 | this.props.uploader.methods.drawThumbnail(
38 | this.props.id,
39 | this._canvas,
40 | this.props.maxSize,
41 | this.props.fromServer,
42 | this.props.customResizer
43 | )
44 | .then(
45 | () => {
46 | this.setState({
47 | drawComplete: true,
48 | success: true
49 | })
50 | },
51 |
52 | () => {
53 | this.setState({
54 | drawComplete: true,
55 | success: false
56 | })
57 | }
58 | )
59 | }
60 |
61 |
62 | render() {
63 | const customContainerClassName = this.props.className && this.props.className + '-container'
64 |
65 | return (
66 |
67 | this._canvas = component }
70 | />
71 |
72 | { this._maybePlaceholder }
73 |
74 | )
75 | }
76 |
77 | get _failure() {
78 | return this.state.drawComplete && !this.state.success
79 | }
80 |
81 | get _maybePlaceholder() {
82 | if (this._failure) {
83 | const notAvailableImage = (
84 |
85 | )
86 |
87 | return (
88 |
93 | )
94 | }
95 | else if (!this.state.drawComplete) {
96 | const waitingImage = (
97 |
98 | )
99 |
100 | return (
101 |
106 | )
107 | }
108 |
109 | return
110 | }
111 | }
112 |
113 | export default Thumbnail
114 |
--------------------------------------------------------------------------------
/src/thumbnail/not-available-placeholder.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class NotAvailablePlaceholder extends Component {
5 | static propTypes = {
6 | maxSize: PropTypes.number
7 | };
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 | }
54 |
55 | export default NotAvailablePlaceholder
56 |
--------------------------------------------------------------------------------
/src/thumbnail/placeholder.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Placeholder = ({ className, image, size, status }) => {
5 | const style = {
6 | display: 'inline-block',
7 | maxHeight: size,
8 | maxWidth: size
9 | }
10 |
11 | return (
12 |
15 | { image }
16 |
17 | )
18 | }
19 |
20 | Placeholder.propTypes = {
21 | image: PropTypes.node.isRequired,
22 | size: PropTypes.number.isRequired,
23 | status: PropTypes.string.isRequired
24 | }
25 |
26 | export default Placeholder
27 |
--------------------------------------------------------------------------------
/src/thumbnail/waiting-placeholder.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class WaitingPlaceholder extends Component {
5 | static propTypes = {
6 | maxSize: PropTypes.number
7 | };
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 | }
21 |
22 | export default WaitingPlaceholder
23 |
--------------------------------------------------------------------------------