123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- import Utils from '../core/Utils'
- import Translator from '../core/Translator'
- import ee from 'events'
- /**
- * Main Uppy core
- *
- * @param {object} opts general options, like locales, to show modal or not to show
- */
- export default class Core {
- constructor (opts) {
- // set default options
- const defaultOptions = {
- // load English as the default locales
- locales: require('../locales/en_US.js'),
- autoProceed: true,
- debug: false
- }
- // Merge default options with the ones set by user
- this.opts = Object.assign({}, defaultOptions, opts)
- // Dictates in what order different plugin types are ran:
- this.types = [ 'presetter', 'orchestrator', 'progressindicator', 'acquirer', 'uploader', 'presenter' ]
- this.type = 'core'
- // Container for different types of plugins
- this.plugins = {}
- this.translator = new Translator({locales: this.opts.locales})
- this.i18n = this.translator.translate.bind(this.translator)
- this.emitter = new ee.EventEmitter()
- this.state = {
- files: {}
- }
- // for debugging and testing
- global.UppyState = this.state
- global.UppyAddFiles = this.addFiles.bind(this)
- }
- /**
- * Iterate on all plugins and run `update` on them. Called each time when state changes
- *
- */
- updateAll () {
- Object.keys(this.plugins).forEach((pluginType) => {
- this.plugins[pluginType].forEach((plugin) => {
- plugin.update()
- })
- })
- }
- /**
- * Updates state
- *
- * @param {newState} object
- */
- setState (newState) {
- this.log(`Setting state to: ${newState}`)
- this.state = Object.assign({}, this.state, newState)
- this.updateAll()
- }
- /**
- * Gets current state, making sure to make a copy of the state object and pass that,
- * instead of an actual reference to `this.state`
- *
- */
- getState () {
- return this.state
- }
- addImgPreviewToFile (file) {
- const reader = new FileReader()
- reader.addEventListener('load', (ev) => {
- const imgSrc = ev.target.result
- const updatedFiles = Object.assign({}, this.state.files)
- updatedFiles[file.id].preview = imgSrc
- this.setState({files: updatedFiles})
- })
- reader.addEventListener('error', (err) => {
- this.core.log('FileReader error' + err)
- })
- reader.readAsDataURL(file.data)
- }
- addMeta (meta, fileID) {
- if (typeof fileID === 'undefined') {
- const updatedFiles = Object.assign({}, this.state.files)
- for (let file in updatedFiles) {
- updatedFiles[file].meta = meta
- }
- this.setState({files: updatedFiles})
- }
- }
- addFile (fileData, fileName, fileType, caller, remote) {
- const updatedFiles = Object.assign({}, this.state.files)
- fileType = fileType.split('/')
- const fileTypeGeneral = fileType[0]
- const fileTypeSpecific = fileType[1]
- const fileID = Utils.generateFileID(fileName)
- updatedFiles[fileID] = {
- acquiredBy: caller,
- id: fileID,
- name: fileName,
- type: {
- general: fileTypeGeneral,
- specific: fileTypeSpecific
- },
- data: fileData,
- progress: 0,
- remote: remote
- }
- this.setState({files: updatedFiles})
- // TODO figure out if and when we need image preview —
- // they eat a ton of memory and slow things down substantially
- if (fileTypeGeneral === 'image') {
- this.addImgPreviewToFile(updatedFiles[fileID])
- }
- if (this.opts.autoProceed) {
- this.emitter.emit('next')
- }
- }
- // TODO: deprecated, switch to `addFile` instead
- addFiles (files, caller) {
- const updatedFiles = Object.assign({}, this.state.files)
- files.forEach((file) => {
- if (!file.remote) {
- const fileName = file.name
- const fileType = file.type.split('/')
- const fileTypeGeneral = fileType[0]
- const fileTypeSpecific = fileType[1]
- const fileID = Utils.generateFileID(fileName)
- updatedFiles[fileID] = {
- acquiredBy: caller,
- id: fileID,
- name: fileName,
- type: {
- general: fileTypeGeneral,
- specific: fileTypeSpecific
- },
- data: file,
- progress: 0,
- uploadURL: ''
- }
- } else {
- updatedFiles[file.id] = {
- acquiredBy: caller,
- data: file
- }
- }
- // TODO figure out if and when we need image preview —
- // they eat a ton of memory and slow things down substantially
- // if (fileTypeGeneral === 'image') {
- // this.addImgPreviewToFile(updatedFiles[fileID])
- // }
- })
- this.setState({files: updatedFiles})
- if (this.opts.autoProceed) {
- this.emitter.emit('next')
- }
- }
- /**
- * Registers listeners for all global actions, like:
- * `file-add`, `file-remove`, `upload-progress`, `reset`
- *
- */
- actions () {
- this.emitter.on('file-add', (data) => {
- const { acquiredFiles, plugin } = data
- // this.addFiles(acquiredFiles, plugin)
- acquiredFiles.forEach((file) => {
- this.addFile(file.data, file.name, file.type, plugin, file.remote)
- })
- })
- // `remove-file` removes a file from `state.files`, after successfull upload
- // or when a user deicdes not to upload particular file and clicks a button to remove it
- this.emitter.on('file-remove', (fileID) => {
- const updatedFiles = Object.assign({}, this.state.files)
- delete updatedFiles[fileID]
- this.setState({files: updatedFiles})
- })
- this.emitter.on('upload-progress', (progressData) => {
- const updatedFiles = Object.assign({}, this.state.files)
- updatedFiles[progressData.id].progress = progressData.percentage
- const inProgress = Object.keys(updatedFiles).map((file) => {
- return file.progress !== 0
- })
- // calculate total progress, using the number of files currently uploading,
- // multiplied by 100 and the summ of individual progress of each file
- const progressMax = Object.keys(inProgress).length * 100
- let progressAll = 0
- Object.keys(updatedFiles).forEach((file) => {
- progressAll = progressAll + updatedFiles[file].progress
- })
- const totalProgress = progressAll * 100 / progressMax
- this.setState({
- totalProgress: totalProgress,
- files: updatedFiles
- })
- })
- // `upload-success` adds successfully uploaded file to `state.uploadedFiles`
- // and fires `remove-file` to remove it from `state.files`
- this.emitter.on('upload-success', (file) => {
- const updatedFiles = Object.assign({}, this.state.files)
- updatedFiles[file.id] = file
- this.setState({files: updatedFiles})
- // this.log(this.state.uploadedFiles)
- // this.emitter.emit('file-remove', file.id)
- })
- }
- /**
- * Registers a plugin with Core
- *
- * @param {Class} Plugin object
- * @param {Object} options object that will be passed to Plugin later
- * @return {Object} self for chaining
- */
- use (Plugin, opts) {
- // Instantiate
- const plugin = new Plugin(this, opts)
- const pluginName = plugin.id
- this.plugins[plugin.type] = this.plugins[plugin.type] || []
- if (!pluginName) {
- throw new Error('Your plugin must have a name')
- }
- if (!plugin.type) {
- throw new Error('Your plugin must have a type')
- }
- let existsPluginAlready = this.getPlugin(pluginName)
- if (existsPluginAlready) {
- let msg = `Already found a plugin named '${existsPluginAlready.name}'.
- Tried to use: '${pluginName}'.
- Uppy is currently limited to running one of every plugin.
- Share your use case with us over at
- https://github.com/transloadit/uppy/issues/
- if you want us to reconsider.`
- throw new Error(msg)
- }
- this.plugins[plugin.type].push(plugin)
- return this
- }
- /**
- * Find one Plugin by name
- *
- * @param string name description
- */
- getPlugin (name) {
- let foundPlugin = false
- this.iteratePlugins((plugin) => {
- const pluginName = plugin.id
- if (pluginName === name) {
- foundPlugin = plugin
- return false
- }
- })
- return foundPlugin
- }
- /**
- * Iterate through all `use`d plugins
- *
- * @param function method description
- */
- iteratePlugins (method) {
- Object.keys(this.plugins).forEach((pluginType) => {
- this.plugins[pluginType].forEach(method)
- })
- }
- /**
- * Logs stuff to console, only if `debug` is set to true. Silent in production.
- *
- * @return {String|Object} to log
- */
- log (msg) {
- if (!this.opts.debug) {
- return
- }
- if (msg === `${msg}`) {
- console.log(`LOG: ${msg}`)
- } else {
- console.log('LOG')
- console.dir(msg)
- }
- global.uppyLog = global.uppyLog || ''
- global.uppyLog = global.uppyLog + '\n' + 'DEBUG LOG: ' + msg
- }
- /**
- * Runs all plugins of the same type in parallel
- *
- * @param {string} type that wants to set progress
- * @param {array} files
- * @return {Promise} of all methods
- */
- runType (type, method, files) {
- const methods = this.plugins[type].map(
- (plugin) => plugin[method](Utils.flatten(files))
- )
- return Promise.all(methods)
- .catch((error) => console.error(error))
- }
- /**
- * Runs a waterfall of runType plugin packs, like so:
- * All preseters(data) --> All acquirers(data) --> All uploaders(data) --> done
- */
- run () {
- this.log({
- class: this.constructor.name,
- method: 'run'
- })
- this.actions()
- // Forse set `autoProceed` option to false if there are multiple selector Plugins active
- if (this.plugins.acquirer && this.plugins.acquirer.length > 1) {
- this.opts.autoProceed = false
- }
- // Install all plugins
- Object.keys(this.plugins).forEach((pluginType) => {
- this.plugins[pluginType].forEach((plugin) => {
- plugin.install()
- })
- })
- return
- // Each Plugin can have `run` and/or `install` methods.
- // `install` adds event listeners and does some non-blocking work, useful for `progressindicator`,
- // `run` waits for the previous step to finish (user selects files) before proceeding
- // ['install', 'run'].forEach((method) => {
- // // First we select only plugins of current type,
- // // then create an array of runType methods of this plugins
- // const typeMethods = this.types.filter((type) => this.plugins[type])
- // .map((type) => this.runType.bind(this, type, method))
- // // Run waterfall of typeMethods
- // return Utils.promiseWaterfall(typeMethods)
- // .then((result) => {
- // // If results are empty, don't log upload results. Hasn't run yet.
- // if (result[0] !== undefined) {
- // this.log(result)
- // this.log('Upload result -> success!')
- // return result
- // }
- // })
- // .catch((error) => this.log('Upload result -> failed:', error))
- // })
- }
- }
|