├── .gitignore ├── .npmignore ├── README.md ├── config ├── default.coffee ├── dev.coffee ├── static.coffee └── ws.coffee ├── entry └── template.coffee ├── gulpfile.coffee ├── package.json ├── packing ├── asset-links.coffee ├── webpack-build.coffee └── webpack-dev.coffee └── src ├── app └── page.coffee ├── demo.css ├── index.coffee ├── main.coffee └── upload-util.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules 3 | /build 4 | /index.html 5 | /lib 6 | 7 | /packing/assets.json 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | /build 3 | /gulpfile.coffee 4 | /packing 5 | /entry 6 | /config 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | React Lite Uploader 3 | ---- 4 | 5 | > This project is no mature, do not use it in production! 6 | 7 | Uploader component from Talk by Teambition. 8 | 9 | Demo http://ui.talk.ai/react-lite-uploader 10 | 11 | Based on the work of https://github.com/mailru/FileAPI 12 | 13 | ### Supposition 14 | 15 | Contains internal business logic defined at Talk by Teambition. 16 | 17 | ### Usage 18 | 19 | ```bash 20 | npm i --save react-lite-uploader 21 | ``` 22 | 23 | ```coffee 24 | UploadButton = require('react-lite-uploader').Button 25 | UploadArea = require('react-lite-uploader').Area 26 | uploadUtil = require('react-lite-uploader').util 27 | ``` 28 | 29 | #### `UploadButton`, `UploadArea`: React Components 30 | 31 | props: 32 | 33 | * `url`, string, server url to upload files, required 34 | * `headers`, object of headers, optional 35 | * `accept`, string of accepted types, seperated by commas like `jpg,png`, optional, defaults to `''` 36 | * `multiple`, bool, optional, defaults to `false` 37 | * `onFileHover`, function to response to dragging files, optional(only used handling dropping files) 38 | * `onCreate`, function, optional 39 | * `onProgress`, function, optional 40 | * `onSuccess`, function, required 41 | * `onError`, function, required 42 | 43 | Notice: `UploadArea` is not suggested due to the lack of flexibility, use `uploadUtil` instead. 44 | 45 | ##### `uploadUtil`: utilities to handle dropping and pasting 46 | 47 | * `uploadUtil.handleFileDropping` `(event, props) ->` 48 | * `uploadUtil.handlePasteEvent` `(targetElement, props) ->` 49 | * `uploadUtil.onFilesLoad` `(files, props) ->` 50 | 51 | Remember to include `react-lite-uploader/src/styles.css` in you project. 52 | 53 | Read [`page.coffee`][example] for details. 54 | 55 | [example]: https://github.com/teambition/react-lite-uploader/blob/master/src/app/page.coffee 56 | 57 | ### Develop 58 | 59 | Based on Jianliao's project template: 60 | 61 | https://github.com/teambition/coffee-webpack-starter 62 | 63 | ### License 64 | 65 | MIT 66 | -------------------------------------------------------------------------------- /config/default.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | env: 'dev' 4 | webpackDevPort: 8015 5 | isMinified: no 6 | uploadUrl: 'https://striker.teambition.net/upload' 7 | 8 | # token of striker, expires at Dec 19 9 | token: "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0NTMyODgzOTEsInN0b3JhZ2UiOiJzdHJpa2VyLWh6In0.BjPn5yYfYggs2kemcbgVhs2u6snCbfPqcGbN4DwKlCk" -------------------------------------------------------------------------------- /config/dev.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = {} 3 | -------------------------------------------------------------------------------- /config/static.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | env: 'static' 4 | isMinified: no 5 | -------------------------------------------------------------------------------- /config/ws.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | env: 'ws' 4 | isMinified: yes 5 | -------------------------------------------------------------------------------- /entry/template.coffee: -------------------------------------------------------------------------------- 1 | 2 | stir = require 'stir-template' 3 | React = require 'react' 4 | config = require 'config' 5 | 6 | assetLinks = require '../packing/asset-links' 7 | 8 | Page = React.createFactory require '../src/app/page' 9 | 10 | {html, head, title, body, meta, script, link, div, a, span} = stir 11 | 12 | module.exports = -> 13 | 14 | stir.render stir.doctype(), 15 | html null, 16 | head null, 17 | title null, "React Lite Uploader" 18 | meta charset: 'utf-8' 19 | link rel: 'icon', href: 'http://tp4.sinaimg.cn/5592259015/180/5725970590/1' 20 | if assetLinks.style? 21 | link rel: 'stylesheet', href: assetLinks.style 22 | script null, "window._initialStore = (#{JSON.stringify(config)})" 23 | script src: assetLinks.vendor, defer: true 24 | script src: assetLinks.main, defer: true 25 | body null, 26 | div class: 'intro', 27 | div class: 'title', "Demo of Uploader" 28 | div null, 29 | span null, "Read more at " 30 | a href: 'http://github.com/teambition/react-lite-uploader', 31 | 'github.com/teambition/react-lite-uploader' 32 | span null, '.' 33 | div null, 'In the demo below we need a token, which may be expired...' 34 | div class: 'demo', 35 | React.renderToString Page() 36 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | gulp = require 'gulp' 4 | gutil = require 'gulp-util' 5 | config = require 'config' 6 | webpack = require 'webpack' 7 | sequence = require 'run-sequence' 8 | WebpackDevServer = require 'webpack-dev-server' 9 | 10 | 11 | gulp.task 'del-lib', (cb) -> 12 | del = require('del') 13 | del 'lib', cb 14 | 15 | gulp.task 'script', ['del-lib'], -> 16 | coffee = require('gulp-coffee') 17 | gulp 18 | .src 'src/**/*.coffee' 19 | .pipe coffee(bare: true) 20 | .pipe gulp.dest('lib/') 21 | 22 | gulp.task 'rsync', (cb) -> 23 | wrapper = require 'rsyncwrapper' 24 | wrapper.rsync 25 | ssh: true 26 | src: ['build/*'] 27 | recursive: true 28 | args: ['--verbose'] 29 | dest: 'talk-ui:/teambition/server/talk-ui/react-lite-uploader' 30 | deleteAll: true 31 | , (error, stdout, stderr, cmd) -> 32 | if error? 33 | throw error 34 | console.error stderr 35 | console.log cmd 36 | cb() 37 | 38 | gulp.task 'html', (cb) -> 39 | html = require('./entry/template') 40 | fs = require('fs') 41 | fs.writeFile 'build/index.html', html(), cb 42 | 43 | gulp.task 'del', (cb) -> 44 | del = require('del') 45 | del 'build/**/*', cb 46 | 47 | # webpack tasks 48 | 49 | gulp.task 'webpack-dev', (cb) -> 50 | webpackDev = require './packing/webpack-dev' 51 | webpackServer = 52 | publicPath: '/' 53 | hot: true 54 | stats: 55 | colors: true 56 | info = 57 | __dirname: __dirname 58 | env: config.env 59 | 60 | compiler = webpack (webpackDev info) 61 | server = new WebpackDevServer compiler, webpackServer 62 | 63 | server.listen config.webpackDevPort, 'localhost', (err) -> 64 | if err? 65 | throw new gutil.PluginError("webpack-dev-server", err) 66 | gutil.log "[webpack-dev-server] is running..." 67 | cb() 68 | 69 | gulp.task 'webpack-build', (cb) -> 70 | webpackBuild = require './packing/webpack-build' 71 | info = 72 | __dirname: __dirname 73 | isMinified: config.isMinified 74 | useCDN: config.useCDN 75 | cdn: config.cdn 76 | env: config.env 77 | webpack (webpackBuild info), (err, stats) -> 78 | if err 79 | throw new gutil.PluginError("webpack", err) 80 | gutil.log '[webpack]', stats.toString() 81 | fileContent = JSON.stringify stats.toJson().assetsByChunkName 82 | fs.writeFileSync 'packing/assets.json', fileContent 83 | cb() 84 | 85 | # aliases 86 | 87 | gulp.task 'dev', (cb) -> 88 | sequence 'html', 'webpack-dev', cb 89 | 90 | gulp.task 'build', (cb) -> 91 | gutil.log gutil.colors.yellow("Running Gulp in `#{config.env}` mode!") 92 | sequence 'del', 'webpack-build', 'html', cb 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lite-uploader", 3 | "version": "0.3.2", 4 | "description": "Uploader plugin menu from Talk by teambition", 5 | "main": "lib/index.js", 6 | "dependencies": { 7 | "fileapi": "^2.0.17", 8 | "object-assign": "^4.0.1", 9 | "shortid": "^2.2.4" 10 | }, 11 | "devDependencies": { 12 | "react": "^0.13.1", 13 | "autoprefixer-loader": "^2.0.0", 14 | "coffee-loader": "^0.7.2", 15 | "coffee-script": "^1.9.1", 16 | "config": "^1.17.1", 17 | "css-loader": "^0.10.1", 18 | "del": "^1.1.1", 19 | "extract-text-webpack-plugin": "^0.8.2", 20 | "file-loader": "^0.8.4", 21 | "gulp": "^3.8.11", 22 | "gulp-coffee": "^2.3.1", 23 | "gulp-util": "^3.0.7", 24 | "less": "^2.5.1", 25 | "less-loader": "^2.2.0", 26 | "rsyncwrapper": "^0.4.3", 27 | "run-sequence": "^1.0.2", 28 | "skip-webpack-plugin": "^0.1.1", 29 | "stir-template": "^0.1.4", 30 | "style-loader": "^0.10.2", 31 | "url-loader": "^0.5.6", 32 | "volubile-ui": "0.0.10", 33 | "webpack": "^1.8.4", 34 | "webpack-dev-server": "^1.8.0" 35 | }, 36 | "scripts": { 37 | "test": "echo \"Error: no test specified\" && exit 1", 38 | "static": "NODE_ENV=static gulp build", 39 | "ws": "NODE_ENV=ws gulp build" 40 | }, 41 | "keywords": [ 42 | "react-component" 43 | ], 44 | "author": "Teambition", 45 | "license": "MIT", 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/teambition/react-lite-uploader.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/teambition/react-lite-uploader/issues" 52 | }, 53 | "homepage": "https://github.com/teambition/react-lite-uploader" 54 | } 55 | -------------------------------------------------------------------------------- /packing/asset-links.coffee: -------------------------------------------------------------------------------- 1 | 2 | config = require 'config' 3 | 4 | if config.env is 'dev' 5 | module.exports = 6 | main: "http://localhost:#{config.webpackDevPort}/main.js" 7 | vendor: "http://localhost:#{config.webpackDevPort}/vendor.js" 8 | style: null 9 | else 10 | assets = require '../packing/assets' 11 | prefix = '' 12 | 13 | module.exports = 14 | main: "#{prefix}#{assets.main[0]}" 15 | style: "#{prefix}#{assets.main[1]}" 16 | vendor: "#{prefix}#{assets.vendor}" 17 | -------------------------------------------------------------------------------- /packing/webpack-build.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require('fs') 3 | path = require 'path' 4 | webpack = require('webpack') 5 | SkipPlugin = require 'skip-webpack-plugin' 6 | ExtractTextPlugin = require 'extract-text-webpack-plugin' 7 | 8 | webpackDev = require('./webpack-dev') 9 | 10 | fontName = 'fonts/[name].[ext]' 11 | 12 | module.exports = (info) -> 13 | webpackConfig = webpackDev info 14 | publicPath = '' 15 | 16 | # return 17 | entry: 18 | vendor: [] 19 | main: ['./src/main'] 20 | output: 21 | path: path.join info.__dirname, 'build/' 22 | filename: '[name].[chunkhash:8].js' 23 | publicPath: publicPath 24 | resolve: webpackConfig.resolve 25 | module: 26 | loaders: [ 27 | {test: /\.coffee$/, loader: 'coffee'} 28 | {test: /\.less$/, loader: 'style!css!less'} 29 | {test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css!autoprefixer')} 30 | {test: /\.(eot|woff|woff2|ttf|svg)((\?|\#)[\?\#\w\d_-]+)?$/, loader: "url", query: {limit: 100, name: fontName}} 31 | ] 32 | plugins: [ 33 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.[chunkhash:8].js') 34 | if info.isMinified 35 | new webpack.optimize.UglifyJsPlugin(compress: {warnings: false}, sourceMap: false) 36 | else 37 | new SkipPlugin info: 'UglifyJsPlugin skipped' 38 | new ExtractTextPlugin("style.[chunkhash:8].css") 39 | ] 40 | -------------------------------------------------------------------------------- /packing/webpack-dev.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require('fs') 3 | path = require 'path' 4 | config = require 'config' 5 | webpack = require 'webpack' 6 | 7 | fontName = 'fonts/[name].[ext]' 8 | 9 | module.exports = (info) -> 10 | entry: 11 | vendor: [ 12 | "webpack-dev-server/client?http://localhost:#{config.webpackDevPort}" 13 | 'webpack/hot/dev-server' 14 | 'react' 15 | ] 16 | main: [ 17 | './src/main' 18 | ] 19 | output: 20 | path: path.join info.__dirname, 'build' 21 | filename: '[name].js' 22 | publicPath: "http://localhost:#{config.webpackDevPort}/" 23 | resolve: extensions: ['.js', '.coffee', ''] 24 | module: 25 | loaders: [ 26 | {test: /\.coffee$/, loader: 'coffee'} 27 | {test: /\.less$/, loader: 'style!css!less'} 28 | {test: /\.css$/, loader: 'style!css!autoprefixer'} 29 | {test: /\.(eot|woff|woff2|ttf|svg)((\?|\#)[\?\#\w\d_-]+)?$/, loader: "url", query: {limit: 100, name: fontName}} 30 | ] 31 | plugins: [ 32 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js') 33 | new webpack.HotModuleReplacementPlugin() 34 | ] 35 | -------------------------------------------------------------------------------- /src/app/page.coffee: -------------------------------------------------------------------------------- 1 | 2 | React = require 'react' 3 | 4 | if typeof window isnt 'undefined' 5 | FileAPI = require 'fileapi' 6 | 7 | uploadUtil = require '../upload-util' 8 | 9 | {div, img, span, br, textarea} = React.DOM 10 | 11 | if typeof window is 'undefined' 12 | config = require '../../config/default' 13 | else 14 | config = window._initialStore 15 | 16 | module.exports = React.createClass 17 | getInitialState: -> 18 | image: null 19 | showCover: false 20 | 21 | componentDidMount: -> 22 | uploadUtil.handleFileDropping @refs.text.getDOMNode(), 23 | url: config.uploadUrl 24 | headers: 25 | authorization: config.token 26 | accept: ".gif,.jpg,.jpeg,.bmp,.png" 27 | multiple: false 28 | onCreate: @onCreate 29 | onProgress: @onProgress 30 | onSuccess: @onSuccess 31 | onError: @onError 32 | 33 | FileAPI.event.dnd @refs.drop.getDOMNode(), @onFileHover, @onFilesLoad 34 | 35 | onCreate: (file, fileId) -> 36 | console.log('onCreate:', file, fileId) 37 | image = FileAPI.Image file 38 | image.preview 200, 200 39 | image.get (err, imageEL) => 40 | if err 41 | console.error 'err', err 42 | else 43 | @setState image: imageEL.toDataURL() 44 | 45 | onSuccess: (data, fileId) -> 46 | console.log('onSuccess:', data, fileId) 47 | 48 | onProgress: (loaded, total, fileId) -> 49 | console.log('onProgress:', loaded, total, fileId) 50 | 51 | onError: (data, fileId) -> 52 | console.error('onError:', data, fileId) 53 | 54 | onPaste: (event) -> 55 | uploadUtil.handlePasteEvent event.nativeEvent, 56 | url: config.uploadUrl 57 | headers: 58 | authorization: config.token 59 | accept: ".gif,.jpg,.jpeg,.bmp,.png" 60 | multiple: false 61 | onCreate: this.onCreate 62 | onProgress: this.onProgress 63 | onSuccess: this.onSuccess 64 | onError: this.onError 65 | 66 | onClickUpload: (event) -> 67 | uploadUtil.handleClick 68 | url: config.uploadUrl 69 | headers: 70 | authorization: config.token 71 | accept: ".gif,.jpg,.jpeg,.bmp,.png" 72 | multiple: false 73 | onCreate: this.onCreate 74 | onProgress: this.onProgress 75 | onSuccess: this.onSuccess 76 | onError: this.onError 77 | 78 | onFileHover: (isHover) -> 79 | if isHover isnt @state.isHover 80 | @setState isHover: isHover 81 | 82 | onFilesLoad: (files) -> 83 | uploadUtil.onFilesLoad files, 84 | url: config.uploadUrl 85 | headers: 86 | authorization: config.token 87 | accept: ".gif,.jpg,.jpeg,.bmp,.png" 88 | multiple: false 89 | onCreate: this.onCreate 90 | onProgress: this.onProgress 91 | onSuccess: this.onSuccess 92 | onError: this.onError 93 | 94 | renderButton: -> 95 | span className: "trigger", onClick: @onClickUpload, 'click to upload' 96 | 97 | renderArea: -> 98 | div className: "target", ref: 'drop', "Drop file here", 99 | if @state.isHover 100 | div className: 'drop-place' 101 | 102 | render: -> 103 | div null, 104 | div className: "demo", 105 | div null, 'Drop file only' 106 | this.renderArea() 107 | div className: "demo", 108 | span null, 'Click only: ' 109 | this.renderButton() 110 | br() 111 | if this.state.image 112 | img src: this.state.image 113 | div className: 'demo', 114 | textarea ref: 'text', onPaste: @onPaste, placeholder: 'Drop or Paste' 115 | div className: 'demo', 116 | div className: 'note', "Open console find details..." 117 | -------------------------------------------------------------------------------- /src/demo.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding: 100px 100px 300px 100px; 4 | font-family: Palatino, Optima, Georgia, serif; 5 | font-size: 16px; 6 | line-height: 1.6em; 7 | } 8 | 9 | .title { 10 | font-size: 20px; 11 | margin: 20px 0; 12 | } 13 | 14 | .trigger, .target { 15 | width: 160px; 16 | height: 40px; 17 | line-height: 40px; 18 | background: hsl(240, 100%, 80%); 19 | color: white; 20 | text-align: center; 21 | display: inline-block; 22 | } 23 | 24 | .trigger { 25 | cursor: pointer; 26 | } 27 | 28 | .uploader-area, .uploader-button { 29 | display: inline-block; 30 | } 31 | 32 | .target { 33 | width: 400px; 34 | height: 100px; 35 | position: relative; 36 | } 37 | 38 | .target .drop-place { 39 | position: absolute; 40 | width: 100%; 41 | height: 100%; 42 | top: 0; 43 | left: 0; 44 | background-color: hsla(0,0%,0%,0.4); 45 | } 46 | 47 | .demo { 48 | margin: 40px 0; 49 | } 50 | 51 | .uploader-area { 52 | background-color: hsl(240,80%,90%); 53 | } 54 | 55 | .uploader-area.is-dropping { 56 | background-color: hsl(240,80%,80%); 57 | } 58 | 59 | .demo img { 60 | margin: 40px 0; 61 | } 62 | 63 | .note { 64 | margin-top: 40px; 65 | } 66 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | 2 | exports.util = require('./upload-util') 3 | -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | 2 | React = require 'react' 3 | 4 | require './demo.css' 5 | 6 | render = -> 7 | App = React.createFactory require './app/page' 8 | React.render App(), document.querySelector('.demo') 9 | 10 | render() 11 | 12 | if module.hot 13 | module.hot.accept './app/page', -> 14 | render() 15 | -------------------------------------------------------------------------------- /src/upload-util.coffee: -------------------------------------------------------------------------------- 1 | 2 | shortid = require 'shortid' 3 | 4 | if typeof window isnt 'undefined' 5 | FileAPI = require 'fileapi' 6 | gloablInputButton = document.createElement 'input' 7 | gloablInputButton.type = 'file' 8 | 9 | checkDefaultProps = (props) -> 10 | if not props.accept? 11 | props.accept = '' 12 | if not props.multiple? 13 | props.multiple = false 14 | props 15 | 16 | module.exports = 17 | uploadFile: (file, props) -> 18 | fileId = shortid.generate() 19 | 20 | file.xhr = FileAPI.upload 21 | url: props.url 22 | headers: props.headers 23 | data: 24 | size: file.size 25 | files: 26 | file: file 27 | fileupload: (file, xhr, options) => 28 | props.onCreate? file, fileId 29 | fileprogress: (event, file, xhr, options) => 30 | props.onProgress? event.loaded, event.total, fileId 31 | filecomplete: (err, xhr, file, options) => 32 | # err is a boolean, strange style from fileapi, be cautious! 33 | if err 34 | errorDetails = 35 | type: 'failed-upload', data: err 36 | xhr: xhr, file: file, options: options 37 | props.onError errorDetails, fileId 38 | else 39 | data = null 40 | isDataParsed = false 41 | try 42 | data = JSON.parse xhr.responseText 43 | isDataParsed = true 44 | catch error 45 | errorDetails = 46 | type: 'failed-parsing-result', data: error 47 | xhr: xhr, file: file, options: options 48 | props.onError errorDetails, fileId 49 | if isDataParsed 50 | props.onSuccess data, fileId 51 | 52 | handlePasteEvent: (event, props) -> 53 | clipboardData = event.clipboardData 54 | 55 | maybeFiles = Array::map.call clipboardData.types, (type, i) => 56 | if type is 'Files' 57 | fileType = clipboardData.items[i].type 58 | copiedName = 'copy-paste' 59 | # Blob to File http://stackoverflow.com/q/27159179/883571 60 | if window.File? 61 | fileBlob = clipboardData.items[i].getAsFile() 62 | file = new File [fileBlob], copiedName, type: fileType 63 | else 64 | file = clipboardData.items[i].getAsFile() 65 | file.lastModifiedDate = new Date() 66 | file.name = copiedName 67 | file 68 | else null 69 | maybeFiles = maybeFiles.filter (file) -> file? 70 | if maybeFiles.length > 0 71 | event.preventDefault() 72 | @onFilesLoad maybeFiles, props 73 | 74 | handleFileDropping: (target, props) -> 75 | onFilesLoad = (files) => @onFilesLoad files, props 76 | onFileHover = (isHover) -> props.onFileHover? isHover 77 | FileAPI.event.dnd target, onFileHover, onFilesLoad 78 | 79 | onFilesLoad: (files, props) -> 80 | props = checkDefaultProps props 81 | # throw error if no file is available 82 | if files.length is 0 83 | return props.onError type: 'no-file', data: 'No file available' 84 | # throw error if too many files are selected 85 | if (not props.multiple) and files.length > 1 86 | return props.onError type: 'too-many-files', data: 'Too many files selected' 87 | # throw error if accepted types is not satisfied 88 | if props.accept.trim().length > 0 89 | acceptTypes = props.accept.split(',').map (extension) -> 90 | if extension[0] is '.' 91 | extension.slice(1) 92 | else extension 93 | allTypesOk = files.every (file) -> 94 | acceptTypes.some (extension) -> 95 | file.type.split('/').indexOf(extension) >= 0 96 | if (not allTypesOk) 97 | return props.onError type: 'invalid-type', data: 'Invalid file type' 98 | 99 | files.forEach (file) => 100 | @uploadFile file, props 101 | 102 | handleInputChange: (event, props) -> 103 | files = FileAPI.getFiles event 104 | @onFilesLoad files, props 105 | 106 | handleClick: (props) -> 107 | gloablInputButton.multiple = props.multiple 108 | gloablInputButton.accept = props.accept 109 | gloablInputButton.click() 110 | gloablInputButton.onchange = (event) => 111 | # rewrite method to inject null value 112 | oldOnSuccess = props.onSuccess 113 | oldOnError = props.onError 114 | props.onSuccess = (args...) -> 115 | oldOnSuccess args... 116 | gloablInputButton.value = null 117 | props.onError = (args...) -> 118 | oldOnError args... 119 | gloablInputButton.value = null 120 | 121 | @handleInputChange event, props 122 | --------------------------------------------------------------------------------