├── .travis.yml ├── .eslintignore ├── screenshot-upload.png ├── .gitignore ├── .babelrc ├── screenshot-upload-preview-empty.png ├── screenshot-upload-preview-with-pictures.png ├── nix-cage.json ├── .npmignore ├── .jest.json ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── src ├── Upload │ ├── index.css │ ├── index_test.js │ └── index.js └── UploadPreview │ ├── index.css │ ├── index_test.js │ └── index.js ├── shell.nix ├── scripts ├── ensure-emacs-modes └── build ├── Makefile ├── .eslintrc ├── LICENSE ├── package.json ├── stories └── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: make test 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .* 2 | 3 | /node_modules 4 | /Upload 5 | /UploadPreview 6 | /storybook* 7 | -------------------------------------------------------------------------------- /screenshot-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpix/material-ui-upload/HEAD/screenshot-upload.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .tern-port 4 | *.tgz 5 | /Upload* 6 | /index.js 7 | /storybook-static -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /screenshot-upload-preview-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpix/material-ui-upload/HEAD/screenshot-upload-preview-empty.png -------------------------------------------------------------------------------- /screenshot-upload-preview-with-pictures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corpix/material-ui-upload/HEAD/screenshot-upload-preview-with-pictures.png -------------------------------------------------------------------------------- /nix-cage.json: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": { 3 | "rw": [".", "~/.emacs.d"], 4 | "ro": ["~/.gitconfig"], 5 | "tmpfs": ["~"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.patch 2 | .* 3 | *.tgz 4 | Makefile 5 | 6 | /node_modules 7 | /src 8 | /screenshot*.png 9 | /scripts 10 | /stories 11 | /storybook-static 12 | -------------------------------------------------------------------------------- /.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "testMatch": ["**/*_test.js"], 3 | "transform": { 4 | "\\.js$": "babel-jest", 5 | "\\.css$": "jest-css-modules" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | 3 | import '@storybook/addon-actions/register'; 4 | import '@storybook/addon-links/register'; 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | 3 | import {configure} from '@storybook/react'; 4 | 5 | function loadStories() { 6 | require('../stories'); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /src/Upload/index.css: -------------------------------------------------------------------------------- 1 | .Container { 2 | position: relative; 3 | display: inline-block; 4 | box-sizing: border-box; 5 | } 6 | 7 | .FileInput { 8 | opacity: 0; 9 | position: absolute; 10 | top: 0px; 11 | left: 0px; 12 | right: 0px; 13 | bottom: 0px; 14 | } 15 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | nixpkgs = builtins.fetchTarball { 3 | url = "https://github.com/nixos/nixpkgs/archive/2633767b6042d9138b9e219d8eec69700f15110a.tar.gz"; 4 | sha256 = "08c6m7hsnfrwv88hsni4i4l0a0gl8lax500avkaq38simwh7cx8b"; 5 | }; 6 | in with import nixpkgs {}; 7 | stdenv.mkDerivation { 8 | name = "nix-shell"; 9 | buildInputs = [ 10 | nodejs-10_x 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /scripts/ensure-emacs-modes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | prepend_mode() { 5 | case "$1" in 6 | *.js) 7 | head -n 1 "$1" | grep rjsx > /dev/null || { 8 | sed -i '1s|^|// -*- mode: rjsx -*-\n|' "$1" 9 | echo "$1" 10 | } 11 | ;; 12 | esac 13 | } 14 | export -f prepend_mode 15 | 16 | find src stories -type f -name '*.js' \ 17 | | xargs -I{} bash -c "prepend_mode '{}'" 18 | -------------------------------------------------------------------------------- /src/UploadPreview/index.css: -------------------------------------------------------------------------------- 1 | .PreviewsContainer { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .PreviewContainer { 7 | position: relative; 8 | box-sizing: border-box; 9 | flex: 1 0 200px; 10 | max-height: 250px; 11 | overflow: hidden; 12 | } 13 | 14 | .Image { 15 | vertical-align: top; 16 | max-width: 100%; 17 | min-width: 100%; 18 | width: 100%; 19 | } 20 | 21 | .RemoveItem { 22 | position: absolute; 23 | bottom: 25px; 24 | right: 25px; 25 | } 26 | 27 | .ActionsContainer { 28 | display: flex; 29 | } 30 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | strictExportPresence: true, 4 | rules: [ 5 | { 6 | test: /\.css$/, 7 | use: [ 8 | 'style-loader', 9 | { 10 | loader: 'css-loader', 11 | options: { 12 | modules: true, 13 | importLoaders: 1, 14 | minimize: false 15 | } 16 | } 17 | ] 18 | }, 19 | ] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [ -z "$1" ] 5 | then 6 | echo 7 | echo "Usage: $0 " 8 | echo 9 | exit 1 10 | fi 1>&2 11 | 12 | src="$1" 13 | 14 | ls $src | xargs mkdir -p 15 | 16 | find $src -type f -name '*.*' | while read source 17 | do 18 | target=$(basename $(dirname $source))/$(basename $source) 19 | case $source in 20 | *.js) 21 | if [[ $source != *"_test.js" ]] 22 | then 23 | babel $source -o $target 24 | fi 25 | ;; 26 | *.css) 27 | cp -f $source $target 28 | ;; 29 | esac 30 | done 31 | 32 | cat < index.js 33 | module.exports = { 34 | Upload: require('./Upload'), 35 | UploadPreview: require('./UploadPreview') 36 | }; 37 | EOF 38 | -------------------------------------------------------------------------------- /src/Upload/index_test.js: -------------------------------------------------------------------------------- 1 | // -*- mode: rjsx -*- 2 | import React from 'react'; 3 | import injectTapEventPlugin from 'react-tap-event-plugin'; 4 | injectTapEventPlugin(); 5 | 6 | import {mount} from 'enzyme'; 7 | import {assert} from 'chai'; 8 | import PropTypes from 'prop-types'; 9 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 10 | import Upload from './index'; 11 | 12 | 13 | const mountWithTheme = (node) => mount( 14 | node, 15 | { 16 | context: {muiTheme: getMuiTheme()}, 17 | childContextTypes: {muiTheme: PropTypes.object} 18 | } 19 | ); 20 | 21 | describe('Upload', () => { 22 | const newUpload = (props = {}) => mountWithTheme( 23 | 24 | ); 25 | 26 | it( 27 | 'always renders an input', 28 | () => { 29 | const nodes = newUpload().find('input'); 30 | assert.equal(nodes.length, 1); 31 | } 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /src/UploadPreview/index_test.js: -------------------------------------------------------------------------------- 1 | // -*- mode: rjsx -*- 2 | import React from 'react'; 3 | import injectTapEventPlugin from 'react-tap-event-plugin'; 4 | injectTapEventPlugin(); 5 | 6 | import {mount} from 'enzyme'; 7 | import {assert} from 'chai'; 8 | import PropTypes from 'prop-types'; 9 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 10 | import UploadPreview from './index'; 11 | 12 | 13 | const mountWithTheme = (node) => mount( 14 | node, 15 | { 16 | context: {muiTheme: getMuiTheme()}, 17 | childContextTypes: {muiTheme: PropTypes.object} 18 | } 19 | ); 20 | 21 | describe('UploadPreview', () => { 22 | const newUploadPreview = (props = {}) => mountWithTheme( 23 | 24 | ); 25 | 26 | it( 27 | 'always renders a div', 28 | () => { 29 | const nodes = newUploadPreview().find('> div'); 30 | assert.equal(nodes.length, 1); 31 | } 32 | ); 33 | }); 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | 3 | name := material-ui-upload 4 | modules := ./node_modules 5 | bin := $(modules)/.bin 6 | src := ./src 7 | scripts := ./scripts 8 | PATH := $(scripts):$(bin):$(PATH) 9 | 10 | export PATH 11 | 12 | .PHONY: all 13 | all: build 14 | 15 | .PHONY: build 16 | build: $(modules) $(src) 17 | $(scripts)/ensure-emacs-modes 18 | $(scripts)/build $(src) 19 | 20 | .PHONY: test 21 | test: $(modules) lint build 22 | $(bin)/jest --config .jest.json 23 | 24 | .PHONY: $(modules) lint 25 | lint: 26 | $(bin)/eslint . 27 | 28 | package-lock.json $(modules): package.json 29 | npm install --ignore-scripts 30 | touch package-lock.json $(modules) 31 | 32 | .PHONY: tag 33 | tag: 34 | git tag $(shell jq -r .version package.json) 35 | 36 | .PHONY: server 37 | server: build $(modules) 38 | if [ ! -e $(modules)/$(name) ]; \ 39 | then \ 40 | ln -s $(PWD) $(modules)/$(name); \ 41 | fi 42 | $(bin)/start-storybook -p 9001 -c .storybook 43 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaFeatures": { 4 | "jsx": true, 5 | "modules": true 6 | } 7 | }, 8 | "env": { 9 | "browser": true, 10 | "node": true 11 | }, 12 | "parser": "babel-eslint", 13 | "rules": { 14 | "quotes": [ 15 | 2, 16 | "single" 17 | ], 18 | "strict": [ 19 | 2, 20 | "never" 21 | ], 22 | "no-console": 1, 23 | "react/display-name": 0, 24 | "react/jsx-no-undef": 1, 25 | "react/jsx-sort-props": 0, 26 | "react/jsx-uses-react": 2, 27 | "react/jsx-uses-vars": 2, 28 | "react/jsx-wrap-multilines": 1, 29 | "react/no-did-mount-set-state": 1, 30 | "react/no-did-update-set-state": 1, 31 | "react/no-multi-comp": 1, 32 | "react/no-unknown-property": 1, 33 | "react/prop-types": 1, 34 | "react/react-in-jsx-scope": 2, 35 | "react/self-closing-comp": 1 36 | }, 37 | "plugins": [ 38 | "react", 39 | "babel", 40 | "jest" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dmitry Moskowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui-upload", 3 | "version": "1.2.1", 4 | "description": "Upload controls made in material-ui.", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublish": "make build" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/corpix/material-ui-upload.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "material", 16 | "upload", 17 | "preview", 18 | "file" 19 | ], 20 | "author": "Dmitry Moskowski ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/corpix/material-ui-upload/issues" 24 | }, 25 | "homepage": "https://github.com/corpix/material-ui-upload#readme", 26 | "devDependencies": { 27 | "@storybook/cli": "^5.2.0-alpha.22", 28 | "@storybook/react": "^5.2.0-alpha.22", 29 | "@babel/cli": "^7.4.4", 30 | "@babel/core": "^7.4.5", 31 | "@babel/plugin-proposal-class-properties": "^7.4.4", 32 | "@babel/preset-react": "^7.0.0", 33 | "babel-eslint": "^11.0.0-beta.0", 34 | "babel-jest": "^24.8.0", 35 | "chai": "^4.2.0", 36 | "css-loader": "^2.1.1", 37 | "enzyme": "^3.10.0", 38 | "eslint": "^6.0.0-alpha.2", 39 | "eslint-plugin-babel": "^5.3.0", 40 | "eslint-plugin-jest": "^22.6.4", 41 | "eslint-plugin-react": "^7.13.0", 42 | "jest": "^24.8.0", 43 | "jest-css-modules": "^2.1.0", 44 | "react-test-renderer": "^16.8.6", 45 | "regenerator-runtime": "^0.13.2", 46 | "style-loader": "^0.23.1" 47 | }, 48 | "dependencies": { 49 | "babel-loader": "^8.0.6", 50 | "jshashes": "^1.0.7", 51 | "material-ui": "^0.20.2", 52 | "prop-types": "^15.7.2", 53 | "react": "^16.8.6", 54 | "react-dom": "^16.8.6", 55 | "react-tap-event-plugin": "^3.0.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | // -*- mode: rjsx -*- 2 | import React from 'react'; 3 | 4 | import injectTapEventPlugin from 'react-tap-event-plugin'; 5 | injectTapEventPlugin(); 6 | 7 | import {storiesOf} from '@storybook/react'; 8 | import {action} from '@storybook/addon-actions'; 9 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 10 | 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 12 | import RaisedButton from 'material-ui/RaisedButton'; 13 | import Upload from 'material-ui-upload/Upload'; 14 | import UploadPreview from 'material-ui-upload/UploadPreview'; 15 | 16 | let theme = (v) => ( 17 | 18 | {v} 19 | 20 | ); 21 | 22 | storiesOf('Upload', module) 23 | .add( 24 | 'Default button', 25 | () => theme( 26 | 30 | ) 31 | ) 32 | .add( 33 | 'RaisedButton', 34 | () => theme( 35 | 40 | ) 41 | ) 42 | ; 43 | 44 | storiesOf('UploadPreview', module) 45 | .add( 46 | 'Default button', 47 | () => theme( 48 | 54 | ) 55 | ) 56 | .add( 57 | 'RaisedButton', 58 | () => theme( 59 | 66 | ) 67 | ) 68 | ; 69 | -------------------------------------------------------------------------------- /src/Upload/index.js: -------------------------------------------------------------------------------- 1 | // *-* mode: rjsx -*- 2 | import React, {Component} from 'react'; 3 | import propTypes from 'prop-types'; 4 | import {findDOMNode} from 'react-dom'; 5 | import FlatButton from 'material-ui/FlatButton'; 6 | 7 | import styles from './index.css'; 8 | 9 | 10 | export default class Upload extends Component { 11 | 12 | static defaultProps = { 13 | fileTypeRegex: /.*/, 14 | onFileLoad: (e) => undefined, 15 | buttonControl: FlatButton 16 | }; 17 | 18 | static propTypes = { 19 | fileTypeRegex: propTypes.object, 20 | onFileLoad: propTypes.func, 21 | buttonControl: propTypes.func 22 | }; 23 | 24 | exclusiveProps = [ 25 | 'fileTypeRegex', 26 | 'onFileLoad', 27 | 'buttonControl' 28 | ]; 29 | 30 | onInputChange = (e) => { 31 | e.target.files 32 | .filter( 33 | (file) => file.type.match(this.props.fileTypeRegex) !== null 34 | ) 35 | .forEach( 36 | (file) => { 37 | let reader = new FileReader(); 38 | reader.onload = (e) => this.props.onFileLoad(e, file); 39 | reader.readAsDataURL(file); 40 | } 41 | ); 42 | }; 43 | 44 | getButtonProps = () => { 45 | return Object 46 | .keys(this.props) 47 | .filter( 48 | (name) => this.exclusiveProps.indexOf(name) === -1 49 | ) 50 | .reduce( 51 | (acc, name) => { 52 | acc[name] = this.props[name]; 53 | return acc; 54 | }, 55 | {} 56 | ); 57 | }; 58 | 59 | render() { 60 | return ( 61 |
62 | 63 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | material-ui-upload 2 | ---------------------- 3 | 4 | [![Build Status](https://travis-ci.org/corpix/material-ui-upload.svg?branch=master)](https://travis-ci.org/corpix/material-ui-upload) 5 | 6 | Upload controls made in material-ui using [FileAPI][file-api] 7 | 8 | # Components 9 | 10 | ## Upload 11 | 12 | Upload button, based on [FlatButton][flat-button]. 13 | 14 | ![Upload button](screenshot-upload.png) 15 | 16 | ## Upload Preview 17 | 18 | Upload with preview for images, based on [Card][card]. 19 | 20 | > Empty 21 | 22 | ![Upload preview](screenshot-upload-preview-empty.png) 23 | 24 | > With pictures 25 | 26 | ![Upload preview](screenshot-upload-preview-with-pictures.png) 27 | 28 | # Requirements 29 | 30 | - Your project should be configured to use CSS modules 31 | 32 | # Installation 33 | 34 | ``` shell 35 | yarn add material-ui-upload 36 | ``` 37 | 38 | Or with npm 39 | 40 | ```shell 41 | npm i --save material-ui-upload 42 | ``` 43 | 44 | # Usage example 45 | 46 | > You could see live examples providen with storybook, just `make server` in the root of the repository. 47 | 48 | ## Upload 49 | 50 | ``` jsx 51 | import React, {Component} from 'react'; 52 | import Upload from 'material-ui-upload/Upload'; 53 | 54 | class MyComponent extends Component { 55 | onFileLoad = (e, file) => console.log(e.target.result, file.name); 56 | 57 | render() { 58 | return ( 59 | 60 | ); 61 | } 62 | } 63 | 64 | ``` 65 | 66 | ## UploadPreview 67 | 68 | ``` jsx 69 | import React, {Component} from 'react'; 70 | import UploadPreview from 'material-ui-upload/UploadPreview'; 71 | 72 | class MyComponent extends Component { 73 | constructor() { 74 | super(); 75 | this.state = { 76 | pictures: {} 77 | }; 78 | } 79 | 80 | onChange = (pictures) => this.setState({pictures}); 81 | 82 | render() { 83 | return ( 84 | 90 | ); 91 | } 92 | } 93 | ``` 94 | 95 | # Properties 96 | 97 | ## Upload 98 | 99 | [FlatButton][flat-button] props can be used on this component. 100 | 101 | > For FlatButton props please see [material-ui docs][flat-button]. 102 | 103 | | Name | Type | Default | Description | 104 | | ---- | ---- | ------- | ----------- | 105 | | fileTypeRegex | `RegExp` | `/.*/` | Regexp that matches allowed file names. | 106 | | onFileLoad | `function` | `(e, file) => undefined` | [FileReader#onload][onload] event handler which receives a `FileReader` event and original file object. | 107 | | buttonControl | `function` | `material-ui/FlatButton` | Control constructor to use as upload button. | 108 | 109 | ## UploadPreview 110 | 111 | Upload component props can be used on this component. 112 | 113 | | Name | Type | Default | Description | 114 | | ---- | ---- | ------- | ----------- | 115 | | title | `string` | `''` | Title of the [Card][card]. | 116 | | onFileLoad | `function` | `(e, file) => undefined` | [FileReader#onload][onload] event handler which receives a `FileReader` event and original file object. | 117 | | label | `string` | `Upload` | Upload button label. | 118 | | onChange | `function` | `(items) => undefined` | When state of the component changes(file added, removed, removed all) this function will be fired with a hash of items as argument(each item key is a sha1 of the base64 dataURI __this may change to 'hash of a file content' in the future__). | 119 | | initialItems | `object` | `{}` | Predefined items. | 120 | 121 | # License 122 | 123 | [MIT](/LICENSE) 124 | 125 | 126 | [card]: http://www.material-ui.com/#/components/card 127 | [flat-button]: http://www.material-ui.com/#/components/flat-button 128 | [file-api]: https://developer.mozilla.org/en-US/docs/Web/API/File 129 | [onload]: https://developer.mozilla.org/ru/docs/Web/API/FileReader/onload 130 | -------------------------------------------------------------------------------- /src/UploadPreview/index.js: -------------------------------------------------------------------------------- 1 | // *-* mode: rjsx -*- 2 | import React, {Component} from 'react'; 3 | import propTypes from 'prop-types'; 4 | import {Card, CardHeader, CardMedia, CardActions} from 'material-ui/Card'; 5 | import FloatingActionButton from 'material-ui/FloatingActionButton'; 6 | import ContentRemove from 'material-ui/svg-icons/content/remove'; 7 | import FlatButton from 'material-ui/FlatButton'; 8 | import {SHA1} from 'jshashes'; 9 | 10 | import Upload from 'material-ui-upload/Upload'; 11 | 12 | import styles from './index.css'; 13 | 14 | 15 | export default class UploadPreview extends Component { 16 | 17 | static defaultProps = { 18 | title: '', 19 | label: 'Upload', 20 | fileTypeRegex: /^image.*$/, 21 | onFileLoad: (e, file) => undefined, 22 | onChange: (items) => undefined, 23 | initialItems: {} 24 | }; 25 | 26 | static propTypes = { 27 | title: propTypes.string, 28 | label: propTypes.string, 29 | fileTypeRegex: propTypes.object, 30 | onFileLoad: propTypes.func, 31 | onChange: propTypes.func, 32 | initialItems: propTypes.object 33 | }; 34 | 35 | exclusiveProps = [ 36 | 'title', 37 | 'children', 38 | 'onFileLoad', 39 | 'initialItems' 40 | ]; 41 | 42 | constructor(props) { 43 | super(); 44 | this.state = {items: props.initialItems}; 45 | }; 46 | 47 | onFileLoad = (e, file) => { 48 | let hash = new SHA1().hex(e.target.result); 49 | let items = {...this.state.items}; 50 | items[hash] = e.target.result; 51 | this.setState({items}); 52 | 53 | this.props.onFileLoad(e, file); 54 | this.props.onChange(items); 55 | }; 56 | 57 | onRemoveAllClick = (e) => { 58 | let items = {}; 59 | this.setState({items}); 60 | this.props.onChange(items); 61 | }; 62 | 63 | onRemoveClick = (key) => (e) => { 64 | let items = {...this.state.items}; 65 | delete items[key]; 66 | this.setState({items}); 67 | this.props.onChange(items); 68 | }; 69 | 70 | getUploadProps() { 71 | return Object 72 | .keys(this.props) 73 | .filter( 74 | (name) => this.exclusiveProps.indexOf(name) === -1 75 | ) 76 | .reduce( 77 | (acc, name) => { 78 | acc[name] = this.props[name]; 79 | return acc; 80 | }, 81 | {onFileLoad: this.onFileLoad} 82 | ); 83 | }; 84 | 85 | renderPreview = (key) => ( 86 |
87 | 88 | 93 | 94 | 95 |
96 | ); 97 | 98 | renderPreviews = () => ( 99 |
100 | { 101 | Object 102 | .keys(this.state.items) 103 | .map(this.renderPreview) 104 | } 105 |
106 | ); 107 | 108 | renderAddButton = () => ( 109 | React.createElement( 110 | Upload, 111 | { 112 | onFileLoad: this.onFileLoad, 113 | // XXX: Force re-render on items change 114 | // see: https://github.com/corpix/material-ui-upload/issues/8 115 | key: Object.keys(this.state.items).length, 116 | ...this.getUploadProps() 117 | } 118 | ) 119 | ); 120 | 121 | renderRemoveButton = () => ( 122 | 133 | ); 134 | 135 | render() { 136 | return ( 137 | 138 | 139 | 140 | {this.renderPreviews()} 141 | 142 | 143 |
144 | {this.renderAddButton()} 145 | {this.renderRemoveButton()} 146 |
147 |
148 |
149 | ); 150 | }; 151 | } 152 | --------------------------------------------------------------------------------