diff --git a/components/vc-upload/index.js b/components/vc-upload/index.js
new file mode 100644
index 000000000..e6c4df13a
--- /dev/null
+++ b/components/vc-upload/index.js
@@ -0,0 +1,3 @@
+import upload from './src'
+
+export default upload
diff --git a/components/vc-upload/src/AjaxUploader.jsx b/components/vc-upload/src/AjaxUploader.jsx
new file mode 100644
index 000000000..ab329ab06
--- /dev/null
+++ b/components/vc-upload/src/AjaxUploader.jsx
@@ -0,0 +1,200 @@
+import PropTypes from '../../_util/vue-types'
+import BaseMixin from '../../_util/BaseMixin'
+import classNames from 'classnames'
+import defaultRequest from './request'
+import getUid from './uid'
+import attrAccept from './attr-accept'
+
+const upLoadPropTypes = {
+ component: PropTypes.string,
+ // style: PropTypes.object,
+ prefixCls: PropTypes.string,
+ // className: PropTypes.string,
+ multiple: PropTypes.bool,
+ disabled: PropTypes.bool,
+ accept: PropTypes.string,
+ // children: PropTypes.any,
+ // onStart: PropTypes.func,
+ data: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.func,
+ ]),
+ headers: PropTypes.object,
+ beforeUpload: PropTypes.func,
+ customRequest: PropTypes.func,
+ // onProgress: PropTypes.func,
+ withCredentials: PropTypes.bool,
+}
+
+const AjaxUploader = {
+ mixins: [BaseMixin],
+ props: upLoadPropTypes,
+ data () {
+ this.reqs = {}
+ return {
+ uid: getUid(),
+ }
+ },
+ methods: {
+ onChange (e) {
+ const files = e.target.files
+ this.uploadFiles(files)
+ this.reset()
+ },
+ onClick () {
+ const el = this.$refs.fileInputRef
+ if (!el) {
+ return
+ }
+ el.click()
+ },
+ onKeyDown (e) {
+ if (e.key === 'Enter') {
+ this.onClick()
+ }
+ },
+ onFileDrop (e) {
+ if (e.type === 'dragover') {
+ e.preventDefault()
+ return
+ }
+ const files = Array.prototype.slice.call(e.dataTransfer.files).filter(
+ file => attrAccept(file, this.accept)
+ )
+ this.uploadFiles(files)
+
+ e.preventDefault()
+ },
+ uploadFiles (files) {
+ const postFiles = Array.prototype.slice.call(files)
+ postFiles.forEach((file) => {
+ file.uid = getUid()
+ this.upload(file, postFiles)
+ })
+ },
+ upload (file, fileList) {
+ if (!this.beforeUpload) {
+ // always async in case use react state to keep fileList
+ return setTimeout(() => this.post(file), 0)
+ }
+
+ const before = this.beforeUpload(file, fileList)
+ if (before && before.then) {
+ before.then((processedFile) => {
+ const processedFileType = Object.prototype.toString.call(processedFile)
+ if (processedFileType === '[object File]' || processedFileType === '[object Blob]') {
+ this.post(processedFile)
+ } else {
+ this.post(file)
+ }
+ }).catch(e => {
+ console && console.log(e); // eslint-disable-line
+ })
+ } else if (before !== false) {
+ setTimeout(() => this.post(file), 0)
+ }
+ },
+ post (file) {
+ if (!this._isMounted) {
+ return
+ }
+ let { data } = this.$props
+ if (typeof data === 'function') {
+ data = data(file)
+ }
+ const { uid } = file
+ const request = this.customRequest || defaultRequest
+ this.reqs[uid] = request({
+ action: this.action,
+ filename: this.name,
+ file,
+ data,
+ headers: this.headers,
+ withCredentials: this.withCredentials,
+ onProgress: e => {
+ this.$emit('progress', e, file)
+ },
+ onSuccess: (ret, xhr) => {
+ delete this.reqs[uid]
+ this.$emit('success', ret, file, xhr)
+ },
+ onError: (err, ret) => {
+ delete this.reqs[uid]
+ this.$emit('error', err, ret, file)
+ },
+ })
+ this.$emit('start', file)
+ },
+ reset () {
+ this.setState({
+ uid: getUid(),
+ })
+ },
+ abort (file) {
+ const { reqs } = this
+ if (file) {
+ let uid = file
+ if (file && file.uid) {
+ uid = file.uid
+ }
+ if (reqs[uid]) {
+ reqs[uid].abort()
+ delete reqs[uid]
+ }
+ } else {
+ Object.keys(reqs).forEach((uid) => {
+ if (reqs[uid]) {
+ reqs[uid].abort()
+ }
+
+ delete reqs[uid]
+ })
+ }
+ },
+ },
+ mounted () {
+ this.$nextTick(() => {
+ this._isMounted = true
+ })
+ },
+ beforeDestroy () {
+ this._isMounted = false
+ this.abort()
+ },
+ render () {
+ const {
+ component: Tag, prefixCls, disabled, multiple, accept,
+ } = this.$props
+ const cls = classNames({
+ [prefixCls]: true,
+ [`${prefixCls}-disabled`]: disabled,
+ })
+ const events = disabled ? {} : {
+ onClick: this.onClick,
+ onKeydown: this.onKeyDown,
+ onDrop: this.onFileDrop,
+ onDragover: this.onFileDrop,
+ tabIndex: '0',
+ }
+ return (
+
+
+ {this.$slots.default}
+
+ )
+ },
+}
+
+export default AjaxUploader
diff --git a/components/vc-upload/src/IframeUploader.jsx b/components/vc-upload/src/IframeUploader.jsx
new file mode 100644
index 000000000..ac1f3d598
--- /dev/null
+++ b/components/vc-upload/src/IframeUploader.jsx
@@ -0,0 +1,272 @@
+import PropTypes from '../../_util/vue-types'
+import BaseMixin from '../../_util/BaseMixin'
+import classNames from 'classnames'
+import getUid from './uid'
+import warning from '../../_util/warning'
+
+const IFRAME_STYLE = {
+ position: 'absolute',
+ top: 0,
+ opacity: 0,
+ filter: 'alpha(opacity=0)',
+ left: 0,
+ zIndex: 9999,
+}
+
+// diferent from AjaxUpload, can only upload on at one time, serial seriously
+const IframeUploader = {
+ mixins: [BaseMixin],
+ props: {
+ component: PropTypes.string,
+ // style: PropTypes.object,
+ disabled: PropTypes.bool,
+ prefixCls: PropTypes.string,
+ // className: PropTypes.string,
+ accept: PropTypes.string,
+ // onStart: PropTypes.func,
+ multiple: PropTypes.bool,
+ // children: PropTypes.any,
+ data: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.func,
+ ]),
+ action: PropTypes.string,
+ name: PropTypes.string,
+ },
+ data () {
+ this.file = {}
+ return {
+ uploading: false,
+ }
+ },
+ methods: {
+ onLoad () {
+ if (!this.uploading) {
+ return
+ }
+ const { file } = this
+ let response
+ try {
+ const doc = this.getIframeDocument()
+ const script = doc.getElementsByTagName('script')[0]
+ if (script && script.parentNode === doc.body) {
+ doc.body.removeChild(script)
+ }
+ response = doc.body.innerHTML
+ this.$emit('success', response, file)
+ } catch (err) {
+ warning(false, 'cross domain error for Upload. Maybe server should return document.domain script. see Note from https://github.com/react-component/upload')
+ response = 'cross-domain'
+ this.$emit('error', err, null, file)
+ }
+ this.endUpload()
+ },
+ onChange () {
+ const target = this.getFormInputNode()
+ // ie8/9 don't support FileList Object
+ // http://stackoverflow.com/questions/12830058/ie8-input-type-file-get-files
+ const file = this.file = {
+ uid: getUid(),
+ name: target.value,
+ }
+ this.startUpload()
+ const { $props: props } = this
+ if (!props.beforeUpload) {
+ return this.post(file)
+ }
+ const before = props.beforeUpload(file)
+ if (before && before.then) {
+ before.then(() => {
+ this.post(file)
+ }, () => {
+ this.endUpload()
+ })
+ } else if (before !== false) {
+ this.post(file)
+ } else {
+ this.endUpload()
+ }
+ },
+ getIframeNode () {
+ return this.$refs.iframeRef
+ },
+ getIframeDocument () {
+ return this.getIframeNode().contentDocument
+ },
+ getFormNode () {
+ return this.getIframeDocument().getElementById('form')
+ },
+ getFormInputNode () {
+ return this.getIframeDocument().getElementById('input')
+ },
+ getFormDataNode () {
+ return this.getIframeDocument().getElementById('data')
+ },
+ getFileForMultiple (file) {
+ return this.multiple ? [file] : file
+ },
+ getIframeHTML (domain) {
+ let domainScript = ''
+ let domainInput = ''
+ if (domain) {
+ const script = 'script'
+ domainScript = `<${script}>document.domain="${domain}";${script}>`
+ domainInput = ``
+ }
+ return `
+
+
+
+
+
+ ${domainScript}
+
+
+
+
+
+ `
+ },
+ initIframeSrc () {
+ if (this.domain) {
+ this.getIframeNode().src = `javascript:void((function(){
+ var d = document;
+ d.open();
+ d.domain='${this.domain}';
+ d.write('');
+ d.close();
+ })())`
+ }
+ },
+ initIframe () {
+ const iframeNode = this.getIframeNode()
+ let win = iframeNode.contentWindow
+ let doc
+ this.domain = this.domain || ''
+ this.initIframeSrc()
+ try {
+ doc = win.document
+ } catch (e) {
+ this.domain = document.domain
+ this.initIframeSrc()
+ win = iframeNode.contentWindow
+ doc = win.document
+ }
+ doc.open('text/html', 'replace')
+ doc.write(this.getIframeHTML(this.domain))
+ doc.close()
+ this.getFormInputNode().onchange = this.onChange
+ },
+ endUpload () {
+ if (this.uploading) {
+ this.file = {}
+ // hack avoid batch
+ this.uploading = false
+ this.setState({
+ uploading: false,
+ })
+ this.initIframe()
+ }
+ },
+ startUpload () {
+ if (!this.uploading) {
+ this.uploading = true
+ this.setState({
+ uploading: true,
+ })
+ }
+ },
+ updateIframeWH () {
+ const rootNode = this.$el
+ const iframeNode = this.getIframeNode()
+ iframeNode.style.height = `${rootNode.offsetHeight}px`
+ iframeNode.style.width = `${rootNode.offsetWidth}px`
+ },
+ abort (file) {
+ if (file) {
+ let uid = file
+ if (file && file.uid) {
+ uid = file.uid
+ }
+ if (uid === this.file.uid) {
+ this.endUpload()
+ }
+ } else {
+ this.endUpload()
+ }
+ },
+ post (file) {
+ const formNode = this.getFormNode()
+ const dataSpan = this.getFormDataNode()
+ let { data } = this.$props
+ if (typeof data === 'function') {
+ data = data(file)
+ }
+ const inputs = document.createDocumentFragment()
+ for (const key in data) {
+ if (data.hasOwnProperty(key)) {
+ const input = document.createElement('input')
+ input.setAttribute('name', key)
+ input.value = data[key]
+ inputs.appendChild(input)
+ }
+ }
+ dataSpan.appendChild(inputs)
+ formNode.submit()
+ dataSpan.innerHTML = ''
+ this.$emit('start', file)
+ },
+ },
+ mounted () {
+ this.$nextTick(() => {
+ this.updateIframeWH()
+ this.initIframe()
+ })
+ },
+ updated () {
+ this.$nextTick(() => {
+ this.updateIframeWH()
+ })
+ },
+
+ render () {
+ const {
+ component: Tag, disabled,
+ prefixCls,
+ } = this.$props
+ const iframeStyle = {
+ ...IFRAME_STYLE,
+ display: this.uploading || disabled ? 'none' : '',
+ }
+ const cls = classNames({
+ [prefixCls]: true,
+ [`${prefixCls}-disabled`]: disabled,
+ })
+ return (
+
+
+ {this.$slots.default}
+
+ )
+ },
+}
+
+export default IframeUploader
diff --git a/components/vc-upload/src/Upload.jsx b/components/vc-upload/src/Upload.jsx
new file mode 100644
index 000000000..8f043255c
--- /dev/null
+++ b/components/vc-upload/src/Upload.jsx
@@ -0,0 +1,91 @@
+import PropTypes from '../../_util/vue-types'
+import { initDefaultProps } from '../../_util/props-util'
+import BaseMixin from '../../_util/BaseMixin'
+import AjaxUpload from './AjaxUploader'
+import IframeUpload from './IframeUploader'
+
+// function empty () {
+// }
+
+const uploadProps = {
+ component: PropTypes.string,
+ prefixCls: PropTypes.string,
+ action: PropTypes.string,
+ name: PropTypes.string,
+ multipart: PropTypes.bool,
+ // onError: PropTypes.func,
+ // onSuccess: PropTypes.func,
+ // onProgress: PropTypes.func,
+ // onStart: PropTypes.func,
+ data: PropTypes.oneOfType([
+ PropTypes.object,
+ PropTypes.func,
+ ]),
+ headers: PropTypes.object,
+ accept: PropTypes.string,
+ multiple: PropTypes.bool,
+ disabled: PropTypes.bool,
+ beforeUpload: PropTypes.func,
+ customRequest: PropTypes.func,
+ // onReady: PropTypes.func,
+ withCredentials: PropTypes.bool,
+ supportServerRender: PropTypes.bool,
+}
+export default {
+ name: 'Upload',
+ mixins: [BaseMixin],
+ props: initDefaultProps(uploadProps, {
+ component: 'span',
+ prefixCls: 'rc-upload',
+ data: {},
+ headers: {},
+ name: 'file',
+ multipart: false,
+ // onReady: empty,
+ // onStart: empty,
+ // onError: empty,
+ // onSuccess: empty,
+ supportServerRender: false,
+ multiple: false,
+ beforeUpload: null,
+ customRequest: null,
+ withCredentials: false,
+ }),
+ data () {
+ return {
+ Component: null,
+ }
+ },
+ mounted () {
+ this.$nextTick(() => {
+ if (this.supportServerRender) {
+ /* eslint react/no-did-mount-set-state:0 */
+ this.setState({
+ Component: this.getComponent(),
+ }, () => {
+ this.$emit('ready')
+ })
+ }
+ })
+ },
+ methods: {
+ getComponent () {
+ return typeof File !== 'undefined' ? AjaxUpload : IframeUpload
+ },
+ abort (file) {
+ this.$refs.uploaderRef.abort(file)
+ },
+ },
+
+ render () {
+ if (this.supportServerRender) {
+ const ComponentUploader = this.Component
+ if (ComponentUploader) {
+ return
+ }
+ return null
+ }
+ const ComponentUploader = this.getComponent()
+ return
+ },
+}
diff --git a/components/vc-upload/src/attr-accept.js b/components/vc-upload/src/attr-accept.js
new file mode 100644
index 000000000..9bdfa821c
--- /dev/null
+++ b/components/vc-upload/src/attr-accept.js
@@ -0,0 +1,26 @@
+function endsWith (str, suffix) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1
+}
+
+export default (file, acceptedFiles) => {
+ if (file && acceptedFiles) {
+ const acceptedFilesArray = Array.isArray(acceptedFiles)
+ ? acceptedFiles
+ : acceptedFiles.split(',')
+ const fileName = file.name || ''
+ const mimeType = file.type || ''
+ const baseMimeType = mimeType.replace(/\/.*$/, '')
+
+ return acceptedFilesArray.some(type => {
+ const validType = type.trim()
+ if (validType.charAt(0) === '.') {
+ return endsWith(fileName.toLowerCase(), validType.toLowerCase())
+ } else if (/\/\*$/.test(validType)) {
+ // This is something like a image/* mime type
+ return baseMimeType === validType.replace(/\/.*$/, '')
+ }
+ return mimeType === validType
+ })
+ }
+ return true
+}
diff --git a/components/vc-upload/src/index.js b/components/vc-upload/src/index.js
new file mode 100644
index 000000000..4e0dfd8b4
--- /dev/null
+++ b/components/vc-upload/src/index.js
@@ -0,0 +1,4 @@
+// export this package's api
+import Upload from './Upload'
+
+export default Upload
diff --git a/components/vc-upload/src/request.js b/components/vc-upload/src/request.js
new file mode 100644
index 000000000..8abb7a4ec
--- /dev/null
+++ b/components/vc-upload/src/request.js
@@ -0,0 +1,97 @@
+function getError (option, xhr) {
+ const msg = `cannot post ${option.action} ${xhr.status}'`
+ const err = new Error(msg)
+ err.status = xhr.status
+ err.method = 'post'
+ err.url = option.action
+ return err
+}
+
+function getBody (xhr) {
+ const text = xhr.responseText || xhr.response
+ if (!text) {
+ return text
+ }
+
+ try {
+ return JSON.parse(text)
+ } catch (e) {
+ return text
+ }
+}
+
+// option {
+// onProgress: (event: { percent: number }): void,
+// onError: (event: Error, body?: Object): void,
+// onSuccess: (body: Object): void,
+// data: Object,
+// filename: String,
+// file: File,
+// withCredentials: Boolean,
+// action: String,
+// headers: Object,
+// }
+export default function upload (option) {
+ const xhr = new window.XMLHttpRequest()
+
+ if (option.onProgress && xhr.upload) {
+ xhr.upload.onprogress = function progress (e) {
+ if (e.total > 0) {
+ e.percent = e.loaded / e.total * 100
+ }
+ option.onProgress(e)
+ }
+ }
+
+ const formData = new window.FormData()
+
+ if (option.data) {
+ Object.keys(option.data).map(key => {
+ formData.append(key, option.data[key])
+ })
+ }
+
+ formData.append(option.filename, option.file)
+
+ xhr.onerror = function error (e) {
+ option.onError(e)
+ }
+
+ xhr.onload = function onload () {
+ // allow success when 2xx status
+ // see https://github.com/react-component/upload/issues/34
+ if (xhr.status < 200 || xhr.status >= 300) {
+ return option.onError(getError(option, xhr), getBody(xhr))
+ }
+
+ option.onSuccess(getBody(xhr), xhr)
+ }
+
+ xhr.open('post', option.action, true)
+
+ // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
+ if (option.withCredentials && 'withCredentials' in xhr) {
+ xhr.withCredentials = true
+ }
+
+ const headers = option.headers || {}
+
+ // when set headers['X-Requested-With'] = null , can close default XHR header
+ // see https://github.com/react-component/upload/issues/33
+ if (headers['X-Requested-With'] !== null) {
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
+ }
+
+ for (const h in headers) {
+ if (headers.hasOwnProperty(h) && headers[h] !== null) {
+ xhr.setRequestHeader(h, headers[h])
+ }
+ }
+ xhr.send(formData)
+
+ return {
+ abort () {
+ xhr.abort()
+ },
+ }
+}
diff --git a/components/vc-upload/src/uid.js b/components/vc-upload/src/uid.js
new file mode 100644
index 000000000..00a9e44f7
--- /dev/null
+++ b/components/vc-upload/src/uid.js
@@ -0,0 +1,6 @@
+const now = +(new Date())
+let index = 0
+
+export default function uid () {
+ return `rc-upload-${now}-${++index}`
+}