uppy
Version:
Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:
234 lines (204 loc) • 8.61 kB
JavaScript
const throttle = require('lodash.throttle')
const { h } = require('preact')
function progressDetails (props) {
return <span>{props.totalProgress || 0}%・{props.complete} / {props.inProgress}・{props.totalUploadedSize} / {props.totalSize}・↑ {props.totalSpeed}/s・{props.totalETA}</span>
}
const ThrottledProgressDetails = throttle(progressDetails, 500, {leading: true, trailing: true})
const STATE_ERROR = 'error'
const STATE_WAITING = 'waiting'
const STATE_PREPROCESSING = 'preprocessing'
const STATE_UPLOADING = 'uploading'
const STATE_POSTPROCESSING = 'postprocessing'
const STATE_COMPLETE = 'complete'
function getUploadingState (props, files) {
if (props.isAllErrored) {
return STATE_ERROR
}
// If ALL files have been completed, show the completed state.
if (props.isAllComplete) {
return STATE_COMPLETE
}
let state = STATE_WAITING
const fileIDs = Object.keys(files)
for (let i = 0; i < fileIDs.length; i++) {
const progress = files[fileIDs[i]].progress
// If ANY files are being uploaded right now, show the uploading state.
if (progress.uploadStarted && !progress.uploadComplete) {
return STATE_UPLOADING
}
// If files are being preprocessed AND postprocessed at this time, we show the
// preprocess state. If any files are being uploaded we show uploading.
if (progress.preprocess && state !== STATE_UPLOADING) {
state = STATE_PREPROCESSING
}
// If NO files are being preprocessed or uploaded right now, but some files are
// being postprocessed, show the postprocess state.
if (progress.postprocess && state !== STATE_UPLOADING && state !== STATE_PREPROCESSING) {
state = STATE_POSTPROCESSING
}
}
return state
}
function calculateProcessingProgress (files) {
// Collect pre or postprocessing progress states.
const progresses = []
Object.keys(files).forEach((fileID) => {
const { progress } = files[fileID]
if (progress.preprocess) {
progresses.push(progress.preprocess)
}
if (progress.postprocess) {
progresses.push(progress.postprocess)
}
})
// In the future we should probably do this differently. For now we'll take the
// mode and message from the first file…
const { mode, message } = progresses[0]
const value = progresses.filter(isDeterminate).reduce((total, progress, index, all) => {
return total + progress.value / all.length
}, 0)
function isDeterminate (progress) {
return progress.mode === 'determinate'
}
return {
mode,
message,
value
}
}
function togglePauseResume (props) {
if (props.isAllComplete) return
if (!props.resumableUploads) {
return props.cancelAll()
}
if (props.isAllPaused) {
return props.resumeAll()
}
return props.pauseAll()
}
module.exports = (props) => {
props = props || {}
const uploadState = getUploadingState(props, props.files || {})
let progressValue = props.totalProgress
let progressMode
let progressBarContent
if (uploadState === STATE_PREPROCESSING || uploadState === STATE_POSTPROCESSING) {
const progress = calculateProcessingProgress(props.files)
progressMode = progress.mode
if (progressMode === 'determinate') {
progressValue = progress.value * 100
}
progressBarContent = ProgressBarProcessing(progress)
} else if (uploadState === STATE_COMPLETE) {
progressBarContent = ProgressBarComplete(props)
} else if (uploadState === STATE_UPLOADING) {
progressBarContent = ProgressBarUploading(props)
} else if (uploadState === STATE_ERROR) {
progressValue = undefined
progressBarContent = ProgressBarError(props)
}
const width = typeof progressValue === 'number' ? progressValue : 100
const isHidden = (uploadState === STATE_WAITING && props.hideUploadButton) ||
(uploadState === STATE_WAITING && !props.newFiles > 0) ||
(uploadState === STATE_COMPLETE && props.hideAfterFinish)
const progressClasses = `uppy-StatusBar-progress
${progressMode ? 'is-' + progressMode : ''}`
return (
<div class={`uppy uppy-StatusBar is-${uploadState}`} aria-hidden={isHidden}>
<div class={progressClasses}
style={{ width: width + '%' }}
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={progressValue} />
{progressBarContent}
<div class="uppy-StatusBar-actions">
{ props.newFiles && !props.hideUploadButton ? <UploadBtn {...props} /> : null }
{ props.error ? <RetryBtn {...props} /> : null }
</div>
</div>
)
}
const UploadBtn = (props) => {
return <button type="button"
class="uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--upload"
aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
onclick={props.startUpload}>
{props.inProgress
? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
: props.i18n('uploadXFiles', { smart_count: props.newFiles })
}
</button>
}
const RetryBtn = (props) => {
return <button type="button"
class="uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
aria-label={props.i18n('retryUpload')}
onclick={props.retryAll}>{props.i18n('retry')}</button>
}
const ProgressBarProcessing = (props) => {
const value = Math.round(props.value * 100)
return <div class="uppy-StatusBar-content">
{props.mode === 'determinate' ? `${value}%・` : ''}
{props.message}
</div>
}
const ProgressBarUploading = (props) => {
const { i18n } = props
return (
<div class="uppy-StatusBar-content">
{props.isUploadStarted && !props.isAllComplete
? !props.isAllPaused
? <div title="Uploading">{ <PauseResumeButtons {...props} /> } {i18n('uploading')} { <ThrottledProgressDetails {...props} /> }</div>
: <div title="Paused">{ <PauseResumeButtons {...props} /> } {i18n('paused')}・{props.totalProgress}%</div>
: null
}
</div>
)
}
const ProgressBarComplete = ({ totalProgress, i18n }) => {
return (
<div class="uppy-StatusBar-content" role="status">
<span title="Complete">
<svg aria-hidden="true" class="uppy-StatusBar-statusIndicator UppyIcon" width="18" height="17" viewBox="0 0 23 17">
<path d="M8.944 17L0 7.865l2.555-2.61 6.39 6.525L20.41 0 23 2.645z" />
</svg>
{i18n('uploadComplete')}・{totalProgress}%
</span>
</div>
)
}
const ProgressBarError = ({ error, retryAll, i18n }) => {
return (
<div class="uppy-StatusBar-content" role="alert">
<strong>{i18n('uploadFailed')}.</strong> <span>{i18n('pleasePressRetry')}</span>
<span class="uppy-StatusBar-details"
aria-label={error}
data-microtip-position="top"
data-microtip-size="large"
role="tooltip">?</span>
</div>
)
}
const PauseResumeButtons = (props) => {
const { resumableUploads, isAllPaused, i18n } = props
const title = resumableUploads
? isAllPaused
? i18n('resumeUpload')
: i18n('pauseUpload')
: i18n('cancelUpload')
return <button title={title} class="uppy-StatusBar-statusIndicator" type="button" onclick={() => togglePauseResume(props)}>
{resumableUploads
? isAllPaused
? <svg aria-hidden="true" class="UppyIcon" width="15" height="17" viewBox="0 0 11 13">
<path d="M1.26 12.534a.67.67 0 0 1-.674.012.67.67 0 0 1-.336-.583v-11C.25.724.38.5.586.382a.658.658 0 0 1 .673.012l9.165 5.5a.66.66 0 0 1 .325.57.66.66 0 0 1-.325.573l-9.166 5.5z" />
</svg>
: <svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
<path d="M4.888.81v11.38c0 .446-.324.81-.722.81H2.722C2.324 13 2 12.636 2 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81zM9.888.81v11.38c0 .446-.324.81-.722.81H7.722C7.324 13 7 12.636 7 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81z" />
</svg>
: <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
<path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
</svg>
}
</button>
}