Prechádzať zdrojové kódy

Merge conflict, Editor.js

Artur Paikin 3 rokov pred
rodič
commit
bb1c615dc4
39 zmenil súbory, kde vykonal 402 pridanie a 454 odobranie
  1. 8 1
      examples/custom-provider/client/MyCustomProvider.js
  2. 1 1
      package-lock.json
  3. 0 13
      packages/@uppy/aws-s3/src/index.js
  4. 8 0
      packages/@uppy/box/src/index.js
  5. 59 65
      packages/@uppy/companion-client/src/RequestClient.js
  6. 32 31
      packages/@uppy/companion-client/src/Socket.js
  7. 12 10
      packages/@uppy/companion-client/src/Socket.test.js
  8. 1 1
      packages/@uppy/companion-client/types/index.d.ts
  9. 11 0
      packages/@uppy/core/src/BasePlugin.js
  10. 28 27
      packages/@uppy/core/src/index.js
  11. 12 45
      packages/@uppy/core/src/index.test.js
  12. 0 12
      packages/@uppy/dashboard/src/index.js
  13. 2 14
      packages/@uppy/drag-drop/src/index.js
  14. 8 0
      packages/@uppy/dropbox/src/index.js
  15. 8 0
      packages/@uppy/facebook/src/index.js
  16. 0 12
      packages/@uppy/file-input/src/index.js
  17. 8 0
      packages/@uppy/google-drive/src/index.js
  18. 73 50
      packages/@uppy/image-editor/src/Editor.js
  19. 5 13
      packages/@uppy/image-editor/src/index.js
  20. 6 1
      packages/@uppy/image-editor/types/index.d.ts
  21. 8 1
      packages/@uppy/instagram/src/index.js
  22. 8 0
      packages/@uppy/locales/src/en_US.js
  23. 8 0
      packages/@uppy/onedrive/src/index.js
  24. 1 3
      packages/@uppy/screen-capture/src/index.js
  25. 0 11
      packages/@uppy/status-bar/src/index.js
  26. 0 13
      packages/@uppy/thumbnail-generator/src/index.js
  27. 0 12
      packages/@uppy/transloadit/src/index.js
  28. 0 12
      packages/@uppy/url/src/index.js
  29. 2 12
      packages/@uppy/webcam/src/index.js
  30. 0 13
      packages/@uppy/xhr-upload/src/index.js
  31. 8 0
      packages/@uppy/zoom/src/index.js
  32. 5 5
      test/endtoend/chaos-monkey/test.js
  33. 3 3
      test/endtoend/transloadit/main.js
  34. 14 14
      test/endtoend/utils.js
  35. 1 1
      test/endtoend/wdio.base.conf.js
  36. 1 1
      test/resources/DeepFrozenStore.js
  37. 9 9
      website/private_modules/hexo-renderer-uppyexamples/index.js
  38. 5 1
      website/src/docs/image-editor.md
  39. 47 47
      website/themes/uppy/source/js/common.js

+ 8 - 1
examples/custom-provider/client/MyCustomProvider.js

@@ -10,7 +10,6 @@ module.exports = class MyCustomProvider extends UIPlugin {
     this.id = this.opts.id || 'MyCustomProvider'
     Provider.initPlugin(this, opts)
 
-    this.title = 'MyUnsplash'
     this.icon = () => (
       <svg width="32" height="32" xmlns="http://www.w3.org/2000/svg">
         <path d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z" fill="#000000" fillRule="nonzero" />
@@ -24,6 +23,14 @@ module.exports = class MyCustomProvider extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameMyUnsplash: 'MyUnsplash',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('MyUnsplash')
+
     this.files = []
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)

+ 1 - 1
package-lock.json

@@ -89718,7 +89718,7 @@
         "@uppy/google-drive": "*",
         "@uppy/informer": "file:../informer",
         "@uppy/provider-views": "file:../provider-views",
-        "@uppy/status-bar": "*",
+        "@uppy/status-bar": "file:../status-bar",
         "@uppy/thumbnail-generator": "file:../thumbnail-generator",
         "@uppy/utils": "file:../utils",
         "classnames": "^2.2.6",

+ 0 - 13
packages/@uppy/aws-s3/src/index.js

@@ -90,24 +90,11 @@ module.exports = class AwsS3 extends BasePlugin {
 
     this.opts = { ...defaultOptions, ...opts }
 
-    this.i18nInit()
-
     this.client = new RequestClient(uppy, opts)
     this.handleUpload = this.handleUpload.bind(this)
     this.requests = new RateLimitedQueue(this.opts.limit)
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   getUploadParameters (file) {
     if (!this.opts.companionUrl) {
       throw new Error('Expected a `companionUrl` option containing a Companion address.')

+ 8 - 0
packages/@uppy/box/src/index.js

@@ -32,6 +32,14 @@ module.exports = class Box extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameBox: 'Box',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameBox')
+
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)
   }

+ 59 - 65
packages/@uppy/companion-client/src/RequestClient.js

@@ -8,9 +8,33 @@ function stripSlash (url) {
   return url.replace(/\/$/, '')
 }
 
+async function handleJSONResponse (res) {
+  if (res.status === 401) {
+    throw new AuthError()
+  }
+
+  const jsonPromise = res.json()
+
+  if (res.status < 200 || res.status > 300) {
+    let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`
+    try {
+      const errData = await jsonPromise
+      errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg
+      errMsg = errData.requestId ? `${errMsg} request-Id: ${errData.requestId}` : errMsg
+    } finally {
+      // eslint-disable-next-line no-unsafe-finally
+      throw new Error(errMsg)
+    }
+  }
+  return jsonPromise
+}
+
 module.exports = class RequestClient {
+  // eslint-disable-next-line global-require
   static VERSION = require('../package.json').version
 
+  #getPostResponseFunc = skip => response => (skip ? response : this.onReceiveResponse(response))
+
   constructor (uppy, opts) {
     this.uppy = uppy
     this.opts = opts
@@ -25,32 +49,20 @@ module.exports = class RequestClient {
     return stripSlash(companion && companion[host] ? companion[host] : host)
   }
 
-  get defaultHeaders () {
-    return {
-      Accept: 'application/json',
-      'Content-Type': 'application/json',
-      'Uppy-Versions': `@uppy/companion-client=${RequestClient.VERSION}`,
-    }
+  static defaultHeaders ={
+    Accept: 'application/json',
+    'Content-Type': 'application/json',
+    'Uppy-Versions': `@uppy/companion-client=${RequestClient.VERSION}`,
   }
 
   headers () {
     const userHeaders = this.opts.companionHeaders || {}
     return Promise.resolve({
-      ...this.defaultHeaders,
+      ...RequestClient.defaultHeaders,
       ...userHeaders,
     })
   }
 
-  _getPostResponseFunc (skip) {
-    return (response) => {
-      if (!skip) {
-        return this.onReceiveResponse(response)
-      }
-
-      return response
-    }
-  }
-
   onReceiveResponse (response) {
     const state = this.uppy.getState()
     const companion = state.companion || {}
@@ -65,28 +77,22 @@ module.exports = class RequestClient {
     return response
   }
 
-  _getUrl (url) {
+  #getUrl (url) {
     if (/^(https?:|)\/\//.test(url)) {
       return url
     }
     return `${this.hostname}/${url}`
   }
 
-  _json (res) {
-    if (res.status === 401) {
-      throw new AuthError()
-    }
-
-    if (res.status < 200 || res.status > 300) {
-      let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`
-      return res.json()
-        .then((errData) => {
-          errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg
-          errMsg = errData.requestId ? `${errMsg} request-Id: ${errData.requestId}` : errMsg
-          throw new Error(errMsg)
-        }).catch(() => { throw new Error(errMsg) })
+  #errorHandler (method, path) {
+    return (err) => {
+      if (!err?.isAuthError) {
+        const error = new Error(`Could not ${method} ${this.#getUrl(path)}`)
+        error.cause = err
+        err = error // eslint-disable-line no-param-reassign
+      }
+      return Promise.reject(err)
     }
-    return res.json()
   }
 
   preflight (path) {
@@ -94,7 +100,7 @@ module.exports = class RequestClient {
       return Promise.resolve(this.allowedHeaders.slice())
     }
 
-    return fetch(this._getUrl(path), {
+    return fetch(this.#getUrl(path), {
       method: 'OPTIONS',
     })
       .then((response) => {
@@ -117,9 +123,9 @@ module.exports = class RequestClient {
       .then(([allowedHeaders, headers]) => {
         // filter to keep only allowed Headers
         Object.keys(headers).forEach((header) => {
-          if (allowedHeaders.indexOf(header.toLowerCase()) === -1) {
-            this.uppy.log(`[CompanionClient] excluding unallowed header ${header}`)
-            delete headers[header]
+          if (!allowedHeaders.includes(header.toLowerCase())) {
+            this.uppy.log(`[CompanionClient] excluding disallowed header ${header}`)
+            delete headers[header] // eslint-disable-line no-param-reassign
           }
         })
 
@@ -128,55 +134,43 @@ module.exports = class RequestClient {
   }
 
   get (path, skipPostResponse) {
+    const method = 'get'
     return this.preflightAndHeaders(path)
-      .then((headers) => fetchWithNetworkError(this._getUrl(path), {
-        method: 'get',
+      .then((headers) => fetchWithNetworkError(this.#getUrl(path), {
+        method,
         headers,
         credentials: this.opts.companionCookiesRule || 'same-origin',
       }))
-      .then(this._getPostResponseFunc(skipPostResponse))
-      .then((res) => this._json(res))
-      .catch((err) => {
-        if (!err.isAuthError) {
-          err.message = `Could not get ${this._getUrl(path)}. ${err.message}`
-        }
-        return Promise.reject(err)
-      })
+      .then(this.#getPostResponseFunc(skipPostResponse))
+      .then(handleJSONResponse)
+      .catch(this.#errorHandler(method, path))
   }
 
   post (path, data, skipPostResponse) {
+    const method = 'post'
     return this.preflightAndHeaders(path)
-      .then((headers) => fetchWithNetworkError(this._getUrl(path), {
-        method: 'post',
+      .then((headers) => fetchWithNetworkError(this.#getUrl(path), {
+        method,
         headers,
         credentials: this.opts.companionCookiesRule || 'same-origin',
         body: JSON.stringify(data),
       }))
-      .then(this._getPostResponseFunc(skipPostResponse))
-      .then((res) => this._json(res))
-      .catch((err) => {
-        if (!err.isAuthError) {
-          err.message = `Could not post ${this._getUrl(path)}. ${err.message}`
-        }
-        return Promise.reject(err)
-      })
+      .then(this.#getPostResponseFunc(skipPostResponse))
+      .then(handleJSONResponse)
+      .catch(this.#errorHandler(method, path))
   }
 
   delete (path, data, skipPostResponse) {
+    const method = 'delete'
     return this.preflightAndHeaders(path)
       .then((headers) => fetchWithNetworkError(`${this.hostname}/${path}`, {
-        method: 'delete',
+        method,
         headers,
         credentials: this.opts.companionCookiesRule || 'same-origin',
         body: data ? JSON.stringify(data) : null,
       }))
-      .then(this._getPostResponseFunc(skipPostResponse))
-      .then((res) => this._json(res))
-      .catch((err) => {
-        if (!err.isAuthError) {
-          err.message = `Could not delete ${this._getUrl(path)}. ${err.message}`
-        }
-        return Promise.reject(err)
-      })
+      .then(this.#getPostResponseFunc(skipPostResponse))
+      .then(handleJSONResponse)
+      .catch(this.#errorHandler(method, path))
   }
 }

+ 32 - 31
packages/@uppy/companion-client/src/Socket.js

@@ -1,83 +1,84 @@
 const ee = require('namespace-emitter')
 
 module.exports = class UppySocket {
-  constructor (opts) {
-    this.opts = opts
-    this._queued = []
-    this.isOpen = false
-    this.emitter = ee()
+  #queued = []
+
+  #emitter = ee()
 
-    this._handleMessage = this._handleMessage.bind(this)
+  #isOpen = false
 
-    this.close = this.close.bind(this)
-    this.emit = this.emit.bind(this)
-    this.on = this.on.bind(this)
-    this.once = this.once.bind(this)
-    this.send = this.send.bind(this)
+  #socket
+
+  constructor (opts) {
+    this.opts = opts
 
     if (!opts || opts.autoOpen !== false) {
       this.open()
     }
   }
 
+  get isOpen () { return this.#isOpen }
+
+  [Symbol.for('uppy test: getSocket')] () { return this.#socket }
+
+  [Symbol.for('uppy test: getQueued')] () { return this.#queued }
+
   open () {
-    this.socket = new WebSocket(this.opts.target)
+    this.#socket = new WebSocket(this.opts.target)
 
-    this.socket.onopen = (e) => {
-      this.isOpen = true
+    this.#socket.onopen = () => {
+      this.#isOpen = true
 
-      while (this._queued.length > 0 && this.isOpen) {
-        const first = this._queued[0]
+      while (this.#queued.length > 0 && this.#isOpen) {
+        const first = this.#queued.shift()
         this.send(first.action, first.payload)
-        this._queued = this._queued.slice(1)
       }
     }
 
-    this.socket.onclose = (e) => {
-      this.isOpen = false
+    this.#socket.onclose = () => {
+      this.#isOpen = false
     }
 
-    this.socket.onmessage = this._handleMessage
+    this.#socket.onmessage = this.#handleMessage
   }
 
   close () {
-    if (this.socket) {
-      this.socket.close()
-    }
+    this.#socket?.close()
   }
 
   send (action, payload) {
     // attach uuid
 
-    if (!this.isOpen) {
-      this._queued.push({ action, payload })
+    if (!this.#isOpen) {
+      this.#queued.push({ action, payload })
       return
     }
 
-    this.socket.send(JSON.stringify({
+    this.#socket.send(JSON.stringify({
       action,
       payload,
     }))
   }
 
   on (action, handler) {
-    this.emitter.on(action, handler)
+    this.#emitter.on(action, handler)
   }
 
   emit (action, payload) {
-    this.emitter.emit(action, payload)
+    this.#emitter.emit(action, payload)
   }
 
   once (action, handler) {
-    this.emitter.once(action, handler)
+    this.#emitter.once(action, handler)
   }
 
-  _handleMessage (e) {
+  #handleMessage= (e) => {
     try {
       const message = JSON.parse(e.data)
       this.emit(message.action, message.payload)
     } catch (err) {
-      console.log(err)
+      // TODO: use a more robust error handler.
+      console.log(err) // eslint-disable-line no-console
     }
   }
 }

+ 12 - 10
packages/@uppy/companion-client/src/Socket.test.js

@@ -15,10 +15,12 @@ describe('Socket', () => {
         webSocketConstructorSpy(target)
       }
 
+      // eslint-disable-next-line class-methods-use-this
       close (args) {
         webSocketCloseSpy(args)
       }
 
+      // eslint-disable-next-line class-methods-use-this
       send (json) {
         webSocketSendSpy(json)
       }
@@ -52,7 +54,7 @@ describe('Socket', () => {
 
   it('should send a message via the websocket if the connection is open', () => {
     const uppySocket = new UppySocket({ target: 'foo' })
-    const webSocketInstance = uppySocket.socket
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
     webSocketInstance.triggerOpen()
 
     uppySocket.send('bar', 'boo')
@@ -66,17 +68,17 @@ describe('Socket', () => {
     const uppySocket = new UppySocket({ target: 'foo' })
 
     uppySocket.send('bar', 'boo')
-    expect(uppySocket._queued).toEqual([{ action: 'bar', payload: 'boo' }])
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
     expect(webSocketSendSpy.mock.calls.length).toEqual(0)
   })
 
   it('should queue any messages for the websocket if the connection is not open, then send them when the connection is open', () => {
     const uppySocket = new UppySocket({ target: 'foo' })
-    const webSocketInstance = uppySocket.socket
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
 
     uppySocket.send('bar', 'boo')
     uppySocket.send('moo', 'baa')
-    expect(uppySocket._queued).toEqual([
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([
       { action: 'bar', payload: 'boo' },
       { action: 'moo', payload: 'baa' },
     ])
@@ -84,7 +86,7 @@ describe('Socket', () => {
 
     webSocketInstance.triggerOpen()
 
-    expect(uppySocket._queued).toEqual([])
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
     expect(webSocketSendSpy.mock.calls.length).toEqual(2)
     expect(webSocketSendSpy.mock.calls[0]).toEqual([
       JSON.stringify({ action: 'bar', payload: 'boo' }),
@@ -96,19 +98,19 @@ describe('Socket', () => {
 
   it('should start queuing any messages when the websocket connection is closed', () => {
     const uppySocket = new UppySocket({ target: 'foo' })
-    const webSocketInstance = uppySocket.socket
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
     webSocketInstance.triggerOpen()
     uppySocket.send('bar', 'boo')
-    expect(uppySocket._queued).toEqual([])
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
 
     webSocketInstance.triggerClose()
     uppySocket.send('bar', 'boo')
-    expect(uppySocket._queued).toEqual([{ action: 'bar', payload: 'boo' }])
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
   })
 
   it('should close the websocket when it is force closed', () => {
     const uppySocket = new UppySocket({ target: 'foo' })
-    const webSocketInstance = uppySocket.socket
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
     webSocketInstance.triggerOpen()
 
     uppySocket.close()
@@ -117,7 +119,7 @@ describe('Socket', () => {
 
   it('should be able to subscribe to messages received on the websocket', () => {
     const uppySocket = new UppySocket({ target: 'foo' })
-    const webSocketInstance = uppySocket.socket
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
 
     const emitterListenerMock = jest.fn()
     uppySocket.on('hi', emitterListenerMock)

+ 1 - 1
packages/@uppy/companion-client/types/index.d.ts

@@ -65,7 +65,7 @@ export interface SocketOptions {
 }
 
 export class Socket {
-  isOpen: boolean
+  readonly isOpen: boolean
 
   constructor (opts: SocketOptions)
 

+ 11 - 0
packages/@uppy/core/src/BasePlugin.js

@@ -6,6 +6,9 @@
  *
  * See `Plugin` for the extended version with Preact rendering for interfaces.
  */
+
+const Translator = require('@uppy/utils/lib/Translator')
+
 module.exports = class BasePlugin {
   constructor (uppy, opts = {}) {
     this.uppy = uppy
@@ -34,6 +37,14 @@ module.exports = class BasePlugin {
   setOptions (newOpts) {
     this.opts = { ...this.opts, ...newOpts }
     this.setPluginState() // so that UI re-renders with new options
+    this.i18nInit()
+  }
+
+  i18nInit () {
+    const translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
+    this.i18n = translator.translate.bind(translator)
+    this.i18nArray = translator.translateArray.bind(translator)
+    this.setPluginState() // so that UI re-renders and we see the updated locale
   }
 
   /**

+ 28 - 27
packages/@uppy/core/src/index.js

@@ -54,6 +54,14 @@ class Uppy {
 
   #emitter = ee()
 
+  #translator
+
+  #preProcessors = new Set()
+
+  #uploaders = new Set()
+
+  #postProcessors = new Set()
+
   /**
    * Instantiate Uppy
    *
@@ -173,10 +181,6 @@ class Uppy {
     //    [Practical Check] Firefox, try to upload a big file for a prolonged period of time. Laptop will start to heat up.
     this.calculateProgress = throttle(this.calculateProgress.bind(this), 500, { leading: true, trailing: true })
 
-    this.preProcessors = []
-    this.uploaders = []
-    this.postProcessors = []
-
     this.store = this.opts.store
     this.setState({
       plugins: {},
@@ -279,10 +283,16 @@ class Uppy {
   }
 
   i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.opts.locale])
-    this.locale = this.translator.locale
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.#translator = new Translator([this.defaultLocale, this.opts.locale])
+    this.locale = this.#translator.locale
+  }
+
+  i18n (...args) {
+    return this.#translator.translate(...args)
+  }
+
+  i18nArray (...args) {
+    return this.#translator.translateArray(...args)
   }
 
   setOptions (newOpts) {
@@ -335,36 +345,27 @@ class Uppy {
   }
 
   addPreProcessor (fn) {
-    this.preProcessors.push(fn)
+    this.#preProcessors.add(fn)
   }
 
   removePreProcessor (fn) {
-    const i = this.preProcessors.indexOf(fn)
-    if (i !== -1) {
-      this.preProcessors.splice(i, 1)
-    }
+    return this.#preProcessors.delete(fn)
   }
 
   addPostProcessor (fn) {
-    this.postProcessors.push(fn)
+    this.#postProcessors.add(fn)
   }
 
   removePostProcessor (fn) {
-    const i = this.postProcessors.indexOf(fn)
-    if (i !== -1) {
-      this.postProcessors.splice(i, 1)
-    }
+    return this.#postProcessors.delete(fn)
   }
 
   addUploader (fn) {
-    this.uploaders.push(fn)
+    this.#uploaders.add(fn)
   }
 
   removeUploader (fn) {
-    const i = this.uploaders.indexOf(fn)
-    if (i !== -1) {
-      this.uploaders.splice(i, 1)
-    }
+    return this.#uploaders.delete(fn)
   }
 
   setMeta (data) {
@@ -1199,7 +1200,7 @@ class Uppy {
       this.setFileState(file.id, {
         progress: {
           ...currentProgress,
-          postprocess: this.postProcessors.length > 0 ? {
+          postprocess: this.#postProcessors.size > 0 ? {
             mode: 'indeterminate',
           } : null,
           uploadComplete: true,
@@ -1592,9 +1593,9 @@ class Uppy {
     const restoreStep = uploadData.step
 
     const steps = [
-      ...this.preProcessors,
-      ...this.uploaders,
-      ...this.postProcessors,
+      ...this.#preProcessors,
+      ...this.#uploaders,
+      ...this.#postProcessors,
     ]
     let lastStep = Promise.resolve()
     steps.forEach((fn, step) => {

+ 12 - 45
packages/@uppy/core/src/index.test.js

@@ -327,24 +327,13 @@ describe('src/Core', () => {
   })
 
   describe('preprocessors', () => {
-    it('should add a preprocessor', () => {
+    it('should add and remove preprocessor', () => {
       const core = new Core()
       const preprocessor = () => { }
+      expect(core.removePreProcessor(preprocessor)).toBe(false)
       core.addPreProcessor(preprocessor)
-      expect(core.preProcessors[0]).toEqual(preprocessor)
-    })
-
-    it('should remove a preprocessor', () => {
-      const core = new Core()
-      const preprocessor1 = () => { }
-      const preprocessor2 = () => { }
-      const preprocessor3 = () => { }
-      core.addPreProcessor(preprocessor1)
-      core.addPreProcessor(preprocessor2)
-      core.addPreProcessor(preprocessor3)
-      expect(core.preProcessors.length).toEqual(3)
-      core.removePreProcessor(preprocessor2)
-      expect(core.preProcessors.length).toEqual(2)
+      expect(core.removePreProcessor(preprocessor)).toBe(true)
+      expect(core.removePreProcessor(preprocessor)).toBe(false)
     })
 
     it('should execute all the preprocessors when uploading a file', () => {
@@ -456,24 +445,13 @@ describe('src/Core', () => {
   })
 
   describe('postprocessors', () => {
-    it('should add a postprocessor', () => {
+    it('should add and remove postprocessor', () => {
       const core = new Core()
       const postprocessor = () => { }
+      expect(core.removePostProcessor(postprocessor)).toBe(false)
       core.addPostProcessor(postprocessor)
-      expect(core.postProcessors[0]).toEqual(postprocessor)
-    })
-
-    it('should remove a postprocessor', () => {
-      const core = new Core()
-      const postprocessor1 = () => { }
-      const postprocessor2 = () => { }
-      const postprocessor3 = () => { }
-      core.addPostProcessor(postprocessor1)
-      core.addPostProcessor(postprocessor2)
-      core.addPostProcessor(postprocessor3)
-      expect(core.postProcessors.length).toEqual(3)
-      core.removePostProcessor(postprocessor2)
-      expect(core.postProcessors.length).toEqual(2)
+      expect(core.removePostProcessor(postprocessor)).toBe(true)
+      expect(core.removePostProcessor(postprocessor)).toBe(false)
     })
 
     it('should execute all the postprocessors when uploading a file', () => {
@@ -586,24 +564,13 @@ describe('src/Core', () => {
   })
 
   describe('uploaders', () => {
-    it('should add an uploader', () => {
+    it('should add and remove uploader', () => {
       const core = new Core()
       const uploader = () => { }
+      expect(core.removeUploader(uploader)).toBe(false)
       core.addUploader(uploader)
-      expect(core.uploaders[0]).toEqual(uploader)
-    })
-
-    it('should remove an uploader', () => {
-      const core = new Core()
-      const uploader1 = () => { }
-      const uploader2 = () => { }
-      const uploader3 = () => { }
-      core.addUploader(uploader1)
-      core.addUploader(uploader2)
-      core.addUploader(uploader3)
-      expect(core.uploaders.length).toEqual(3)
-      core.removeUploader(uploader2)
-      expect(core.uploaders.length).toEqual(2)
+      expect(core.removeUploader(uploader)).toBe(true)
+      expect(core.removeUploader(uploader)).toBe(false)
     })
   })
 

+ 0 - 12
packages/@uppy/dashboard/src/index.js

@@ -165,18 +165,6 @@ module.exports = class Dashboard extends UIPlugin {
     this.removeDragOverClassTimeout = null
   }
 
-  setOptions = (newOpts) => {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit = () => {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   removeTarget = (plugin) => {
     const pluginState = this.getPluginState()
     // filter out the one we want to remove

+ 2 - 14
packages/@uppy/drag-drop/src/index.js

@@ -38,12 +38,12 @@ module.exports = class DragDrop extends UIPlugin {
     // Merge default options with the ones set by user
     this.opts = { ...defaultOpts, ...opts }
 
+    this.i18nInit()
+
     // Check for browser dragDrop support
     this.isDragDropSupported = isDragDropSupported()
     this.removeDragOverClassTimeout = null
 
-    this.i18nInit()
-
     // Bind `this` to class methods
     this.onInputChange = this.onInputChange.bind(this)
     this.handleDragOver = this.handleDragOver.bind(this)
@@ -53,18 +53,6 @@ module.exports = class DragDrop extends UIPlugin {
     this.render = this.render.bind(this)
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   addFiles (files) {
     const descriptors = files.map((file) => ({
       source: this.id,

+ 8 - 0
packages/@uppy/dropbox/src/index.js

@@ -29,6 +29,14 @@ module.exports = class Dropbox extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameDropbox: 'Dropbox',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameDropbox')
+
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)
   }

+ 8 - 0
packages/@uppy/facebook/src/index.js

@@ -29,6 +29,14 @@ module.exports = class Facebook extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameFacebook: 'Facebook',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameFacebook')
+
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)
   }

+ 0 - 12
packages/@uppy/file-input/src/index.js

@@ -38,18 +38,6 @@ module.exports = class FileInput extends UIPlugin {
     this.handleClick = this.handleClick.bind(this)
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   addFiles (files) {
     const descriptors = files.map((file) => ({
       source: this.id,

+ 8 - 0
packages/@uppy/google-drive/src/index.js

@@ -30,6 +30,14 @@ module.exports = class GoogleDrive extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameGoogleDrive: 'Google Drive',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameGoogleDrive')
+
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)
   }

+ 73 - 50
packages/@uppy/image-editor/src/Editor.js

@@ -31,14 +31,64 @@ module.exports = class Editor extends Component {
     this.cropper.destroy()
   }
 
+  save = () => {
+    const { opts, save, currentImage } = this.props
+
+    this.cropper.getCroppedCanvas(opts.cropperOptions.croppedCanvasOptions)
+      .toBlob(
+        (blob) => save(blob),
+        currentImage.type,
+        opts.quality
+      )
+  }
+
+  granularRotateOnChange = (ev) => {
+    const { rotationAngle, rotationDelta } = this.state
+    const pendingRotationDelta = Number(ev.target.value) - rotationDelta
+    cancelAnimationFrame(this.granularRotateOnInputNextFrame)
+    if (pendingRotationDelta !== 0) {
+      const pendingRotationAngle = rotationAngle + pendingRotationDelta
+      this.granularRotateOnInputNextFrame = requestAnimationFrame(() => {
+        this.cropper.rotateTo(pendingRotationAngle)
+      })
+    }
+  }
+
+  renderGranularRotate () {
+    const { i18n } = this.props
+    const { rotationDelta, rotationAngle } = this.state
+
+    return (
+      // eslint-disable-next-line jsx-a11y/label-has-associated-control
+      <label
+        data-microtip-position="top"
+        role="tooltip"
+        aria-label={`${rotationAngle}º`}
+        className="uppy-ImageCropper-rangeWrapper uppy-u-reset"
+      >
+        <input
+          className="uppy-ImageCropper-range uppy-u-reset"
+          type="range"
+          onInput={this.granularRotateOnChange}
+          onChange={this.granularRotateOnChange}
+          value={rotationDelta}
+          min="-45"
+          max="44"
+          aria-label={i18n('rotate')}
+        />
+      </label>
+    )
+  }
+
   renderRevert () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('revert')}
+        aria-label={i18n('revert')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => {
           this.cropper.reset()
           this.cropper.setAspectRatio(0)
@@ -53,14 +103,15 @@ module.exports = class Editor extends Component {
   }
 
   renderRotate () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
         onClick={() => this.cropper.rotate(-90)}
-        aria-label={this.props.i18n('rotate')}
+        aria-label={i18n('rotate')}
         data-microtip-position="top"
-        role="tooltip"
       >
         <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
           <path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none" />
@@ -70,48 +121,15 @@ module.exports = class Editor extends Component {
     )
   }
 
-  granularRotateOnChange = (ev) => {
-    const { rotationAngle, rotationDelta } = this.state
-    const pendingRotationDelta = Number(ev.target.value) - rotationDelta
-    cancelAnimationFrame(this.granularRotateOnInputNextFrame)
-    if (pendingRotationDelta !== 0) {
-      const pendingRotationAngle = rotationAngle + pendingRotationDelta
-      this.granularRotateOnInputNextFrame = requestAnimationFrame(() => {
-        this.cropper.rotateTo(pendingRotationAngle)
-      })
-    }
-  }
-
-  renderGranularRotate () {
-    return (
-      <label
-        data-microtip-position="top"
-        role="tooltip"
-        aria-label={`${this.state.rotationAngle}º`}
-        className="uppy-ImageCropper-rangeWrapper uppy-u-reset"
-      >
-        <input
-          className="uppy-ImageCropper-range uppy-u-reset"
-          type="range"
-          onInput={this.granularRotateOnChange}
-          onChange={this.granularRotateOnChange}
-          value={this.state.rotationDelta}
-          min="-45"
-          max="44"
-          aria-label={this.props.i18n('rotate')}
-        />
-      </label>
-    )
-  }
-
   renderFlip () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('flipHorizontal')}
+        aria-label={i18n('flipHorizontal')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => this.cropper.scaleX(-this.cropper.getData().scaleX || -1)}
       >
         <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
@@ -123,13 +141,14 @@ module.exports = class Editor extends Component {
   }
 
   renderZoomIn () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('zoomIn')}
+        aria-label={i18n('zoomIn')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => this.cropper.zoom(0.1)}
       >
         <svg aria-hidden="true" className="uppy-c-icon" height="24" viewBox="0 0 24 24" width="24">
@@ -142,13 +161,14 @@ module.exports = class Editor extends Component {
   }
 
   renderZoomOut () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('zoomOut')}
+        aria-label={i18n('zoomOut')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => this.cropper.zoom(-0.1)}
       >
         <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
@@ -160,13 +180,14 @@ module.exports = class Editor extends Component {
   }
 
   renderCropSquare () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('aspectRatioSquare')}
+        aria-label={i18n('aspectRatioSquare')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => this.cropper.setAspectRatio(1)}
       >
         <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
@@ -178,13 +199,14 @@ module.exports = class Editor extends Component {
   }
 
   renderCropWidescreen () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('aspectRatioLandscape')}
+        aria-label={i18n('aspectRatioLandscape')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => this.cropper.setAspectRatio(16 / 9)}
       >
         <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
@@ -196,13 +218,14 @@ module.exports = class Editor extends Component {
   }
 
   renderCropWidescreenVertical () {
+    const { i18n } = this.props
+
     return (
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        aria-label={this.props.i18n('aspectRatioPortrait')}
+        aria-label={i18n('aspectRatioPortrait')}
         data-microtip-position="top"
-        role="tooltip"
         onClick={() => this.cropper.setAspectRatio(9 / 16)}
       >
         <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">

+ 5 - 13
packages/@uppy/image-editor/src/index.js

@@ -4,6 +4,7 @@ const { h } = require('preact')
 const Editor = require('./Editor')
 
 module.exports = class ImageEditor extends UIPlugin {
+  // eslint-disable-next-line global-require
   static VERSION = require('../package.json').version
 
   constructor (uppy, opts) {
@@ -30,6 +31,7 @@ module.exports = class ImageEditor extends UIPlugin {
       background: false,
       autoCropArea: 1,
       responsive: true,
+      croppedCanvasOptions: {},
     }
 
     const defaultActions = {
@@ -64,18 +66,7 @@ module.exports = class ImageEditor extends UIPlugin {
     this.i18nInit()
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    // this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
+  // eslint-disable-next-line class-methods-use-this
   canEditFile (file) {
     if (!file.type || file.isRemote) {
       return false
@@ -145,8 +136,9 @@ module.exports = class ImageEditor extends UIPlugin {
 
   render () {
     const { currentImage } = this.getPluginState()
+
     if (currentImage === null || currentImage.isRemote) {
-      return
+      return null
     }
 
     return (

+ 6 - 1
packages/@uppy/image-editor/types/index.d.ts

@@ -1,4 +1,5 @@
 import type { PluginOptions, UIPlugin, PluginTarget } from '@uppy/core'
+import type Cropper from 'cropperjs'
 import ImageEditorLocale from './generatedLocale'
 
 type Actions = {
@@ -13,8 +14,12 @@ type Actions = {
   cropWidescreenVertical: boolean
 }
 
+interface UppyCropperOptions extends Cropper.Options {
+  croppedCanvasOptions: Cropper.GetCroppedCanvasOptions
+}
+
 export interface ImageEditorOptions extends PluginOptions {
-  cropperOptions?: Record<string, unknown>
+  cropperOptions?: UppyCropperOptions
   actions?: Actions
   quality?: number
   target?: PluginTarget

+ 8 - 1
packages/@uppy/instagram/src/index.js

@@ -10,7 +10,6 @@ module.exports = class Instagram extends UIPlugin {
     super(uppy, opts)
     this.id = this.opts.id || 'Instagram'
     Provider.initPlugin(this, opts)
-    this.title = this.opts.title || 'Instagram'
     this.icon = () => (
       <svg aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
         <g fill="none" fillRule="evenodd">
@@ -20,6 +19,14 @@ module.exports = class Instagram extends UIPlugin {
       </svg>
     )
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameInstagram: 'Instagram',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameInstagram')
+
     this.provider = new Provider(uppy, {
       companionUrl: this.opts.companionUrl,
       companionHeaders: this.opts.companionHeaders,

+ 8 - 0
packages/@uppy/locales/src/en_US.js

@@ -91,6 +91,14 @@ en_US.strings = {
   pause: 'Pause',
   paused: 'Paused',
   pauseUpload: 'Pause upload',
+  pluginNameBox: 'Box',
+  pluginNameCamera: 'Camera',
+  pluginNameDropbox: 'Dropbox',
+  pluginNameFacebook: 'Facebook',
+  pluginNameGoogleDrive: 'Google Drive',
+  pluginNameInstagram: 'Instagram',
+  pluginNameOneDrive: 'OneDrive',
+  pluginNameZoom: 'Zoom',
   poweredBy: 'Powered by %{uppy}',
   processingXFiles: {
     '0': 'Processing %{smart_count} file',

+ 8 - 0
packages/@uppy/onedrive/src/index.js

@@ -31,6 +31,14 @@ module.exports = class OneDrive extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameOneDrive: 'OneDrive',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameOneDrive')
+
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)
   }

+ 1 - 3
packages/@uppy/screen-capture/src/index.js

@@ -65,9 +65,7 @@ module.exports = class ScreenCapture extends UIPlugin {
     this.opts = { ...defaultOptions, ...opts }
 
     // i18n
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
+    this.i18nInit()
 
     // uppy plugin class related
     this.install = this.install.bind(this)

+ 0 - 11
packages/@uppy/status-bar/src/index.js

@@ -73,17 +73,6 @@ module.exports = class StatusBar extends UIPlugin {
     this.install = this.install.bind(this)
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   getTotalSpeed (files) {
     let totalSpeed = 0
     files.forEach((file) => {

+ 0 - 13
packages/@uppy/thumbnail-generator/src/index.js

@@ -40,19 +40,6 @@ module.exports = class ThumbnailGenerator extends UIPlugin {
     if (this.opts.lazy && this.opts.waitForThumbnailsBeforeUpload) {
       throw new Error('ThumbnailGenerator: The `lazy` and `waitForThumbnailsBeforeUpload` options are mutually exclusive. Please ensure at most one of them is set to `true`.')
     }
-
-    this.i18nInit()
-  }
-
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
   }
 
   /**

+ 0 - 12
packages/@uppy/transloadit/src/index.js

@@ -82,18 +82,6 @@ module.exports = class Transloadit extends BasePlugin {
     this.completedFiles = Object.create(null)
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   #getClientVersion () {
     const list = [
       `uppy-core:${this.uppy.constructor.VERSION}`,

+ 0 - 12
packages/@uppy/url/src/index.js

@@ -65,18 +65,6 @@ module.exports = class Url extends UIPlugin {
     })
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   getFileNameFromUrl (url) {
     return url.substring(url.lastIndexOf('/') + 1)
   }

+ 2 - 12
packages/@uppy/webcam/src/index.js

@@ -61,7 +61,6 @@ module.exports = class Webcam extends UIPlugin {
     // eslint-disable-next-line no-restricted-globals
     this.protocol = location.protocol.match(/https/i) ? 'https' : 'http'
     this.id = this.opts.id || 'Webcam'
-    this.title = this.opts.title || 'Camera'
     this.type = 'acquirer'
     this.capturedMediaFile = null
     this.icon = () => (
@@ -75,6 +74,7 @@ module.exports = class Webcam extends UIPlugin {
 
     this.defaultLocale = {
       strings: {
+        pluginNameCamera: 'Camera',
         smile: 'Smile!',
         takePicture: 'Take a picture',
         startRecording: 'Begin video recording',
@@ -109,12 +109,11 @@ module.exports = class Webcam extends UIPlugin {
     }
 
     this.opts = { ...defaultOptions, ...opts }
-
     this.i18nInit()
+    this.title = this.i18n('pluginNameCamera')
 
     this.install = this.install.bind(this)
     this.setPluginState = this.setPluginState.bind(this)
-
     this.render = this.render.bind(this)
 
     // Camera controls
@@ -154,15 +153,6 @@ module.exports = class Webcam extends UIPlugin {
         ...newOpts?.videoConstraints,
       },
     })
-
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.i18nArray = this.translator.translateArray.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
   }
 
   hasCameraCheck () {

+ 0 - 13
packages/@uppy/xhr-upload/src/index.js

@@ -119,8 +119,6 @@ module.exports = class XHRUpload extends BasePlugin {
 
     this.opts = { ...defaultOptions, ...opts }
 
-    this.i18nInit()
-
     this.handleUpload = this.handleUpload.bind(this)
 
     // Simultaneous upload limiting is shared across all uploads with this plugin.
@@ -137,17 +135,6 @@ module.exports = class XHRUpload extends BasePlugin {
     this.uploaderEvents = Object.create(null)
   }
 
-  setOptions (newOpts) {
-    super.setOptions(newOpts)
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    this.translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale])
-    this.i18n = this.translator.translate.bind(this.translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
   getOptions (file) {
     const overrides = this.uppy.getState().xhrUpload
     const { headers } = this.opts

+ 8 - 0
packages/@uppy/zoom/src/index.js

@@ -30,6 +30,14 @@ module.exports = class Zoom extends UIPlugin {
       pluginId: this.id,
     })
 
+    this.defaultLocale = {
+      strings: {
+        pluginNameZoom: 'Zoom',
+      },
+    }
+    this.i18nInit()
+    this.title = this.i18n('pluginNameZoom')
+
     this.onFirstRender = this.onFirstRender.bind(this)
     this.render = this.render.bind(this)
   }

+ 5 - 5
test/endtoend/chaos-monkey/test.js

@@ -47,11 +47,11 @@ describe('Chaos monkey', function () {
       return browser.execute(() => {
         window.addLogMessage('Cancelling a file')
         // prefer deleting a file that is uploading right now
-        var selector = Math.random() <= 0.7
+        const selector = Math.random() <= 0.7
           ? '.is-inprogress .uppy-Dashboard-Item-action--remove'
           : '.uppy-Dashboard-Item-action--remove'
-        var buttons = document.querySelectorAll(selector)
-        var del = buttons[Math.floor(Math.random() * buttons.length)]
+        const buttons = document.querySelectorAll(selector)
+        const del = buttons[Math.floor(Math.random() * buttons.length)]
         if (del) del.click()
       })
     }
@@ -59,7 +59,7 @@ describe('Chaos monkey', function () {
     function startUploadIfAnyWaitingFiles () {
       return browser.execute(() => {
         window.addLogMessage('Starting upload')
-        var start = document.querySelector('.uppy-StatusBar-actionBtn--upload')
+        const start = document.querySelector('.uppy-StatusBar-actionBtn--upload')
         if (start) start.click()
       })
     }
@@ -67,7 +67,7 @@ describe('Chaos monkey', function () {
     function cancelAll () {
       return browser.execute(() => {
         window.addLogMessage('Cancelling everything')
-        var button = document.querySelector('.uppy-DashboardContent-back')
+        const button = document.querySelector('.uppy-DashboardContent-back')
         if (button) button.click()
       })
     }

+ 3 - 3
test/endtoend/transloadit/main.js

@@ -3,7 +3,7 @@ const Dashboard = require('@uppy/dashboard')
 const Transloadit = require('@uppy/transloadit')
 
 function initUppyTransloadit (transloaditKey) {
-  var uppyTransloadit = new Uppy({
+  const uppyTransloadit = new Uppy({
     id: 'uppyTransloadit',
     debug: true,
     autoProceed: true,
@@ -36,9 +36,9 @@ function initUppyTransloadit (transloaditKey) {
     console.log('Result here ====>', stepName, result)
     console.log('Cropped image url is here ====>', result.url)
 
-    var img = new Image()
+    const img = new Image()
     img.onload = function () {
-      var result = document.createElement('div')
+      const result = document.createElement('div')
       result.setAttribute('id', 'uppy-result')
       result.textContent = 'ok'
       document.body.appendChild(result)

+ 14 - 14
test/endtoend/utils.js

@@ -13,18 +13,18 @@ function selectFakeFile (uppyID, name, type, b64) {
   // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
   function base64toBlob (base64Data, contentType) {
     contentType = contentType || ''
-    var sliceSize = 1024
-    var byteCharacters = atob(base64Data)
-    var bytesLength = byteCharacters.length
-    var slicesCount = Math.ceil(bytesLength / sliceSize)
-    var byteArrays = new Array(slicesCount)
-
-    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
-      var begin = sliceIndex * sliceSize
-      var end = Math.min(begin + sliceSize, bytesLength)
-
-      var bytes = new Array(end - begin)
-      for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
+    const sliceSize = 1024
+    const byteCharacters = atob(base64Data)
+    const bytesLength = byteCharacters.length
+    const slicesCount = Math.ceil(bytesLength / sliceSize)
+    const byteArrays = new Array(slicesCount)
+
+    for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
+      const begin = sliceIndex * sliceSize
+      const end = Math.min(begin + sliceSize, bytesLength)
+
+      const bytes = new Array(end - begin)
+      for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
         bytes[i] = byteCharacters[offset].charCodeAt(0)
       }
       byteArrays[sliceIndex] = new Uint8Array(bytes)
@@ -32,7 +32,7 @@ function selectFakeFile (uppyID, name, type, b64) {
     return new Blob(byteArrays, { type: contentType })
   }
 
-  var blob = base64toBlob(b64, type)
+  const blob = base64toBlob(b64, type)
 
   window[uppyID].addFile({
     source: 'test',
@@ -43,7 +43,7 @@ function selectFakeFile (uppyID, name, type, b64) {
 }
 
 function ensureInputVisible (selector) {
-  var input = document.querySelector(selector)
+  const input = document.querySelector(selector)
   input.style = 'width: auto; height: auto; opacity: 1; z-index: 199'
   input.removeAttribute('hidden')
   input.removeAttribute('aria-hidden')

+ 1 - 1
test/endtoend/wdio.base.conf.js

@@ -128,7 +128,7 @@ exports.config = {
    * @param {Array<string>} specs List of spec file paths that are to be run
    */
   before (capabilities, specs) {
-    var chai = require('chai')
+    const chai = require('chai')
     global.expect = chai.expect
     global.capabilities = capabilities
     chai.Should()

+ 1 - 1
test/resources/DeepFrozenStore.js

@@ -1,4 +1,4 @@
-var deepFreeze = require('deep-freeze')
+const deepFreeze = require('deep-freeze')
 
 /**
  * Default store + deepFreeze on setState to make sure nothing is mutated accidentally

+ 9 - 9
website/private_modules/hexo-renderer-uppyexamples/index.js

@@ -2,13 +2,13 @@
 // We fire our own build-examples.js and tell it which example to build -
 // that script then writes temporary js files
 // which we return via the callback.
-var exec = require('child_process').exec
-var path = require('path')
-var fs = require('fs')
-var uuid = require('uuid')
+const { exec } = require('child_process')
+const path = require('path')
+const fs = require('fs')
+const uuid = require('uuid')
 
-var webRoot = path.dirname(path.dirname(__dirname))
-var browserifyScript = `${webRoot}/build-examples.js`
+const webRoot = path.dirname(path.dirname(__dirname))
+const browserifyScript = `${webRoot}/build-examples.js`
 
 function parseExamplesBrowserify (data, options, callback) {
   if (!data || !data.path) {
@@ -20,9 +20,9 @@ function parseExamplesBrowserify (data, options, callback) {
   }
 
   // var slug    = data.path.replace(/[^a-zA-Z0-9\_\.]/g, '-')
-  var slug = uuid.v4()
-  var tmpFile = `/tmp/${slug}.js`
-  var cmd = `node ${browserifyScript} ${data.path} ${tmpFile} --colors`
+  const slug = uuid.v4()
+  const tmpFile = `/tmp/${slug}.js`
+  const cmd = `node ${browserifyScript} ${data.path} ${tmpFile} --colors`
   // hexo.log.i('hexo-renderer-uppyexamples: change detected in examples. running: ' + cmd);
   exec(cmd, (err, stdout, stderr) => {
     if (err) {

+ 5 - 1
website/src/docs/image-editor.md

@@ -64,6 +64,7 @@ uppy.use(ImageEditor, {
     background: false,
     autoCropArea: 1,
     responsive: true,
+    croppedCanvasOptions: {},
   },
   actions: {
     revert: true,
@@ -89,7 +90,10 @@ Quality of the resulting blob that will be saved in Uppy after editing/cropping.
 
 ### `cropperOptions`
 
-Image Editor is using the excellent [Cropper.js](https://fengyuanchen.github.io/cropperjs/), and if you’d like to fine tune the Cropper.js instance, you can pass options to it.
+Image Editor is using the excellent [Cropper.js](https://fengyuanchen.github.io/cropperjs/).
+`cropperOptions` will be directly passed to `Cropper` and therefor can expect the same values as documented
+in their [README](https://github.com/fengyuanchen/cropperjs/blob/HEAD/README.md#options),
+with the addition of `croppedCanvasOptions`, which will be passed to [`getCroppedCanvas`](https://github.com/fengyuanchen/cropperjs/blob/HEAD/README.md#getcroppedcanvasoptions).
 
 ### `actions`
 

+ 47 - 47
website/themes/uppy/source/js/common.js

@@ -1,9 +1,9 @@
 (function () {
-  var each = [].forEach
-  var doc = document.documentElement
-  var body = document.body
+  let each = [].forEach
+  let doc = document.documentElement
+  let { body } = document
 
-  var isIndex = body.classList.contains('page-index')
+  let isIndex = body.classList.contains('page-index')
 
   // On index page
   if (isIndex) {
@@ -15,29 +15,29 @@
 
   function InnerPage () {
     // var main = document.querySelector('.js-MainContent')
-    var menuButton = document.querySelector('.js-MenuBtn')
-    var header = document.querySelector('.js-MainHeader')
-    var menu = document.querySelector('.js-Sidebar')
-    var content = document.querySelector('.js-Content')
-    var transloaditBar = document.querySelector('.js-TransloaditBar')
+    let menuButton = document.querySelector('.js-MenuBtn')
+    let header = document.querySelector('.js-MainHeader')
+    let menu = document.querySelector('.js-Sidebar')
+    let content = document.querySelector('.js-Content')
+    let transloaditBar = document.querySelector('.js-TransloaditBar')
 
-    var animating = false
-    var allLinks = []
+    let animating = false
+    let allLinks = []
 
     // // listen for scroll event to do positioning & highlights
     // window.addEventListener('scroll', updateSidebar)
     // window.addEventListener('resize', updateSidebar)
 
     function makeSidebarTop () {
-      var headerHeight = header.offsetHeight
-      var transloaditBarHeight = 0
+      let headerHeight = header.offsetHeight
+      let transloaditBarHeight = 0
 
       if (transloaditBar) {
         transloaditBarHeight = transloaditBar.offsetHeight
       }
 
       if (window.matchMedia('(min-width: 1024px)').matches) {
-        var headerTopOffset = header.getBoundingClientRect().top
+        let headerTopOffset = header.getBoundingClientRect().top
         menu.style.top = `${headerHeight + headerTopOffset}px`
       } else {
         menu.style.paddingTop = `${headerHeight + transloaditBarHeight + 20}px`
@@ -50,8 +50,8 @@
     window.addEventListener('resize', makeSidebarTop)
 
     function updateSidebar () {
-      var top = (doc && doc.scrollTop) || body.scrollTop
-      var headerHeight = header.offsetHeight
+      let top = (doc && doc.scrollTop) || body.scrollTop
+      let headerHeight = header.offsetHeight
       if (top > (headerHeight - 25)) {
         // main.classList.add('fix-sidebar')
         header.classList.add('fix-header')
@@ -60,9 +60,9 @@
         header.classList.remove('fix-header')
       }
       if (animating || !allLinks) return
-      var last
-      for (var i = 0; i < allLinks.length; i++) {
-        var link = allLinks[i]
+      let last
+      for (let i = 0; i < allLinks.length; i++) {
+        let link = allLinks[i]
         if (link.offsetTop > top) {
           if (!last) last = link
           break
@@ -76,8 +76,8 @@
     }
 
     function makeLink (h) {
-      var link = document.createElement('li')
-      var text = h.textContent.replace(/\(.*\)$/, '')
+      let link = document.createElement('li')
+      let text = h.textContent.replace(/\(.*\)$/, '')
       // make sure the ids are link-able...
       h.id = h.id
         .replace(/\(.*\)$/, '')
@@ -90,8 +90,8 @@
     }
 
     function collectH3s (h) {
-      var h3s = []
-      var next = h.nextSibling
+      let h3s = []
+      let next = h.nextSibling
       while (next && next.tagName !== 'H2') {
         if (next.tagName === 'H3') {
           h3s.push(next)
@@ -102,7 +102,7 @@
     }
 
     function makeSubLinks (h3s, small) {
-      var container = document.createElement('ul')
+      let container = document.createElement('ul')
       if (small) {
         container.className = 'menu-sub'
       }
@@ -113,8 +113,8 @@
     }
 
     function setActive (id) {
-      var previousActive = menu.querySelector('.section-link.active')
-      var currentActive = typeof id === 'string'
+      let previousActive = menu.querySelector('.section-link.active')
+      let currentActive = typeof id === 'string'
         ? menu.querySelector(`.section-link[href="#${id}"]`)
         : id
       if (currentActive !== previousActive) {
@@ -127,7 +127,7 @@
       if (link.getAttribute('data-scroll') === 'no') {
         return
       }
-      var wrapper = document.createElement('a')
+      let wrapper = document.createElement('a')
       wrapper.href = `#${link.id}`
       wrapper.setAttribute('data-scroll', '')
       link.parentNode.insertBefore(wrapper, link)
@@ -146,13 +146,13 @@
 
     function initSubHeaders () {
       // build sidebar
-      var currentPageAnchor = menu.querySelector('.sidebar-link.current')
-      var isDocs = content.classList.contains('docs')
+      let currentPageAnchor = menu.querySelector('.sidebar-link.current')
+      let isDocs = content.classList.contains('docs')
 
       if (!isDocs) return
 
       if (currentPageAnchor) {
-        var sectionContainer
+        let sectionContainer
 
         // if (false && isAPI) {
         //   sectionContainer = document.querySelector('.menu-root')
@@ -166,12 +166,12 @@
         sectionContainer.className = 'menu-sub'
         currentPageAnchor.parentNode.appendChild(sectionContainer)
 
-        var h2s = content.querySelectorAll('h2')
+        let h2s = content.querySelectorAll('h2')
 
         if (h2s.length) {
           each.call(h2s, (h) => {
             sectionContainer.appendChild(makeLink(h))
-            var h3s = collectH3s(h)
+            let h3s = collectH3s(h)
             allLinks.push(h)
             allLinks.push.apply(allLinks, h3s)
             if (h3s.length) {
@@ -179,7 +179,7 @@
             }
           })
         } else {
-          var h3s = content.querySelectorAll('h3')
+          let h3s = content.querySelectorAll('h3')
           each.call(h3s, (h) => {
             sectionContainer.appendChild(makeLink(h))
             allLinks.push(h)
@@ -215,7 +215,7 @@
       window.addEventListener('resize', updateSidebar)
     }
 
-    var isBlog = menu.classList.contains('is-blog')
+    let isBlog = menu.classList.contains('is-blog')
     if (!isBlog) {
       initSubHeaders()
     }
@@ -224,19 +224,19 @@
   function IndexPage () {
     // Tabs
     window.addEventListener('load', () => {
-      var tabs = document.querySelectorAll('.Tabs-link')
+      let tabs = document.querySelectorAll('.Tabs-link')
 
       function myTabClicks (tabClickEvent) {
         for (var i = 0; i < tabs.length; i++) {
           tabs[i].classList.remove('Tabs-link--active')
         }
 
-        var clickedTab = tabClickEvent.currentTarget
+        let clickedTab = tabClickEvent.currentTarget
         clickedTab.classList.add('Tabs-link--active')
         tabClickEvent.preventDefault()
         tabClickEvent.stopPropagation()
 
-        var myContentPanes = document.querySelectorAll('.TabPane')
+        let myContentPanes = document.querySelectorAll('.TabPane')
 
         for (i = 0; i < myContentPanes.length; i++) {
           myContentPanes[i].classList.remove('TabPane--active')
@@ -244,24 +244,24 @@
 
         // storing reference to event.currentTarget, otherwise we get
         // all the children like SVGs, instead of our target — the link element
-        var anchorReference = tabClickEvent.currentTarget
-        var activePaneId = anchorReference.getAttribute('href')
-        var activePane = document.querySelector(activePaneId)
+        let anchorReference = tabClickEvent.currentTarget
+        let activePaneId = anchorReference.getAttribute('href')
+        let activePane = document.querySelector(activePaneId)
         activePane.classList.add('TabPane--active')
       }
 
-      for (var i = 0; i < tabs.length; i++) {
+      for (let i = 0; i < tabs.length; i++) {
         tabs[i].addEventListener('click', myTabClicks)
       }
     })
 
-    var tagline = document.querySelector('.MainHeader-tagline')
-    var taglinePart = document.querySelector('.MainHeader-taglinePart')
-    var taglineList = document.querySelector('.MainHeader-taglineList')
-    var taglineCounter = taglineList.children.length
+    let tagline = document.querySelector('.MainHeader-tagline')
+    let taglinePart = document.querySelector('.MainHeader-taglinePart')
+    let taglineList = document.querySelector('.MainHeader-taglineList')
+    let taglineCounter = taglineList.children.length
 
     function shuffleTaglines () {
-      for (var i = taglineList.children.length; i >= 0; i--) {
+      for (let i = taglineList.children.length; i >= 0; i--) {
         taglineList.appendChild(taglineList.children[Math.random() * i | 0])
       }
     }
@@ -269,7 +269,7 @@
     function loopTaglines () {
       taglineCounter--
       if (taglineCounter >= 0) {
-        var taglineText = taglineList.children[taglineCounter].textContent
+        let taglineText = taglineList.children[taglineCounter].textContent
         showTagline(taglineText)
         return
       }