blob: 693051595ecfaae6e188aed1695455a73ff5476a [file] [log] [blame]
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);