Core.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import Utils from '../core/Utils'
  2. import Translator from '../core/Translator'
  3. import ee from 'events'
  4. /**
  5. * Main Uppy core
  6. *
  7. * @param {object} opts general options, like locales, to show modal or not to show
  8. */
  9. export default class Core {
  10. constructor (opts) {
  11. // set default options
  12. const defaultOptions = {
  13. // load English as the default locales
  14. locales: require('../locales/en_US.js'),
  15. autoProceed: true,
  16. debug: false
  17. }
  18. // Merge default options with the ones set by user
  19. this.opts = Object.assign({}, defaultOptions, opts)
  20. // Dictates in what order different plugin types are ran:
  21. this.types = [ 'presetter', 'orchestrator', 'progressindicator', 'acquirer', 'uploader', 'presenter' ]
  22. this.type = 'core'
  23. // Container for different types of plugins
  24. this.plugins = {}
  25. this.translator = new Translator({locales: this.opts.locales})
  26. this.i18n = this.translator.translate.bind(this.translator)
  27. this.emitter = new ee.EventEmitter()
  28. this.state = {
  29. files: {}
  30. }
  31. // for debugging and testing
  32. global.UppyState = this.state
  33. global.UppyAddFiles = this.addFiles.bind(this)
  34. }
  35. /**
  36. * Iterate on all plugins and run `update` on them. Called each time when state changes
  37. *
  38. */
  39. updateAll () {
  40. Object.keys(this.plugins).forEach((pluginType) => {
  41. this.plugins[pluginType].forEach((plugin) => {
  42. plugin.update()
  43. })
  44. })
  45. }
  46. /**
  47. * Updates state
  48. *
  49. * @param {newState} object
  50. */
  51. setState (newState) {
  52. this.log(`Setting state to: ${newState}`)
  53. this.state = Object.assign({}, this.state, newState)
  54. this.updateAll()
  55. }
  56. /**
  57. * Gets current state, making sure to make a copy of the state object and pass that,
  58. * instead of an actual reference to `this.state`
  59. *
  60. */
  61. getState () {
  62. return this.state
  63. }
  64. addImgPreviewToFile (file) {
  65. const reader = new FileReader()
  66. reader.addEventListener('load', (ev) => {
  67. const imgSrc = ev.target.result
  68. const updatedFiles = Object.assign({}, this.state.files)
  69. updatedFiles[file.id].preview = imgSrc
  70. this.setState({files: updatedFiles})
  71. })
  72. reader.addEventListener('error', (err) => {
  73. this.core.log('FileReader error' + err)
  74. })
  75. reader.readAsDataURL(file.data)
  76. }
  77. addMeta (meta, fileID) {
  78. if (typeof fileID === 'undefined') {
  79. const updatedFiles = Object.assign({}, this.state.files)
  80. for (let file in updatedFiles) {
  81. updatedFiles[file].meta = meta
  82. }
  83. this.setState({files: updatedFiles})
  84. }
  85. }
  86. addFile (fileData, fileName, fileType, caller, remote) {
  87. const updatedFiles = Object.assign({}, this.state.files)
  88. fileType = fileType.split('/')
  89. const fileTypeGeneral = fileType[0]
  90. const fileTypeSpecific = fileType[1]
  91. const fileID = Utils.generateFileID(fileName)
  92. updatedFiles[fileID] = {
  93. acquiredBy: caller,
  94. id: fileID,
  95. name: fileName,
  96. type: {
  97. general: fileTypeGeneral,
  98. specific: fileTypeSpecific
  99. },
  100. data: fileData,
  101. progress: 0,
  102. remote: remote
  103. }
  104. this.setState({files: updatedFiles})
  105. // TODO figure out if and when we need image preview —
  106. // they eat a ton of memory and slow things down substantially
  107. if (fileTypeGeneral === 'image') {
  108. this.addImgPreviewToFile(updatedFiles[fileID])
  109. }
  110. if (this.opts.autoProceed) {
  111. this.emitter.emit('next')
  112. }
  113. }
  114. // TODO: deprecated, switch to `addFile` instead
  115. addFiles (files, caller) {
  116. const updatedFiles = Object.assign({}, this.state.files)
  117. files.forEach((file) => {
  118. if (!file.remote) {
  119. const fileName = file.name
  120. const fileType = file.type.split('/')
  121. const fileTypeGeneral = fileType[0]
  122. const fileTypeSpecific = fileType[1]
  123. const fileID = Utils.generateFileID(fileName)
  124. updatedFiles[fileID] = {
  125. acquiredBy: caller,
  126. id: fileID,
  127. name: fileName,
  128. type: {
  129. general: fileTypeGeneral,
  130. specific: fileTypeSpecific
  131. },
  132. data: file,
  133. progress: 0,
  134. uploadURL: ''
  135. }
  136. } else {
  137. updatedFiles[file.id] = {
  138. acquiredBy: caller,
  139. data: file
  140. }
  141. }
  142. // TODO figure out if and when we need image preview —
  143. // they eat a ton of memory and slow things down substantially
  144. // if (fileTypeGeneral === 'image') {
  145. // this.addImgPreviewToFile(updatedFiles[fileID])
  146. // }
  147. })
  148. this.setState({files: updatedFiles})
  149. if (this.opts.autoProceed) {
  150. this.emitter.emit('next')
  151. }
  152. }
  153. /**
  154. * Registers listeners for all global actions, like:
  155. * `file-add`, `file-remove`, `upload-progress`, `reset`
  156. *
  157. */
  158. actions () {
  159. this.emitter.on('file-add', (data) => {
  160. const { acquiredFiles, plugin } = data
  161. // this.addFiles(acquiredFiles, plugin)
  162. acquiredFiles.forEach((file) => {
  163. this.addFile(file.data, file.name, file.type, plugin, file.remote)
  164. })
  165. })
  166. // `remove-file` removes a file from `state.files`, after successfull upload
  167. // or when a user deicdes not to upload particular file and clicks a button to remove it
  168. this.emitter.on('file-remove', (fileID) => {
  169. const updatedFiles = Object.assign({}, this.state.files)
  170. delete updatedFiles[fileID]
  171. this.setState({files: updatedFiles})
  172. })
  173. this.emitter.on('upload-progress', (progressData) => {
  174. const updatedFiles = Object.assign({}, this.state.files)
  175. updatedFiles[progressData.id].progress = progressData.percentage
  176. const inProgress = Object.keys(updatedFiles).map((file) => {
  177. return file.progress !== 0
  178. })
  179. // calculate total progress, using the number of files currently uploading,
  180. // multiplied by 100 and the summ of individual progress of each file
  181. const progressMax = Object.keys(inProgress).length * 100
  182. let progressAll = 0
  183. Object.keys(updatedFiles).forEach((file) => {
  184. progressAll = progressAll + updatedFiles[file].progress
  185. })
  186. const totalProgress = progressAll * 100 / progressMax
  187. this.setState({
  188. totalProgress: totalProgress,
  189. files: updatedFiles
  190. })
  191. })
  192. // `upload-success` adds successfully uploaded file to `state.uploadedFiles`
  193. // and fires `remove-file` to remove it from `state.files`
  194. this.emitter.on('upload-success', (file) => {
  195. const updatedFiles = Object.assign({}, this.state.files)
  196. updatedFiles[file.id] = file
  197. this.setState({files: updatedFiles})
  198. // this.log(this.state.uploadedFiles)
  199. // this.emitter.emit('file-remove', file.id)
  200. })
  201. }
  202. /**
  203. * Registers a plugin with Core
  204. *
  205. * @param {Class} Plugin object
  206. * @param {Object} options object that will be passed to Plugin later
  207. * @return {Object} self for chaining
  208. */
  209. use (Plugin, opts) {
  210. // Instantiate
  211. const plugin = new Plugin(this, opts)
  212. const pluginName = plugin.id
  213. this.plugins[plugin.type] = this.plugins[plugin.type] || []
  214. if (!pluginName) {
  215. throw new Error('Your plugin must have a name')
  216. }
  217. if (!plugin.type) {
  218. throw new Error('Your plugin must have a type')
  219. }
  220. let existsPluginAlready = this.getPlugin(pluginName)
  221. if (existsPluginAlready) {
  222. let msg = `Already found a plugin named '${existsPluginAlready.name}'.
  223. Tried to use: '${pluginName}'.
  224. Uppy is currently limited to running one of every plugin.
  225. Share your use case with us over at
  226. https://github.com/transloadit/uppy/issues/
  227. if you want us to reconsider.`
  228. throw new Error(msg)
  229. }
  230. this.plugins[plugin.type].push(plugin)
  231. return this
  232. }
  233. /**
  234. * Find one Plugin by name
  235. *
  236. * @param string name description
  237. */
  238. getPlugin (name) {
  239. let foundPlugin = false
  240. this.iteratePlugins((plugin) => {
  241. const pluginName = plugin.id
  242. if (pluginName === name) {
  243. foundPlugin = plugin
  244. return false
  245. }
  246. })
  247. return foundPlugin
  248. }
  249. /**
  250. * Iterate through all `use`d plugins
  251. *
  252. * @param function method description
  253. */
  254. iteratePlugins (method) {
  255. Object.keys(this.plugins).forEach((pluginType) => {
  256. this.plugins[pluginType].forEach(method)
  257. })
  258. }
  259. /**
  260. * Logs stuff to console, only if `debug` is set to true. Silent in production.
  261. *
  262. * @return {String|Object} to log
  263. */
  264. log (msg) {
  265. if (!this.opts.debug) {
  266. return
  267. }
  268. if (msg === `${msg}`) {
  269. console.log(`LOG: ${msg}`)
  270. } else {
  271. console.log('LOG')
  272. console.dir(msg)
  273. }
  274. global.uppyLog = global.uppyLog || ''
  275. global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
  276. }
  277. /**
  278. * Runs all plugins of the same type in parallel
  279. *
  280. * @param {string} type that wants to set progress
  281. * @param {array} files
  282. * @return {Promise} of all methods
  283. */
  284. runType (type, method, files) {
  285. const methods = this.plugins[type].map(
  286. (plugin) => plugin[method](Utils.flatten(files))
  287. )
  288. return Promise.all(methods)
  289. .catch((error) => console.error(error))
  290. }
  291. /**
  292. * Runs a waterfall of runType plugin packs, like so:
  293. * All preseters(data) --> All acquirers(data) --> All uploaders(data) --> done
  294. */
  295. run () {
  296. this.log({
  297. class: this.constructor.name,
  298. method: 'run'
  299. })
  300. this.actions()
  301. // Forse set `autoProceed` option to false if there are multiple selector Plugins active
  302. if (this.plugins.acquirer && this.plugins.acquirer.length > 1) {
  303. this.opts.autoProceed = false
  304. }
  305. // Install all plugins
  306. Object.keys(this.plugins).forEach((pluginType) => {
  307. this.plugins[pluginType].forEach((plugin) => {
  308. plugin.install()
  309. })
  310. })
  311. return
  312. // Each Plugin can have `run` and/or `install` methods.
  313. // `install` adds event listeners and does some non-blocking work, useful for `progressindicator`,
  314. // `run` waits for the previous step to finish (user selects files) before proceeding
  315. // ['install', 'run'].forEach((method) => {
  316. // // First we select only plugins of current type,
  317. // // then create an array of runType methods of this plugins
  318. // const typeMethods = this.types.filter((type) => this.plugins[type])
  319. // .map((type) => this.runType.bind(this, type, method))
  320. // // Run waterfall of typeMethods
  321. // return Utils.promiseWaterfall(typeMethods)
  322. // .then((result) => {
  323. // // If results are empty, don't log upload results. Hasn't run yet.
  324. // if (result[0] !== undefined) {
  325. // this.log(result)
  326. // this.log('Upload result -> success!')
  327. // return result
  328. // }
  329. // })
  330. // .catch((error) => this.log('Upload result -> failed:', error))
  331. // })
  332. }
  333. }