index.js 15 KB

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