StatusBar.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. const throttle = require('lodash.throttle')
  2. const classNames = require('classnames')
  3. const statusBarStates = require('./StatusBarStates')
  4. const prettyBytes = require('@uppy/utils/lib/prettyBytes')
  5. const prettyETA = require('@uppy/utils/lib/prettyETA')
  6. const { h } = require('preact')
  7. function calculateProcessingProgress (files) {
  8. // Collect pre or postprocessing progress states.
  9. const progresses = []
  10. Object.keys(files).forEach((fileID) => {
  11. const { progress } = files[fileID]
  12. if (progress.preprocess) {
  13. progresses.push(progress.preprocess)
  14. }
  15. if (progress.postprocess) {
  16. progresses.push(progress.postprocess)
  17. }
  18. })
  19. // In the future we should probably do this differently. For now we'll take the
  20. // mode and message from the first file…
  21. const { mode, message } = progresses[0]
  22. const value = progresses.filter(isDeterminate).reduce((total, progress, index, all) => {
  23. return total + progress.value / all.length
  24. }, 0)
  25. function isDeterminate (progress) {
  26. return progress.mode === 'determinate'
  27. }
  28. return {
  29. mode,
  30. message,
  31. value
  32. }
  33. }
  34. function togglePauseResume (props) {
  35. if (props.isAllComplete) return
  36. if (!props.resumableUploads) {
  37. return props.cancelAll()
  38. }
  39. if (props.isAllPaused) {
  40. return props.resumeAll()
  41. }
  42. return props.pauseAll()
  43. }
  44. module.exports = (props) => {
  45. props = props || {}
  46. const {
  47. newFiles,
  48. allowNewUpload,
  49. isUploadInProgress,
  50. isAllPaused,
  51. resumableUploads,
  52. error,
  53. hideUploadButton,
  54. hidePauseResumeButton,
  55. hideCancelButton,
  56. hideRetryButton
  57. } = props
  58. const uploadState = props.uploadState
  59. let progressValue = props.totalProgress
  60. let progressMode
  61. let progressBarContent
  62. if (uploadState === statusBarStates.STATE_PREPROCESSING || uploadState === statusBarStates.STATE_POSTPROCESSING) {
  63. const progress = calculateProcessingProgress(props.files)
  64. progressMode = progress.mode
  65. if (progressMode === 'determinate') {
  66. progressValue = progress.value * 100
  67. }
  68. progressBarContent = ProgressBarProcessing(progress)
  69. } else if (uploadState === statusBarStates.STATE_COMPLETE) {
  70. progressBarContent = ProgressBarComplete(props)
  71. } else if (uploadState === statusBarStates.STATE_UPLOADING) {
  72. if (!props.supportsUploadProgress) {
  73. progressMode = 'indeterminate'
  74. progressValue = null
  75. }
  76. progressBarContent = ProgressBarUploading(props)
  77. } else if (uploadState === statusBarStates.STATE_ERROR) {
  78. progressValue = undefined
  79. progressBarContent = ProgressBarError(props)
  80. }
  81. const width = typeof progressValue === 'number' ? progressValue : 100
  82. const isHidden = (uploadState === statusBarStates.STATE_WAITING && props.hideUploadButton) ||
  83. (uploadState === statusBarStates.STATE_WAITING && !props.newFiles > 0) ||
  84. (uploadState === statusBarStates.STATE_COMPLETE && props.hideAfterFinish)
  85. const showUploadBtn = !error && newFiles &&
  86. !isUploadInProgress && !isAllPaused &&
  87. allowNewUpload && !hideUploadButton
  88. const showCancelBtn = !hideCancelButton &&
  89. uploadState !== statusBarStates.STATE_WAITING &&
  90. uploadState !== statusBarStates.STATE_COMPLETE
  91. const showPauseResumeBtn = resumableUploads && !hidePauseResumeButton &&
  92. uploadState === statusBarStates.STATE_UPLOADING
  93. const showRetryBtn = error && !hideRetryButton
  94. const progressClassNames = `uppy-StatusBar-progress
  95. ${progressMode ? 'is-' + progressMode : ''}`
  96. const statusBarClassNames = classNames(
  97. { 'uppy-Root': props.isTargetDOMEl },
  98. 'uppy-StatusBar',
  99. `is-${uploadState}`
  100. )
  101. return (
  102. <div class={statusBarClassNames} aria-hidden={isHidden}>
  103. <div
  104. class={progressClassNames}
  105. style={{ width: width + '%' }}
  106. role="progressbar"
  107. aria-valuemin="0"
  108. aria-valuemax="100"
  109. aria-valuenow={progressValue}
  110. />
  111. {progressBarContent}
  112. <div class="uppy-StatusBar-actions">
  113. {showUploadBtn ? <UploadBtn {...props} uploadState={uploadState} /> : null}
  114. {showRetryBtn ? <RetryBtn {...props} /> : null}
  115. {showPauseResumeBtn ? <PauseResumeButton {...props} /> : null}
  116. {showCancelBtn ? <CancelBtn {...props} /> : null}
  117. </div>
  118. </div>
  119. )
  120. }
  121. const UploadBtn = (props) => {
  122. const uploadBtnClassNames = classNames(
  123. 'uppy-u-reset',
  124. 'uppy-c-btn',
  125. 'uppy-StatusBar-actionBtn',
  126. 'uppy-StatusBar-actionBtn--upload',
  127. { 'uppy-c-btn-primary': props.uploadState === statusBarStates.STATE_WAITING }
  128. )
  129. return (
  130. <button
  131. type="button"
  132. class={uploadBtnClassNames}
  133. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  134. onclick={props.startUpload}
  135. data-uppy-super-focusable
  136. >
  137. {props.newFiles && props.isUploadStarted
  138. ? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
  139. : props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  140. </button>
  141. )
  142. }
  143. const RetryBtn = (props) => {
  144. return (
  145. <button
  146. type="button"
  147. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
  148. aria-label={props.i18n('retryUpload')}
  149. onclick={props.retryAll}
  150. data-uppy-super-focusable
  151. >
  152. <svg aria-hidden="true" focusable="false" class="UppyIcon" width="8" height="10" viewBox="0 0 8 10">
  153. <path d="M4 2.408a2.75 2.75 0 1 0 2.75 2.75.626.626 0 0 1 1.25.018v.023a4 4 0 1 1-4-4.041V.25a.25.25 0 0 1 .389-.208l2.299 1.533a.25.25 0 0 1 0 .416l-2.3 1.533A.25.25 0 0 1 4 3.316v-.908z" />
  154. </svg>
  155. {props.i18n('retry')}
  156. </button>
  157. )
  158. }
  159. const CancelBtn = (props) => {
  160. return (
  161. <button
  162. type="button"
  163. class="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  164. title={props.i18n('cancel')}
  165. aria-label={props.i18n('cancel')}
  166. onclick={props.cancelAll}
  167. data-uppy-super-focusable
  168. >
  169. <svg aria-hidden="true" focusable="false" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16">
  170. <g fill="none" fill-rule="evenodd">
  171. <circle fill="#888" cx="8" cy="8" r="8" />
  172. <path fill="#FFF" d="M9.283 8l2.567 2.567-1.283 1.283L8 9.283 5.433 11.85 4.15 10.567 6.717 8 4.15 5.433 5.433 4.15 8 6.717l2.567-2.567 1.283 1.283z" />
  173. </g>
  174. </svg>
  175. </button>
  176. )
  177. }
  178. const PauseResumeButton = (props) => {
  179. const { isAllPaused, i18n } = props
  180. const title = isAllPaused ? i18n('resume') : i18n('pause')
  181. return (
  182. <button
  183. title={title}
  184. aria-label={title}
  185. class="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  186. type="button"
  187. onclick={() => togglePauseResume(props)}
  188. data-uppy-super-focusable
  189. >
  190. {isAllPaused ? (
  191. <svg aria-hidden="true" focusable="false" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16">
  192. <g fill="none" fill-rule="evenodd">
  193. <circle fill="#888" cx="8" cy="8" r="8" />
  194. <path fill="#FFF" d="M6 4.25L11.5 8 6 11.75z" />
  195. </g>
  196. </svg>
  197. ) : (
  198. <svg aria-hidden="true" focusable="false" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16">
  199. <g fill="none" fill-rule="evenodd">
  200. <circle fill="#888" cx="8" cy="8" r="8" />
  201. <path d="M5 4.5h2v7H5v-7zm4 0h2v7H9v-7z" fill="#FFF" />
  202. </g>
  203. </svg>
  204. )}
  205. </button>
  206. )
  207. }
  208. const LoadingSpinner = () => {
  209. return (
  210. <svg class="uppy-StatusBar-spinner" aria-hidden="true" focusable="false" width="14" height="14">
  211. <path d="M13.983 6.547c-.12-2.509-1.64-4.893-3.939-5.936-2.48-1.127-5.488-.656-7.556 1.094C.524 3.367-.398 6.048.162 8.562c.556 2.495 2.46 4.52 4.94 5.183 2.932.784 5.61-.602 7.256-3.015-1.493 1.993-3.745 3.309-6.298 2.868-2.514-.434-4.578-2.349-5.153-4.84a6.226 6.226 0 0 1 2.98-6.778C6.34.586 9.74 1.1 11.373 3.493c.407.596.693 1.282.842 1.988.127.598.073 1.197.161 1.794.078.525.543 1.257 1.15.864.525-.341.49-1.05.456-1.592-.007-.15.02.3 0 0" fill-rule="evenodd" />
  212. </svg>
  213. )
  214. }
  215. const ProgressBarProcessing = (props) => {
  216. const value = Math.round(props.value * 100)
  217. return (
  218. <div class="uppy-StatusBar-content">
  219. <LoadingSpinner />
  220. {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
  221. {props.message}
  222. </div>
  223. )
  224. }
  225. const renderDot = () =>
  226. ' \u00B7 '
  227. const ProgressDetails = (props) => {
  228. const ifShowFilesUploadedOfTotal = props.numUploads > 1
  229. return (
  230. <div class="uppy-StatusBar-statusSecondary">
  231. {
  232. ifShowFilesUploadedOfTotal &&
  233. props.i18n('filesUploadedOfTotal', {
  234. complete: props.complete,
  235. smart_count: props.numUploads
  236. })
  237. }
  238. <span class="uppy-StatusBar-additionalInfo">
  239. {/* When should we render this dot?
  240. 1. .-additionalInfo is shown (happens only on desktops)
  241. 2. AND 'filesUploadedOfTotal' was shown
  242. */}
  243. {ifShowFilesUploadedOfTotal && renderDot()}
  244. {
  245. props.i18n('dataUploadedOfTotal', {
  246. complete: prettyBytes(props.totalUploadedSize),
  247. total: prettyBytes(props.totalSize)
  248. })
  249. }
  250. {renderDot()}
  251. {
  252. props.i18n('xTimeLeft', {
  253. time: prettyETA(props.totalETA)
  254. })
  255. }
  256. </span>
  257. </div>
  258. )
  259. }
  260. const UnknownProgressDetails = (props) => {
  261. return (
  262. <div class="uppy-StatusBar-statusSecondary">
  263. {props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads })}
  264. </div>
  265. )
  266. }
  267. const UploadNewlyAddedFiles = (props) => {
  268. const uploadBtnClassNames = classNames(
  269. 'uppy-u-reset',
  270. 'uppy-c-btn',
  271. 'uppy-StatusBar-actionBtn',
  272. 'uppy-StatusBar-actionBtn--uploadNewlyAdded'
  273. )
  274. return (
  275. <div class="uppy-StatusBar-statusSecondary">
  276. <div class="uppy-StatusBar-statusSecondaryHint">
  277. {props.i18n('xMoreFilesAdded', { smart_count: props.newFiles })}
  278. </div>
  279. <button
  280. type="button"
  281. class={uploadBtnClassNames}
  282. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  283. onclick={props.startUpload}
  284. >
  285. {props.i18n('upload')}
  286. </button>
  287. </div>
  288. )
  289. }
  290. const ThrottledProgressDetails = throttle(ProgressDetails, 500, { leading: true, trailing: true })
  291. const ProgressBarUploading = (props) => {
  292. if (!props.isUploadStarted || props.isAllComplete) {
  293. return null
  294. }
  295. const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
  296. const showUploadNewlyAddedFiles = props.newFiles && props.isUploadStarted
  297. return (
  298. <div class="uppy-StatusBar-content" aria-label={title} title={title}>
  299. {!props.isAllPaused ? <LoadingSpinner /> : null}
  300. <div class="uppy-StatusBar-status">
  301. <div class="uppy-StatusBar-statusPrimary">
  302. {props.supportsUploadProgress ? `${title}: ${props.totalProgress}%` : title}
  303. </div>
  304. {!props.isAllPaused && !showUploadNewlyAddedFiles && props.showProgressDetails
  305. ? (props.supportsUploadProgress ? <ThrottledProgressDetails {...props} /> : <UnknownProgressDetails {...props} />)
  306. : null}
  307. {showUploadNewlyAddedFiles ? <UploadNewlyAddedFiles {...props} /> : null}
  308. </div>
  309. </div>
  310. )
  311. }
  312. const ProgressBarComplete = ({ totalProgress, i18n }) => {
  313. return (
  314. <div class="uppy-StatusBar-content" role="status" title={i18n('complete')}>
  315. <div class="uppy-StatusBar-status">
  316. <div class="uppy-StatusBar-statusPrimary">
  317. <svg aria-hidden="true" focusable="false" class="uppy-StatusBar-statusIndicator UppyIcon" width="15" height="11" viewBox="0 0 15 11">
  318. <path d="M.414 5.843L1.627 4.63l3.472 3.472L13.202 0l1.212 1.213L5.1 10.528z" />
  319. </svg>
  320. {i18n('complete')}
  321. </div>
  322. </div>
  323. </div>
  324. )
  325. }
  326. const ProgressBarError = ({ error, retryAll, hideRetryButton, i18n }) => {
  327. function displayErrorAlert () {
  328. const errorMessage = `${i18n('uploadFailed')} \n\n ${error}`
  329. alert(errorMessage)
  330. }
  331. return (
  332. <div class="uppy-StatusBar-content" role="alert" title={i18n('uploadFailed')}>
  333. <div class="uppy-StatusBar-status">
  334. <div class="uppy-StatusBar-statusPrimary">
  335. <svg aria-hidden="true" focusable="false" class="uppy-StatusBar-statusIndicator UppyIcon" width="11" height="11" viewBox="0 0 11 11">
  336. <path d="M4.278 5.5L0 1.222 1.222 0 5.5 4.278 9.778 0 11 1.222 6.722 5.5 11 9.778 9.778 11 5.5 6.722 1.222 11 0 9.778z" />
  337. </svg>
  338. {i18n('uploadFailed')}
  339. </div>
  340. </div>
  341. <span
  342. class="uppy-StatusBar-details"
  343. aria-label={error}
  344. data-microtip-position="top-right"
  345. data-microtip-size="medium"
  346. role="tooltip"
  347. onclick={displayErrorAlert}
  348. >
  349. ?
  350. </span>
  351. </div>
  352. )
  353. }