resources/perf.webkit.org/public/v3/components/instant-file-uploader.js (248 lines of code) (raw):

class InstantFileUploader extends ComponentBase { constructor() { super('instant-file-uploader'); this._fileInput = null; this._allowMultipleFiles = false; this._uploadedFiles = []; this._preuploadFiles = []; this._uploadProgress = new WeakMap; this._fileSizeFormatter = Metric.makeFormatter('B', 3); this._renderUploadedFilesLazily = new LazilyEvaluatedFunction(this._renderUploadedFiles.bind(this)); this._renderPreuploadFilesLazily = new LazilyEvaluatedFunction(this._renderPreuploadFiles.bind(this)); } clearUploads() { this._uploadedFiles = []; this._preuploadFiles = []; this._uploadProgress = new WeakMap; } hasFileToUpload() { return !!this._preuploadFiles.length; } uploadedFiles() { return this._uploadedFiles; } allowMultipleFiles() { this._allowMultipleFiles = true; this.enqueueToRender(); } addUploadedFile(uploadedFile) { console.assert(uploadedFile instanceof UploadedFile); if (this._uploadedFiles.includes(uploadedFile)) return; this._uploadedFiles.push(uploadedFile); this.enqueueToRender(); } didConstructShadowTree() { const addButton = this.content('file-adder'); addButton.onclick = () => { inputElement.click(); } addButton.addEventListener('dragover', (event) => { event.dataTransfer.dropEffect = 'copy'; event.preventDefault(); }); addButton.addEventListener('drop', (event) => { event.preventDefault(); let files = event.dataTransfer.files; if (!files.length) return; if (files.length > 1 && !this._allowMultipleFiles) files = [files[0]]; this._uploadFiles(files); }); const inputElement = document.createElement('input'); inputElement.type = 'file'; inputElement.onchange = () => this._didFileInputChange(inputElement); this._fileInput = inputElement; } render() { this._renderUploadedFilesLazily.evaluate(...this._uploadedFiles); const uploadStatusElements = this._renderPreuploadFilesLazily.evaluate(...this._preuploadFiles); this._updateUploadStatus(uploadStatusElements); const fileCount = this._uploadedFiles.length + this._preuploadFiles.length; this.content('file-adder').style.display = this._allowMultipleFiles || !fileCount ? null : 'none'; } _renderUploadedFiles(...uploadedFiles) { const element = ComponentBase.createElement; this.renderReplace(this.content('uploaded-files'), uploadedFiles.map((uploadedFile) => { const authorInfo = uploadedFile.author() ? ' by ' + uploadedFile.author() : ''; const createdAt = Metric.formatTime(uploadedFile.createdAt()); const deleteButton = new CloseButton; deleteButton.listenToAction('activate', () => this._removeUploadedFile(uploadedFile)); return element('li', [ deleteButton, element('code', {class: 'filename'}, uploadedFile.filename()), ' ', element('small', {class: 'filesize'}, '(' + this._fileSizeFormatter(uploadedFile.size()) + ')'), element('small', {class: 'meta'}, `Uploaded${authorInfo} on ${createdAt}`), ]); })); } _renderPreuploadFiles(...preuploadFiles) { const element = ComponentBase.createElement; const uploadStatusElements = []; this.renderReplace(this.content('preupload-files'), preuploadFiles.map((file) => { const progressBar = element('progress'); const meta = element('small', {class: 'meta'}, progressBar); uploadStatusElements.push({file, meta, progressBar}); return element('li', [ element('code', file.name), ' ', element('small', {class: 'filesize'}, '(' + this._fileSizeFormatter(file.size) + ')'), meta, ]); })); return uploadStatusElements; } _updateUploadStatus(uploadStatusElements) { for (let entry of uploadStatusElements) { const progress = this._uploadProgress.get(entry.file); const progressBar = entry.progressBar; if (!progress) { progressBar.removeAttribute('max'); progressBar.removeAttribute('value'); return; } if (progress.error) { entry.meta.classList.add('hasError'); entry.meta.textContent = this._formatUploadError(progress.error); } else { progressBar.max = progress.total; progressBar.value = progress.loaded; } } } _formatUploadError(error) { switch (error) { case 'NotSupported': return 'Failed: File uploading is disabled'; case 'FileSizeLimitExceeded': return 'Failed: The uploaded file was too big'; case 'FileSizeQuotaExceeded': return 'Failed: Exceeded file upload quota'; } return 'Failed to upload the file'; } _didFileInputChange(input) { if (!input.files.length) return; this._uploadFiles(input.files); input.value = null; this.enqueueToRender(); } _uploadFiles(files) { const limit = UploadedFile.fileUploadSizeLimit; files = Array.from(files); for (let file of files) { if (file.size > limit) { alert(`The specified file "${file.name}" is too big (${this._fileSizeFormatter(file.size)}). It must be smaller than ${this._fileSizeFormatter(limit)}`); return; } } const uploadProgress = this._uploadProgress; for (let file of files) { UploadedFile.fetchUploadedFileWithIdenticalHash(file).then((uploadedFile) => { if (uploadedFile) { this._didUploadFile(file, uploadedFile); return; } UploadedFile.uploadFile(file, (progress) => { uploadProgress.set(file, progress); if (this._uploadProgress == uploadProgress) this.enqueueToRender(); }).then((uploadedFile) => { if (this._uploadProgress == uploadProgress) this._didUploadFile(file, uploadedFile); }, (error) => { uploadProgress.set(file, {error: error === 0 ? 'UnknownError' : error}); if (this._uploadedProgress == uploadProgress) this.enqueueToRender(); }); }); } this._preuploadFiles = Array.from(files); } _removeUploadedFile(uploadedFile) { // FIXME: Send a request to delete the file. console.assert(uploadedFile instanceof UploadedFile); const index = this._uploadedFiles.indexOf(uploadedFile); if (index < 0) return; this._uploadedFiles.splice(index, 1); this.dispatchAction('removedFile', uploadedFile); this.enqueueToRender(); } _didUploadFile(file, uploadedFile) { console.assert(file instanceof File); const index = this._preuploadFiles.indexOf(file); if (index >= 0) this._preuploadFiles.splice(index, 1); this._uploadedFiles.push(uploadedFile); this.dispatchAction('uploadedFile', uploadedFile); this.enqueueToRender(); } static htmlTemplate() { return `<ul id="uploaded-files"></ul> <ul id="preupload-files"></ul> <button id="file-adder"><slot>Add a new file</slot></button>`; } static cssTemplate() { return ` ul:empty { display: none; } ul, li { margin: 0; padding: 0; list-style: none; } li { position: relative; margin-bottom: 0.25rem; padding-left: 1.5rem; padding-bottom: 0.25rem; border-bottom: solid 1px #eee; } li:last-child { border-bottom: none; } li > close-button { position: absolute; left: 0; top: 50%; margin-top: -0.5rem; } li > progress { display: block; } code { font-size: 1.1rem; font-weight: inherit; } small { font-size: 0.8rem; font-weight: inherit; color: #666; } small.meta { display: block; } .hasError { color: #c60; font-weight: normal; } `; } } ComponentBase.defineElement('instant-file-uploader', InstantFileUploader);