index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. const Plugin = require('../Plugin')
  2. const Translator = require('../../core/Translator')
  3. const dragDrop = require('drag-drop')
  4. const Dashboard = require('./Dashboard')
  5. const StatusBar = require('../StatusBar')
  6. const Informer = require('../Informer')
  7. const { findAllDOMElements } = require('../../core/Utils')
  8. const prettyBytes = require('prettier-bytes')
  9. const { defaultTabIcon } = require('./icons')
  10. /**
  11. * Modal Dialog & Dashboard
  12. */
  13. module.exports = class DashboardUI extends Plugin {
  14. constructor (core, opts) {
  15. super(core, opts)
  16. this.id = 'Dashboard'
  17. this.title = 'Dashboard'
  18. this.type = 'orchestrator'
  19. const defaultLocale = {
  20. strings: {
  21. selectToUpload: 'Select files to upload',
  22. closeModal: 'Close Modal',
  23. upload: 'Upload',
  24. importFrom: 'Import files from',
  25. dashboardWindowTitle: 'Uppy Dashboard Window (Press escape to close)',
  26. dashboardTitle: 'Uppy Dashboard',
  27. copyLinkToClipboardSuccess: 'Link copied to clipboard.',
  28. copyLinkToClipboardFallback: 'Copy the URL below',
  29. fileSource: 'File source',
  30. done: 'Done',
  31. localDisk: 'Local Disk',
  32. myDevice: 'My Device',
  33. dropPasteImport: 'Drop files here, paste, import from one of the locations above or',
  34. dropPaste: 'Drop files here, paste or',
  35. browse: 'browse',
  36. fileProgress: 'File progress: upload speed and ETA',
  37. numberOfSelectedFiles: 'Number of selected files',
  38. uploadAllNewFiles: 'Upload all new files'
  39. }
  40. }
  41. // set default options
  42. const defaultOptions = {
  43. target: 'body',
  44. getMetaFromForm: true,
  45. trigger: '#uppy-select-files',
  46. inline: false,
  47. width: 750,
  48. height: 550,
  49. semiTransparent: false,
  50. defaultTabIcon: defaultTabIcon(),
  51. showProgressDetails: false,
  52. hideUploadButton: false,
  53. note: false,
  54. closeModalOnClickOutside: false,
  55. locale: defaultLocale,
  56. onRequestCloseModal: () => this.closeModal()
  57. }
  58. // merge default options with the ones set by user
  59. this.opts = Object.assign({}, defaultOptions, opts)
  60. this.locale = Object.assign({}, defaultLocale, this.opts.locale)
  61. this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  62. this.translator = new Translator({locale: this.locale})
  63. this.containerWidth = this.translator.translate.bind(this.translator)
  64. this.closeModal = this.closeModal.bind(this)
  65. this.requestCloseModal = this.requestCloseModal.bind(this)
  66. this.openModal = this.openModal.bind(this)
  67. this.isModalOpen = this.isModalOpen.bind(this)
  68. this.addTarget = this.addTarget.bind(this)
  69. this.actions = this.actions.bind(this)
  70. this.hideAllPanels = this.hideAllPanels.bind(this)
  71. this.showPanel = this.showPanel.bind(this)
  72. this.initEvents = this.initEvents.bind(this)
  73. this.handleEscapeKeyPress = this.handleEscapeKeyPress.bind(this)
  74. this.handleClickOutside = this.handleClickOutside.bind(this)
  75. this.handleFileCard = this.handleFileCard.bind(this)
  76. this.handleDrop = this.handleDrop.bind(this)
  77. this.pauseAll = this.pauseAll.bind(this)
  78. this.resumeAll = this.resumeAll.bind(this)
  79. this.cancelAll = this.cancelAll.bind(this)
  80. this.updateDashboardElWidth = this.updateDashboardElWidth.bind(this)
  81. this.render = this.render.bind(this)
  82. this.install = this.install.bind(this)
  83. }
  84. addTarget (plugin) {
  85. const callerPluginId = plugin.id || plugin.constructor.name
  86. const callerPluginName = plugin.title || callerPluginId
  87. const callerPluginIcon = plugin.icon || this.opts.defaultTabIcon
  88. const callerPluginType = plugin.type
  89. if (callerPluginType !== 'acquirer' &&
  90. callerPluginType !== 'progressindicator' &&
  91. callerPluginType !== 'presenter') {
  92. let msg = 'Dashboard: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
  93. this.core.log(msg)
  94. return
  95. }
  96. const target = {
  97. id: callerPluginId,
  98. name: callerPluginName,
  99. icon: callerPluginIcon,
  100. type: callerPluginType,
  101. render: plugin.render,
  102. isHidden: true
  103. }
  104. const modal = this.core.getState().modal
  105. const newTargets = modal.targets.slice()
  106. newTargets.push(target)
  107. this.core.setState({
  108. modal: Object.assign({}, modal, {
  109. targets: newTargets
  110. })
  111. })
  112. return this.target
  113. }
  114. hideAllPanels () {
  115. const modal = this.core.getState().modal
  116. this.core.setState({modal: Object.assign({}, modal, {
  117. activePanel: false
  118. })})
  119. }
  120. showPanel (id) {
  121. const modal = this.core.getState().modal
  122. const activePanel = modal.targets.filter((target) => {
  123. return target.type === 'acquirer' && target.id === id
  124. })[0]
  125. this.core.setState({modal: Object.assign({}, modal, {
  126. activePanel: activePanel
  127. })})
  128. }
  129. requestCloseModal () {
  130. if (this.opts.onRequestCloseModal) {
  131. return this.opts.onRequestCloseModal()
  132. } else {
  133. this.closeModal()
  134. }
  135. }
  136. openModal () {
  137. const modal = this.core.getState().modal
  138. this.core.setState({
  139. modal: Object.assign({}, modal, {
  140. isHidden: false
  141. })
  142. })
  143. // save scroll position
  144. this.savedDocumentScrollPosition = window.scrollY
  145. // add class to body that sets position fixed, move everything back
  146. // to scroll position
  147. document.body.classList.add('is-UppyDashboard-open')
  148. document.body.style.top = `-${this.savedDocumentScrollPosition}px`
  149. // focus on modal inner block
  150. this.target.querySelector('.UppyDashboard-inner').focus()
  151. // this.updateDashboardElWidth()
  152. // to be sure, sometimes when the function runs, container size is still 0
  153. setTimeout(this.updateDashboardElWidth, 500)
  154. }
  155. closeModal () {
  156. const modal = this.core.getState().modal
  157. this.core.setState({
  158. modal: Object.assign({}, modal, {
  159. isHidden: true
  160. })
  161. })
  162. document.body.classList.remove('is-UppyDashboard-open')
  163. window.scrollTo(0, this.savedDocumentScrollPosition)
  164. }
  165. isModalOpen () {
  166. return !this.core.getState().modal.isHidden || false
  167. }
  168. // Close the Modal on esc key press
  169. handleEscapeKeyPress (event) {
  170. if (event.keyCode === 27) {
  171. this.requestCloseModal()
  172. }
  173. }
  174. handleClickOutside () {
  175. if (this.opts.closeModalOnClickOutside) this.requestCloseModal()
  176. }
  177. initEvents () {
  178. // Modal open button
  179. const showModalTrigger = findAllDOMElements(this.opts.trigger)
  180. if (!this.opts.inline && showModalTrigger) {
  181. showModalTrigger.forEach(trigger => trigger.addEventListener('click', this.openModal))
  182. }
  183. if (!this.opts.inline && !showModalTrigger) {
  184. this.core.log('Dashboard modal trigger not found, you won’t be able to select files. Make sure `trigger` is set correctly in Dashboard options', 'error')
  185. }
  186. document.body.addEventListener('keyup', this.handleEscapeKeyPress)
  187. // Drag Drop
  188. this.removeDragDropListener = dragDrop(this.el, (files) => {
  189. this.handleDrop(files)
  190. })
  191. }
  192. removeEvents () {
  193. const showModalTrigger = findAllDOMElements(this.opts.trigger)
  194. if (!this.opts.inline && showModalTrigger) {
  195. showModalTrigger.forEach(trigger => trigger.removeEventListener('click', this.openModal))
  196. }
  197. this.removeDragDropListener()
  198. document.body.removeEventListener('keyup', this.handleEscapeKeyPress)
  199. }
  200. actions () {
  201. this.core.on('core:file-added', this.hideAllPanels)
  202. this.core.on('dashboard:file-card', this.handleFileCard)
  203. window.addEventListener('resize', this.updateDashboardElWidth)
  204. }
  205. removeActions () {
  206. window.removeEventListener('resize', this.updateDashboardElWidth)
  207. this.core.off('core:file-added', this.hideAllPanels)
  208. this.core.off('dashboard:file-card', this.handleFileCard)
  209. }
  210. updateDashboardElWidth () {
  211. const dashboardEl = this.target.querySelector('.UppyDashboard-inner')
  212. this.core.log(`Dashboard width: ${dashboardEl.offsetWidth}`)
  213. const modal = this.core.getState().modal
  214. this.core.setState({
  215. modal: Object.assign({}, modal, {
  216. containerWidth: dashboardEl.offsetWidth
  217. })
  218. })
  219. }
  220. handleFileCard (fileId) {
  221. const modal = this.core.state.modal
  222. this.core.setState({
  223. modal: Object.assign({}, modal, {
  224. fileCardFor: fileId || false
  225. })
  226. })
  227. }
  228. handleDrop (files) {
  229. this.core.log('[Dashboard] Files were dropped')
  230. files.forEach((file) => {
  231. this.core.addFile({
  232. source: this.id,
  233. name: file.name,
  234. type: file.type,
  235. data: file
  236. })
  237. })
  238. }
  239. cancelAll () {
  240. this.core.emit('core:cancel-all')
  241. }
  242. pauseAll () {
  243. this.core.emit('core:pause-all')
  244. }
  245. resumeAll () {
  246. this.core.emit('core:resume-all')
  247. }
  248. render (state) {
  249. const files = state.files
  250. const newFiles = Object.keys(files).filter((file) => {
  251. return !files[file].progress.uploadStarted
  252. })
  253. const inProgressFiles = Object.keys(files).filter((file) => {
  254. return !files[file].progress.uploadComplete &&
  255. files[file].progress.uploadStarted &&
  256. !files[file].isPaused
  257. })
  258. let inProgressFilesArray = []
  259. inProgressFiles.forEach((file) => {
  260. inProgressFilesArray.push(files[file])
  261. })
  262. let totalSize = 0
  263. let totalUploadedSize = 0
  264. inProgressFilesArray.forEach((file) => {
  265. totalSize = totalSize + (file.progress.bytesTotal || 0)
  266. totalUploadedSize = totalUploadedSize + (file.progress.bytesUploaded || 0)
  267. })
  268. totalSize = prettyBytes(totalSize)
  269. totalUploadedSize = prettyBytes(totalUploadedSize)
  270. const acquirers = state.modal.targets.filter((target) => {
  271. return target.type === 'acquirer'
  272. })
  273. const progressindicators = state.modal.targets.filter((target) => {
  274. return target.type === 'progressindicator'
  275. })
  276. const startUpload = (ev) => {
  277. this.core.upload().catch((err) => {
  278. // Log error.
  279. this.core.log(err.stack || err.message || err)
  280. })
  281. }
  282. const pauseUpload = (fileID) => {
  283. this.core.emit('core:upload-pause', fileID)
  284. }
  285. const cancelUpload = (fileID) => {
  286. this.core.emit('core:upload-cancel', fileID)
  287. this.core.emit('core:file-remove', fileID)
  288. }
  289. const showFileCard = (fileID) => {
  290. this.core.emit('dashboard:file-card', fileID)
  291. }
  292. const fileCardDone = (meta, fileID) => {
  293. this.core.emit('core:update-meta', meta, fileID)
  294. this.core.emit('dashboard:file-card')
  295. }
  296. return Dashboard({
  297. state: state,
  298. modal: state.modal,
  299. newFiles: newFiles,
  300. files: files,
  301. totalFileCount: Object.keys(files).length,
  302. totalProgress: state.totalProgress,
  303. acquirers: acquirers,
  304. activePanel: state.modal.activePanel,
  305. progressindicators: progressindicators,
  306. autoProceed: this.core.opts.autoProceed,
  307. hideUploadButton: this.opts.hideUploadButton,
  308. id: this.id,
  309. closeModal: this.requestCloseModal,
  310. handleClickOutside: this.handleClickOutside,
  311. showProgressDetails: this.opts.showProgressDetails,
  312. inline: this.opts.inline,
  313. semiTransparent: this.opts.semiTransparent,
  314. showPanel: this.showPanel,
  315. hideAllPanels: this.hideAllPanels,
  316. log: this.core.log,
  317. i18n: this.containerWidth,
  318. pauseAll: this.pauseAll,
  319. resumeAll: this.resumeAll,
  320. addFile: this.core.addFile,
  321. removeFile: this.core.removeFile,
  322. info: this.core.info,
  323. note: this.opts.note,
  324. metaFields: state.metaFields,
  325. resumableUploads: this.core.state.capabilities.resumableUploads || false,
  326. startUpload: startUpload,
  327. pauseUpload: pauseUpload,
  328. cancelUpload: cancelUpload,
  329. fileCardFor: state.modal.fileCardFor,
  330. showFileCard: showFileCard,
  331. fileCardDone: fileCardDone,
  332. updateDashboardElWidth: this.updateDashboardElWidth,
  333. maxWidth: this.opts.maxWidth,
  334. maxHeight: this.opts.maxHeight,
  335. currentWidth: state.modal.containerWidth,
  336. isWide: state.modal.containerWidth > 400
  337. })
  338. }
  339. discoverProviderPlugins () {
  340. this.core.iteratePlugins((plugin) => {
  341. if (plugin && !plugin.target && plugin.opts && plugin.opts.target === this.constructor) {
  342. this.addTarget(plugin)
  343. }
  344. })
  345. }
  346. install () {
  347. // Set default state for Modal
  348. this.core.setState({modal: {
  349. isHidden: true,
  350. showFileCard: false,
  351. activePanel: false,
  352. targets: []
  353. }})
  354. const target = this.opts.target
  355. const plugin = this
  356. this.target = this.mount(target, plugin)
  357. if (!this.opts.disableStatusBar) {
  358. this.core.use(StatusBar, {
  359. target: this.constructor
  360. })
  361. }
  362. if (!this.opts.disableInformer) {
  363. this.core.use(Informer, {
  364. target: this.constructor
  365. })
  366. }
  367. this.discoverProviderPlugins()
  368. this.initEvents()
  369. this.actions()
  370. }
  371. uninstall () {
  372. if (!this.opts.disableInformer) {
  373. const informer = this.core.getPlugin('Informer')
  374. if (informer) this.core.removePlugin(informer)
  375. }
  376. if (!this.opts.disableStatusBar) {
  377. const statusBar = this.core.getPlugin('StatusBarUI')
  378. // Checking if this plugin exists, in case it was removed by uppy-core
  379. // before the Dashboard was.
  380. if (statusBar) this.core.removePlugin(statusBar)
  381. }
  382. this.unmount()
  383. this.removeActions()
  384. this.removeEvents()
  385. }
  386. }