parent
2bea54c4fa
commit
e9899ac09f
@ -0,0 +1,3 @@
|
||||
import upload from './src'
|
||||
|
||||
export default upload
|
@ -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 (
|
||||
<Tag
|
||||
{...events}
|
||||
class={cls}
|
||||
role='button'
|
||||
>
|
||||
<input
|
||||
type='file'
|
||||
ref='fileInputRef'
|
||||
key={this.uid}
|
||||
style={{ display: 'none' }}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{this.$slots.default}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default AjaxUploader
|
@ -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 = `<input name="_documentDomain" value="${domain}" />`
|
||||
}
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<style>
|
||||
body,html {padding:0;margin:0;border:0;overflow:hidden;}
|
||||
</style>
|
||||
${domainScript}
|
||||
</head>
|
||||
<body>
|
||||
<form method="post"
|
||||
encType="multipart/form-data"
|
||||
action="${this.action}" id="form"
|
||||
style="display:block;height:9999px;position:relative;overflow:hidden;">
|
||||
<input id="input" type="file"
|
||||
name="${this.name}"
|
||||
style="position:absolute;top:0;right:0;height:9999px;font-size:9999px;cursor:pointer;"/>
|
||||
${domainInput}
|
||||
<span id="data"></span>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
},
|
||||
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 (
|
||||
<Tag
|
||||
className={cls}
|
||||
style={{ position: 'relative', zIndex: 0 }}
|
||||
>
|
||||
<iframe
|
||||
ref='iframeRef'
|
||||
onLoad={this.onLoad}
|
||||
style={iframeStyle}
|
||||
/>
|
||||
{this.$slots.default}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default IframeUploader
|
@ -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 <ComponentUploader {...this.$props} ref='uploaderRef' />
|
||||
}
|
||||
return null
|
||||
}
|
||||
const ComponentUploader = this.getComponent()
|
||||
return <ComponentUploader {...this.$props} ref='uploaderRef' />
|
||||
},
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
// export this package's api
|
||||
import Upload from './Upload'
|
||||
|
||||
export default Upload
|
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
const now = +(new Date())
|
||||
let index = 0
|
||||
|
||||
export default function uid () {
|
||||
return `rc-upload-${now}-${++index}`
|
||||
}
|
Loading…
Reference in new issue