uppy/packages/@uppy/file-input/src/index.js
Renée Kooi 1463ee79ce
Improve performance of adding and removing files (#1949)
* core: add an addFiles() method that only updates state once

Previously, adding 1300-ish files took around 260ms, and looked like
this in Firefox's performance tab:

![Flamegraph](https://i.imgur.com/08veuoU.png)

All of the downward peaks are `setState()` calls and GC.

Now it takes around 60ms, and looks like this:

![Flamegraph](https://i.imgur.com/xFdwVBV.png)

Here, most of the time is spent generating file IDs and guessing file
types. Those would be areas to look at next.

* dashboard: prevent frequent state update if nothing changed

After the last commit, `addFiles()` still spends a lot of time in
`emit()` and `hideAllPanels()`. The reason is that `addFiles()` still
emits all the hundreds of file-added events, and the Dashboard responds
to each with `hideAllPanels()`, which does a state update. But this all
happens synchronously, and the state almost certainly did not change
since the last `file-added` event that fired a millisecond ago.

This adds a check to avoid the state update if it is not necessary.

![Flamegraph](https://i.imgur.com/KhuD035.png)

Adding 1300 files takes about 40ms now.

With this change, the `addFiles()` call is no longer the slowest
part—now preact rendering all the items is!

* utils: optimize generateFileID and getFileNameAndExtension

Replaces some clever things with more mundane and faster things!

Now, generateFileID is a bunch of string concatenations.
Now, getFileNameAndExtension uses `lastIndexOf()` instead of a regex.

![Flamegraph](https://i.imgur.com/ZQ1IhxI.png)

Adding 1300 files takes about 25ms.

* dashboard: use preact-virtual-list

* thumbnail-generator: add `lazy` option

* dashboard: request thumbnails once file item renders the first time

* dashboard: fork preact-virtual-list

* core: add removeFiles() to remove files in bulk

* Implement removeFile() in terms of removeFiles()

* thumbnail-generator: only queue files that can be previewed

* rename size constants to accommodate WIDTH/HEIGHT

* Use new uppy.addFiles in DragDrop and FileInput

* utils: fix getFileNameAndExtension() type

* Rip out the lazy thumbnail generation

* Rip out virtualization.

* Remove virtualization leftovers

* tell future people that this is intentionally verbose

* Update package-lock.json

* henlo i am spell

* Make `addFiles()` respect maxNumberOfFiles

* core: show an informer error if some files fail in bulk add

* locales: fix quotes to make build:locale-pack happy

Co-authored-by: Artur Paikin <artur@arturpaikin.com>
2020-01-13 12:29:08 +01:00

134 lines
3.6 KiB
JavaScript

const { Plugin } = require('@uppy/core')
const toArray = require('@uppy/utils/lib/toArray')
const Translator = require('@uppy/utils/lib/Translator')
const { h } = require('preact')
module.exports = class FileInput extends Plugin {
static VERSION = require('../package.json').version
constructor (uppy, opts) {
super(uppy, opts)
this.id = this.opts.id || 'FileInput'
this.title = 'File Input'
this.type = 'acquirer'
this.defaultLocale = {
strings: {
// The same key is used for the same purpose by @uppy/robodog's `form()` API, but our
// locale pack scripts can't access it in Robodog. If it is updated here, it should
// also be updated there!
chooseFiles: 'Choose files'
}
}
// Default options
const defaultOptions = {
target: null,
pretty: true,
inputName: 'files[]'
}
// Merge default options with the ones set by user
this.opts = { ...defaultOptions, ...opts }
this.i18nInit()
this.render = this.render.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
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,
name: file.name,
type: file.type,
data: file
}))
try {
this.uppy.addFiles(descriptors)
} catch (err) {
this.uppy.log(err)
}
}
handleInputChange (event) {
this.uppy.log('[FileInput] Something selected through input...')
const files = toArray(event.target.files)
this.addFiles(files)
// We clear the input after a file is selected, because otherwise
// change event is not fired in Chrome and Safari when a file
// with the same name is selected.
// ___Why not use value="" on <input/> instead?
// Because if we use that method of clearing the input,
// Chrome will not trigger change if we drop the same file twice (Issue #768).
event.target.value = null
}
handleClick (ev) {
this.input.click()
}
render (state) {
/* http://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ */
const hiddenInputStyle = {
width: '0.1px',
height: '0.1px',
opacity: 0,
overflow: 'hidden',
position: 'absolute',
zIndex: -1
}
const restrictions = this.uppy.opts.restrictions
const accept = restrictions.allowedFileTypes ? restrictions.allowedFileTypes.join(',') : null
return (
<div class="uppy-Root uppy-FileInput-container">
<input
class="uppy-FileInput-input"
style={this.opts.pretty && hiddenInputStyle}
type="file"
name={this.opts.inputName}
onchange={this.handleInputChange}
multiple={restrictions.maxNumberOfFiles !== 1}
accept={accept}
ref={(input) => { this.input = input }}
/>
{this.opts.pretty &&
<button
class="uppy-FileInput-btn"
type="button"
onclick={this.handleClick}
>
{this.i18n('chooseFiles')}
</button>}
</div>
)
}
install () {
const target = this.opts.target
if (target) {
this.mount(target, this)
}
}
uninstall () {
this.unmount()
}
}