@vaadin/upload 25.3.0-alpha9 → 25.3.0-beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +482 -84
- package/package.json +11 -11
- package/src/vaadin-upload-button.js +1 -1
- package/src/vaadin-upload-file-list-mixin.d.ts +1 -0
- package/src/vaadin-upload-file-list-mixin.js +10 -103
- package/src/vaadin-upload-helpers.js +143 -0
- package/src/vaadin-upload-internal-manager.js +174 -0
- package/src/vaadin-upload-manager.js +85 -32
- package/src/vaadin-upload-mixin.d.ts +4 -44
- package/src/vaadin-upload-mixin.js +359 -564
- package/src/vaadin-upload.d.ts +39 -38
- package/src/vaadin-upload.js +39 -38
- package/web-types.json +2 -2
- package/web-types.lit.json +2 -2
|
@@ -9,7 +9,9 @@ import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
|
|
|
9
9
|
import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
10
10
|
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
|
|
11
11
|
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
12
|
-
import {
|
|
12
|
+
import { DEFAULT_I18N as FILE_LIST_DEFAULT_I18N } from './vaadin-upload-file-list-mixin.js';
|
|
13
|
+
import { getFilesFromDropEvent, translateErrorKey, updateFileStatus } from './vaadin-upload-helpers.js';
|
|
14
|
+
import { InternalUploadManager, parseHeaders } from './vaadin-upload-internal-manager.js';
|
|
13
15
|
|
|
14
16
|
export const DEFAULT_I18N = {
|
|
15
17
|
dropFiles: {
|
|
@@ -20,92 +22,12 @@ export const DEFAULT_I18N = {
|
|
|
20
22
|
one: 'Upload File...',
|
|
21
23
|
many: 'Upload Files...',
|
|
22
24
|
},
|
|
23
|
-
error:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
uploading: {
|
|
29
|
-
status: {
|
|
30
|
-
connecting: 'Connecting...',
|
|
31
|
-
stalled: 'Stalled',
|
|
32
|
-
processing: 'Processing File...',
|
|
33
|
-
held: 'Queued',
|
|
34
|
-
},
|
|
35
|
-
remainingTime: {
|
|
36
|
-
prefix: 'remaining time: ',
|
|
37
|
-
unknown: 'unknown remaining time',
|
|
38
|
-
},
|
|
39
|
-
error: {
|
|
40
|
-
serverUnavailable: 'Upload failed, please try again later',
|
|
41
|
-
unexpectedServerError: 'Upload failed due to server error',
|
|
42
|
-
forbidden: 'Upload forbidden',
|
|
43
|
-
fileTooLarge: 'File is too large',
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
file: {
|
|
47
|
-
retry: 'Retry',
|
|
48
|
-
start: 'Start',
|
|
49
|
-
remove: 'Remove',
|
|
50
|
-
},
|
|
51
|
-
units: {
|
|
52
|
-
size: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
|
53
|
-
},
|
|
25
|
+
error: FILE_LIST_DEFAULT_I18N.error,
|
|
26
|
+
uploading: FILE_LIST_DEFAULT_I18N.uploading,
|
|
27
|
+
file: FILE_LIST_DEFAULT_I18N.file,
|
|
28
|
+
units: FILE_LIST_DEFAULT_I18N.units,
|
|
54
29
|
};
|
|
55
30
|
|
|
56
|
-
class AddButtonController extends SlotController {
|
|
57
|
-
constructor(host) {
|
|
58
|
-
super(host, 'add-button', 'vaadin-button');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Override method inherited from `SlotController`
|
|
63
|
-
* to add listeners to default and custom node.
|
|
64
|
-
*
|
|
65
|
-
* @param {Node} node
|
|
66
|
-
* @protected
|
|
67
|
-
* @override
|
|
68
|
-
*/
|
|
69
|
-
initNode(node) {
|
|
70
|
-
// Needed by Flow counterpart to apply i18n to custom button
|
|
71
|
-
if (node._isDefault) {
|
|
72
|
-
this.defaultNode = node;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
node.addEventListener('touchend', (e) => {
|
|
76
|
-
this.host._onAddFilesTouchEnd(e);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
node.addEventListener('click', (e) => {
|
|
80
|
-
this.host._onAddFilesClick(e);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
this.host._addButton = node;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
class DropLabelController extends SlotController {
|
|
88
|
-
constructor(host) {
|
|
89
|
-
super(host, 'drop-label', 'span');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Override method inherited from `SlotController`
|
|
94
|
-
* to add listeners to default and custom node.
|
|
95
|
-
*
|
|
96
|
-
* @param {Node} node
|
|
97
|
-
* @protected
|
|
98
|
-
* @override
|
|
99
|
-
*/
|
|
100
|
-
initNode(node) {
|
|
101
|
-
// Needed by Flow counterpart to apply i18n to custom label
|
|
102
|
-
if (node._isDefault) {
|
|
103
|
-
this.defaultNode = node;
|
|
104
|
-
}
|
|
105
|
-
this.host._dropLabel = node;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
31
|
export const UploadMixin = (superClass) =>
|
|
110
32
|
class UploadMixin extends I18nMixin(superClass) {
|
|
111
33
|
static get properties() {
|
|
@@ -208,6 +130,7 @@ export const UploadMixin = (superClass) =>
|
|
|
208
130
|
notify: true,
|
|
209
131
|
value: () => [],
|
|
210
132
|
sync: true,
|
|
133
|
+
observer: '__filesChanged',
|
|
211
134
|
},
|
|
212
135
|
|
|
213
136
|
/**
|
|
@@ -344,32 +267,15 @@ export const UploadMixin = (superClass) =>
|
|
|
344
267
|
_fileList: {
|
|
345
268
|
type: Object,
|
|
346
269
|
},
|
|
347
|
-
|
|
348
|
-
/** @private */
|
|
349
|
-
_files: {
|
|
350
|
-
type: Array,
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
/** @private */
|
|
354
|
-
_uploadQueue: {
|
|
355
|
-
type: Array,
|
|
356
|
-
value: () => [],
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
/** @private */
|
|
360
|
-
_activeUploads: {
|
|
361
|
-
type: Number,
|
|
362
|
-
value: 0,
|
|
363
|
-
},
|
|
364
270
|
};
|
|
365
271
|
}
|
|
366
272
|
|
|
367
273
|
static get observers() {
|
|
368
274
|
return [
|
|
275
|
+
'__updateMaxFilesReached(maxFiles, files)',
|
|
369
276
|
'__updateAddButton(_addButton, maxFiles, __effectiveI18n, maxFilesReached, disabled)',
|
|
370
277
|
'__updateDropLabel(_dropLabel, maxFiles, __effectiveI18n)',
|
|
371
278
|
'__updateFileList(_fileList, files, __effectiveI18n, disabled, _theme)',
|
|
372
|
-
'__updateMaxFilesReached(maxFiles, files)',
|
|
373
279
|
];
|
|
374
280
|
}
|
|
375
281
|
|
|
@@ -444,44 +350,104 @@ export const UploadMixin = (superClass) =>
|
|
|
444
350
|
super.i18n = value;
|
|
445
351
|
}
|
|
446
352
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
processedToken = processedToken.replace(/[+.]/gu, '\\$&');
|
|
456
|
-
// Make extension patterns match the end of the file name
|
|
457
|
-
if (processedToken.startsWith('\\.')) {
|
|
458
|
-
processedToken = `.*${processedToken}$`;
|
|
459
|
-
}
|
|
460
|
-
// Handle star (*) wildcards
|
|
461
|
-
return processedToken.replace(/\/\*/gu, '/.*');
|
|
462
|
-
});
|
|
463
|
-
// Create accept regex
|
|
464
|
-
return new RegExp(`^(${processedTokens.join('|')})$`, 'iu');
|
|
353
|
+
constructor() {
|
|
354
|
+
super();
|
|
355
|
+
|
|
356
|
+
this._manager = new InternalUploadManager(this);
|
|
357
|
+
|
|
358
|
+
// Files that are uploaded without being added to the `files` list
|
|
359
|
+
// (e.g. passed directly to `uploadFiles`)
|
|
360
|
+
this.__externalFiles = new Set();
|
|
465
361
|
}
|
|
466
362
|
|
|
467
363
|
/** @protected */
|
|
468
364
|
ready() {
|
|
469
365
|
super.ready();
|
|
366
|
+
|
|
367
|
+
// Set up manager event listeners
|
|
368
|
+
this._manager.addEventListener('files-changed', (e) => this.__onManagerFilesChanged(e));
|
|
369
|
+
this._manager.addEventListener('file-reject', (e) => this.__onManagerFileReject(e));
|
|
370
|
+
this._manager.addEventListener('file-remove', (e) => this.__onManagerFileRemove(e));
|
|
371
|
+
this._manager.addEventListener('upload-success', (e) => this.__onManagerUploadSuccess(e));
|
|
372
|
+
this._manager.addEventListener('upload-error', (e) => this.__onManagerUploadError(e));
|
|
373
|
+
this._manager.addEventListener('upload-retry', (e) => this.__onManagerUploadRetry(e));
|
|
374
|
+
this._manager.addEventListener('upload-before', (e) => {
|
|
375
|
+
// The manager assigns formDataName when a file is added, but the
|
|
376
|
+
// component has historically assigned it when the upload starts, so
|
|
377
|
+
// that later changes to the property also apply to files that were
|
|
378
|
+
// already added. Listeners of the redispatched event can still
|
|
379
|
+
// override the value before the request body is created.
|
|
380
|
+
if (this.uploadFormat !== 'raw') {
|
|
381
|
+
e.detail.file.formDataName = this.formDataName;
|
|
382
|
+
}
|
|
383
|
+
this.__redispatchEvent(e);
|
|
384
|
+
if (!e.defaultPrevented && typeof this.headers === 'string') {
|
|
385
|
+
// The component has historically parsed a headers JSON string into
|
|
386
|
+
// the property when an upload starts, after the upload-before
|
|
387
|
+
// listeners have run
|
|
388
|
+
this.headers = parseHeaders(this.headers);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
this._manager.addEventListener('upload-progress', (e) => {
|
|
392
|
+
this.__applyLegacyProgressStats(e.detail.file);
|
|
393
|
+
this.__redispatchEvent(e);
|
|
394
|
+
});
|
|
395
|
+
['upload-request', 'upload-start', 'upload-response', 'upload-abort'].forEach((type) =>
|
|
396
|
+
this._manager.addEventListener(type, (e) => this.__redispatchEvent(e)),
|
|
397
|
+
);
|
|
398
|
+
|
|
470
399
|
this.addEventListener('dragover', this._onDragover.bind(this));
|
|
471
400
|
this.addEventListener('dragleave', this._onDragleave.bind(this));
|
|
472
401
|
this.addEventListener('drop', this._onDrop.bind(this));
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.addEventListener('file-start',
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
402
|
+
|
|
403
|
+
// Handle events dispatched by the file elements in the list
|
|
404
|
+
this.addEventListener('file-start', (e) => {
|
|
405
|
+
this.__clearFileError(e.detail.file);
|
|
406
|
+
this.__trackExternalFile(e.detail.file);
|
|
407
|
+
this._manager.startUpload(e.detail.file);
|
|
408
|
+
});
|
|
409
|
+
this.addEventListener('file-abort', (e) => {
|
|
410
|
+
const { file } = e.detail;
|
|
411
|
+
this._manager.abortUpload(file);
|
|
412
|
+
if (file.abort) {
|
|
413
|
+
this.__externalFiles.delete(file);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
this.addEventListener('file-retry', (e) => this._manager.retryUpload(e.detail.file));
|
|
417
|
+
|
|
418
|
+
// Announce the upload lifecycle to screen readers
|
|
419
|
+
const alert = (message) => announce(message, { mode: 'alert' });
|
|
420
|
+
this.addEventListener('file-reject', (e) => alert(`${e.detail.file.name}: ${e.detail.error}`));
|
|
421
|
+
this.addEventListener('upload-start', (e) => alert(`${e.detail.file.name}: 0%`));
|
|
422
|
+
this.addEventListener('upload-success', (e) => alert(`${e.detail.file.name}: 100%`));
|
|
423
|
+
this.addEventListener('upload-error', (e) => alert(`${e.detail.file.name}: ${e.detail.file.error}`));
|
|
424
|
+
|
|
425
|
+
this._addButtonController = new SlotController(this, 'add-button', 'vaadin-button', {
|
|
426
|
+
initializer: (node) => {
|
|
427
|
+
// Needed by Flow counterpart to apply i18n to custom button
|
|
428
|
+
if (node._isDefault) {
|
|
429
|
+
this._addButtonController.defaultNode = node;
|
|
430
|
+
}
|
|
431
|
+
node.addEventListener('touchend', (e) => {
|
|
432
|
+
// Cancel the event to avoid the following click event
|
|
433
|
+
e.preventDefault();
|
|
434
|
+
this._onAddFilesClick(e);
|
|
435
|
+
});
|
|
436
|
+
node.addEventListener('click', (e) => this._onAddFilesClick(e));
|
|
437
|
+
this._addButton = node;
|
|
438
|
+
},
|
|
439
|
+
});
|
|
482
440
|
this.addController(this._addButtonController);
|
|
483
441
|
|
|
484
|
-
this._dropLabelController = new
|
|
442
|
+
this._dropLabelController = new SlotController(this, 'drop-label', 'span', {
|
|
443
|
+
initializer: (label) => {
|
|
444
|
+
// Needed by Flow counterpart to apply i18n to custom label
|
|
445
|
+
if (label._isDefault) {
|
|
446
|
+
this._dropLabelController.defaultNode = label;
|
|
447
|
+
}
|
|
448
|
+
this._dropLabel = label;
|
|
449
|
+
},
|
|
450
|
+
});
|
|
485
451
|
this.addController(this._dropLabelController);
|
|
486
452
|
|
|
487
453
|
this.addController(
|
|
@@ -496,66 +462,225 @@ export const UploadMixin = (superClass) =>
|
|
|
496
462
|
}
|
|
497
463
|
|
|
498
464
|
/** @private */
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
465
|
+
__updateMaxFilesReached(maxFiles, files) {
|
|
466
|
+
this._setMaxFilesReached(maxFiles >= 0 && files.length >= maxFiles);
|
|
467
|
+
}
|
|
503
468
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Restore the `speed` and `remaining` stats the component computed
|
|
471
|
+
* before the manager integration: the speed derived from the total file
|
|
472
|
+
* size instead of the transferred bytes, and unknown (infinite)
|
|
473
|
+
* remaining time when no bytes have been transferred yet. The status
|
|
474
|
+
* strings derived from the stats are recomputed by `__redispatchEvent`
|
|
475
|
+
* before the `upload-progress` listeners run.
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
__applyLegacyProgressStats(file) {
|
|
479
|
+
if (file.uploading && !file.errorKey && !file.abort && file.total && file.elapsed) {
|
|
480
|
+
file.speed = ~~(file.total / file.elapsed / 1024);
|
|
481
|
+
if (!file.loaded) {
|
|
482
|
+
file.remaining = Infinity;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
510
485
|
}
|
|
511
486
|
|
|
512
487
|
/** @private */
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
488
|
+
__filesChanged(files) {
|
|
489
|
+
// Sync files to manager when set directly (e.g., from tests or user code)
|
|
490
|
+
// Skip if this change was triggered by the manager's files-changed event
|
|
491
|
+
if (this._manager && !this.__updatingFromManager) {
|
|
492
|
+
// The flag suppresses the synchronous file list render for the
|
|
493
|
+
// resulting files-changed event, which the component has
|
|
494
|
+
// historically not done for property assignments
|
|
495
|
+
this.__syncingFilesToManager = true;
|
|
496
|
+
this._manager.files = files;
|
|
497
|
+
this.__syncingFilesToManager = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
516
500
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
501
|
+
// ============ Manager event handlers ============
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Update the status strings of the given file, hiding the status when an
|
|
505
|
+
* error has been assigned to the file (e.g. by an `upload-progress`
|
|
506
|
+
* listener) while it is uploading, like the component historically did.
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
__updateFileStatus(file) {
|
|
510
|
+
updateFileStatus(file, this.__effectiveI18n);
|
|
511
|
+
if (file.uploading && file.error) {
|
|
512
|
+
file.status = undefined;
|
|
513
|
+
file.indeterminate = undefined;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** @private */
|
|
518
|
+
__onManagerFilesChanged(event) {
|
|
519
|
+
const files = event.detail.value;
|
|
520
|
+
// Only update the `files` property when files are added or removed, so that
|
|
521
|
+
// `files-changed` is not fired for upload state updates on individual files
|
|
522
|
+
const filesChanged = files.length !== this.files.length || files.some((f, i) => f !== this.files[i]);
|
|
523
|
+
if (filesChanged) {
|
|
524
|
+
// Use flag to prevent recursive sync back to manager
|
|
525
|
+
this.__updatingFromManager = true;
|
|
526
|
+
this.files = [...files];
|
|
527
|
+
this.__updatingFromManager = false;
|
|
528
|
+
// The component has historically not rendered the file list
|
|
529
|
+
// synchronously when files are added or removed — the list is
|
|
530
|
+
// updated through the files property observer instead. Only compute
|
|
531
|
+
// the file status strings, which the file list does not compute
|
|
532
|
+
// when rendering.
|
|
533
|
+
this.__updateFileStatuses();
|
|
534
|
+
} else if (!this.__syncingFilesToManager) {
|
|
535
|
+
// Compute the file status strings before rendering, like the
|
|
536
|
+
// component did before the manager integration; the file list does
|
|
537
|
+
// not compute them when rendering.
|
|
538
|
+
this.__updateFileStatuses();
|
|
539
|
+
this.__renderFileList();
|
|
520
540
|
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Update the status strings of files, including files that are uploaded
|
|
545
|
+
* without being added to the `files` list, which are not rendered by the
|
|
546
|
+
* file list but still need up-to-date status strings. Only files that
|
|
547
|
+
* are uploading or queued are updated: the status of other files stays
|
|
548
|
+
* the same until they are queued again.
|
|
549
|
+
* @private
|
|
550
|
+
*/
|
|
551
|
+
__updateFileStatuses() {
|
|
552
|
+
[...this.__externalFiles, ...this.files]
|
|
553
|
+
.filter((file) => file.uploading || file.held)
|
|
554
|
+
.forEach((file) => this.__updateFileStatus(file));
|
|
555
|
+
}
|
|
521
556
|
|
|
522
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Track a file that is uploaded without being in the `files` list, so
|
|
559
|
+
* that its status strings are also updated while it uploads. The file is
|
|
560
|
+
* removed from the tracked set once its upload finishes or is aborted.
|
|
561
|
+
* @private
|
|
562
|
+
*/
|
|
563
|
+
__trackExternalFile(file) {
|
|
564
|
+
if (!this.files.includes(file)) {
|
|
565
|
+
this.__externalFiles.add(file);
|
|
566
|
+
}
|
|
523
567
|
}
|
|
524
568
|
|
|
525
569
|
/** @private */
|
|
526
|
-
|
|
527
|
-
if (typeof this.
|
|
528
|
-
|
|
570
|
+
__renderFileList() {
|
|
571
|
+
if (this._fileList && typeof this._fileList.requestContentUpdate === 'function') {
|
|
572
|
+
this._fileList.requestContentUpdate();
|
|
529
573
|
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** @private */
|
|
577
|
+
__onManagerFileReject(event) {
|
|
578
|
+
const { file, error } = event.detail;
|
|
579
|
+
// Translate error code to i18n message
|
|
580
|
+
const errorMessage = this.__effectiveI18n.error[error] || error;
|
|
581
|
+
this.dispatchEvent(
|
|
582
|
+
new CustomEvent('file-reject', {
|
|
583
|
+
detail: { file, error: errorMessage },
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
530
587
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
588
|
+
/** @private */
|
|
589
|
+
__onManagerFileRemove(event) {
|
|
590
|
+
const { file, fileIndex } = event.detail;
|
|
591
|
+
|
|
592
|
+
this.dispatchEvent(
|
|
593
|
+
new CustomEvent('file-remove', {
|
|
594
|
+
detail: { file },
|
|
595
|
+
bubbles: true,
|
|
596
|
+
composed: true,
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
this._updateFocus(fileIndex);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** @private */
|
|
603
|
+
__redispatchEvent(event) {
|
|
604
|
+
const { file } = event.detail;
|
|
605
|
+
if (file) {
|
|
606
|
+
// Make sure the status strings are up-to-date when listeners of the
|
|
607
|
+
// redispatched event read them, like they were historically: e.g.
|
|
608
|
+
// 'Connecting...' already at the upload-request event, while the
|
|
609
|
+
// file list only renders it on the next files-changed
|
|
610
|
+
this.__updateFileStatus(file);
|
|
534
611
|
}
|
|
612
|
+
const dispatched = this.dispatchEvent(
|
|
613
|
+
new CustomEvent(event.type, {
|
|
614
|
+
detail: event.detail,
|
|
615
|
+
cancelable: event.cancelable,
|
|
616
|
+
}),
|
|
617
|
+
);
|
|
618
|
+
if (event.cancelable && !dispatched) {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
535
622
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
623
|
+
/** @private */
|
|
624
|
+
__onManagerUploadSuccess(event) {
|
|
625
|
+
const { file } = event.detail;
|
|
626
|
+
this.__externalFiles.delete(file);
|
|
627
|
+
// Check if error was set by upload-response listener (for backwards compatibility)
|
|
628
|
+
if (file.error) {
|
|
629
|
+
file.complete = false;
|
|
630
|
+
this.dispatchEvent(new CustomEvent('upload-error', { detail: event.detail }));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
this.__redispatchEvent(event);
|
|
542
634
|
}
|
|
543
635
|
|
|
544
636
|
/** @private */
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
: this.__effectiveI18n.uploading.remainingTime.unknown;
|
|
637
|
+
__onManagerUploadRetry(event) {
|
|
638
|
+
this.__redispatchEvent(event);
|
|
639
|
+
if (!event.defaultPrevented) {
|
|
640
|
+
this.__clearFileError(event.detail.file);
|
|
550
641
|
|
|
551
|
-
|
|
642
|
+
const fileIndex = this.files.indexOf(event.detail.file);
|
|
643
|
+
if (fileIndex >= 0) {
|
|
644
|
+
this._updateFocus(fileIndex);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Clear the error from a previous upload attempt when the upload of the
|
|
651
|
+
* file is about to be restarted. The manager resets its own error state
|
|
652
|
+
* (`errorKey`) when queueing a file, but not the translated `error` message
|
|
653
|
+
* that this mixin assigns.
|
|
654
|
+
* @private
|
|
655
|
+
*/
|
|
656
|
+
__clearFileError(file) {
|
|
657
|
+
if (!file.uploading) {
|
|
658
|
+
file.error = false;
|
|
659
|
+
}
|
|
552
660
|
}
|
|
553
661
|
|
|
554
662
|
/** @private */
|
|
555
|
-
|
|
556
|
-
|
|
663
|
+
__onManagerUploadError(event) {
|
|
664
|
+
const { file } = event.detail;
|
|
665
|
+
this.__externalFiles.delete(file);
|
|
666
|
+
// Translate errorKey to i18n message and set file.error, also when a
|
|
667
|
+
// listener has already assigned an error, which the component has
|
|
668
|
+
// historically overwritten with the message derived from the response.
|
|
669
|
+
// An error key without an i18n message (e.g. a failure to send the
|
|
670
|
+
// request) leaves the assigned error untouched.
|
|
671
|
+
const error = file.errorKey && translateErrorKey(file.errorKey, this.__effectiveI18n);
|
|
672
|
+
if (error) {
|
|
673
|
+
file.error = error;
|
|
674
|
+
}
|
|
675
|
+
this.__redispatchEvent(event);
|
|
676
|
+
// The manager stops tracking upload progress once the upload has
|
|
677
|
+
// failed, so clear the stale status and progress state here
|
|
678
|
+
file.status = undefined;
|
|
679
|
+
file.indeterminate = undefined;
|
|
557
680
|
}
|
|
558
681
|
|
|
682
|
+
// ============ UI updates ============
|
|
683
|
+
|
|
559
684
|
/** @private */
|
|
560
685
|
__updateAddButton(addButton, maxFiles, effectiveI18n, maxFilesReached, disabled) {
|
|
561
686
|
if (addButton) {
|
|
@@ -586,6 +711,18 @@ export const UploadMixin = (superClass) =>
|
|
|
586
711
|
}
|
|
587
712
|
}
|
|
588
713
|
|
|
714
|
+
// ============ Drag and drop ============
|
|
715
|
+
|
|
716
|
+
/** @private */
|
|
717
|
+
_dragoverChanged(dragover) {
|
|
718
|
+
setOrRemoveAttribute(this, 'dragover', dragover);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** @private */
|
|
722
|
+
_dragoverValidChanged(dragoverValid) {
|
|
723
|
+
setOrRemoveAttribute(this, 'dragover-valid', dragoverValid);
|
|
724
|
+
}
|
|
725
|
+
|
|
589
726
|
/** @private */
|
|
590
727
|
_onDragover(event) {
|
|
591
728
|
event.preventDefault();
|
|
@@ -615,299 +752,27 @@ export const UploadMixin = (superClass) =>
|
|
|
615
752
|
}
|
|
616
753
|
}
|
|
617
754
|
|
|
618
|
-
|
|
619
|
-
_createXhr() {
|
|
620
|
-
return new XMLHttpRequest();
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/** @private */
|
|
624
|
-
_configureXhr(xhr, file = null, isRawUpload = false) {
|
|
625
|
-
if (typeof this.headers === 'string') {
|
|
626
|
-
try {
|
|
627
|
-
this.headers = JSON.parse(this.headers);
|
|
628
|
-
} catch (_) {
|
|
629
|
-
this.headers = undefined;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
Object.entries(this.headers).forEach(([key, value]) => {
|
|
633
|
-
xhr.setRequestHeader(key, value);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// Set Content-Type and filename header for raw binary uploads
|
|
637
|
-
if (isRawUpload && file) {
|
|
638
|
-
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
|
|
639
|
-
xhr.setRequestHeader('X-Filename', encodeURIComponent(file.name));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (this.timeout) {
|
|
643
|
-
xhr.timeout = this.timeout;
|
|
644
|
-
}
|
|
645
|
-
xhr.withCredentials = this.withCredentials;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/** @private */
|
|
649
|
-
_setStatus(file, total, loaded, elapsed) {
|
|
650
|
-
file.elapsed = elapsed;
|
|
651
|
-
file.elapsedStr = this._formatTime(file.elapsed, this._splitTimeByUnits(file.elapsed));
|
|
652
|
-
file.remaining = Math.ceil(elapsed * (total / loaded - 1));
|
|
653
|
-
file.remainingStr = this._formatTime(file.remaining, this._splitTimeByUnits(file.remaining));
|
|
654
|
-
file.speed = ~~(total / elapsed / 1024);
|
|
655
|
-
file.totalStr = this._formatSize(total);
|
|
656
|
-
file.loadedStr = this._formatSize(loaded);
|
|
657
|
-
file.status = this._formatFileProgress(file);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Triggers the upload of any files that are not completed
|
|
662
|
-
*
|
|
663
|
-
* @param {!UploadFile | !Array<!UploadFile>=} files - Files being uploaded. Defaults to all outstanding files
|
|
664
|
-
*/
|
|
665
|
-
uploadFiles(files = this.files) {
|
|
666
|
-
if (files && !Array.isArray(files)) {
|
|
667
|
-
files = [files];
|
|
668
|
-
}
|
|
669
|
-
files.filter((file) => !file.complete).forEach((file) => this._queueFileUpload(file));
|
|
670
|
-
}
|
|
755
|
+
// ============ File input handling ============
|
|
671
756
|
|
|
672
757
|
/** @private */
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
file.held = true;
|
|
679
|
-
file.uploading = file.indeterminate = true;
|
|
680
|
-
file.complete = file.abort = file.error = false;
|
|
681
|
-
file.status = this.__effectiveI18n.uploading.status.held;
|
|
682
|
-
this._renderFileList();
|
|
683
|
-
|
|
684
|
-
this._uploadQueue.push(file);
|
|
685
|
-
this._processUploadQueue();
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Process the upload queue by starting uploads for queued files
|
|
690
|
-
* if there is available capacity.
|
|
691
|
-
*
|
|
692
|
-
* @private
|
|
693
|
-
*/
|
|
694
|
-
_processUploadQueue() {
|
|
695
|
-
// Process as many queued files as we have capacity for
|
|
696
|
-
while (this._uploadQueue.length > 0 && this._activeUploads < this.maxConcurrentUploads) {
|
|
697
|
-
const nextFile = this._uploadQueue.shift();
|
|
698
|
-
if (nextFile) {
|
|
699
|
-
this._uploadFile(nextFile);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
/** @private */
|
|
705
|
-
_uploadFile(file) {
|
|
706
|
-
// Increment active uploads counter
|
|
707
|
-
this._activeUploads += 1;
|
|
708
|
-
|
|
709
|
-
const ini = Date.now();
|
|
710
|
-
const xhr = (file.xhr = this._createXhr());
|
|
711
|
-
|
|
712
|
-
let stalledId, last;
|
|
713
|
-
// Onprogress is called always after onreadystatechange
|
|
714
|
-
xhr.upload.onprogress = (e) => {
|
|
715
|
-
clearTimeout(stalledId);
|
|
716
|
-
|
|
717
|
-
last = Date.now();
|
|
718
|
-
const elapsed = (last - ini) / 1000;
|
|
719
|
-
const loaded = e.loaded,
|
|
720
|
-
total = e.total,
|
|
721
|
-
progress = ~~((loaded / total) * 100);
|
|
722
|
-
file.loaded = loaded;
|
|
723
|
-
file.progress = progress;
|
|
724
|
-
file.indeterminate = loaded <= 0 || loaded >= total;
|
|
725
|
-
|
|
726
|
-
if (file.error) {
|
|
727
|
-
file.indeterminate = file.status = undefined;
|
|
728
|
-
} else if (!file.abort) {
|
|
729
|
-
if (progress < 100) {
|
|
730
|
-
this._setStatus(file, total, loaded, elapsed);
|
|
731
|
-
stalledId = setTimeout(() => {
|
|
732
|
-
file.status = this.__effectiveI18n.uploading.status.stalled;
|
|
733
|
-
this._renderFileList();
|
|
734
|
-
}, 2000);
|
|
735
|
-
} else {
|
|
736
|
-
file.loadedStr = file.totalStr;
|
|
737
|
-
file.status = this.__effectiveI18n.uploading.status.processing;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
this._renderFileList();
|
|
742
|
-
this.dispatchEvent(new CustomEvent('upload-progress', { detail: { file, xhr } }));
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
xhr.onabort = () => {
|
|
746
|
-
// Decrement active uploads counter
|
|
747
|
-
this._activeUploads -= 1;
|
|
748
|
-
this._processUploadQueue();
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
// More reliable than xhr.onload
|
|
752
|
-
xhr.onreadystatechange = () => {
|
|
753
|
-
if (xhr.readyState === 4) {
|
|
754
|
-
clearTimeout(stalledId);
|
|
755
|
-
file.indeterminate = file.uploading = false;
|
|
756
|
-
|
|
757
|
-
// Decrement active uploads counter
|
|
758
|
-
this._activeUploads -= 1;
|
|
759
|
-
this._processUploadQueue();
|
|
760
|
-
|
|
761
|
-
if (file.abort) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
file.status = '';
|
|
765
|
-
// Custom listener can modify the default behavior either
|
|
766
|
-
// preventing default, changing the xhr, or setting the file error
|
|
767
|
-
const evt = this.dispatchEvent(
|
|
768
|
-
new CustomEvent('upload-response', {
|
|
769
|
-
detail: { file, xhr },
|
|
770
|
-
cancelable: true,
|
|
771
|
-
}),
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
if (!evt) {
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
if (xhr.status === 0) {
|
|
778
|
-
file.error = this.__effectiveI18n.uploading.error.serverUnavailable;
|
|
779
|
-
} else if (xhr.status >= 500) {
|
|
780
|
-
file.error = this.__effectiveI18n.uploading.error.unexpectedServerError;
|
|
781
|
-
} else if (xhr.status === 413) {
|
|
782
|
-
file.error = this.__effectiveI18n.uploading.error.fileTooLarge;
|
|
783
|
-
} else if (xhr.status >= 400) {
|
|
784
|
-
file.error = this.__effectiveI18n.uploading.error.forbidden;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
file.complete = !file.error;
|
|
788
|
-
this.dispatchEvent(
|
|
789
|
-
new CustomEvent(`upload-${file.error ? 'error' : 'success'}`, {
|
|
790
|
-
detail: { file, xhr },
|
|
791
|
-
}),
|
|
792
|
-
);
|
|
793
|
-
this._renderFileList();
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
|
|
797
|
-
// Determine upload format and prepare request body
|
|
798
|
-
const isRawUpload = this.uploadFormat === 'raw';
|
|
799
|
-
|
|
800
|
-
if (!file.uploadTarget) {
|
|
801
|
-
file.uploadTarget = this.target || '';
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Only set formDataName for multipart uploads
|
|
805
|
-
if (!isRawUpload) {
|
|
806
|
-
file.formDataName = this.formDataName;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const evt = this.dispatchEvent(
|
|
810
|
-
new CustomEvent('upload-before', {
|
|
811
|
-
detail: { file, xhr },
|
|
812
|
-
cancelable: true,
|
|
813
|
-
}),
|
|
814
|
-
);
|
|
815
|
-
if (!evt) {
|
|
758
|
+
_onAddFilesClick(e) {
|
|
759
|
+
if (this.maxFilesReached) {
|
|
816
760
|
return;
|
|
817
761
|
}
|
|
818
762
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
requestBody = file;
|
|
823
|
-
} else {
|
|
824
|
-
// Multipart upload - use FormData
|
|
825
|
-
const formData = new FormData();
|
|
826
|
-
formData.append(file.formDataName, file, file.name);
|
|
827
|
-
requestBody = formData;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
xhr.open(this.method, file.uploadTarget, true);
|
|
831
|
-
this._configureXhr(xhr, file, isRawUpload);
|
|
832
|
-
|
|
833
|
-
file.held = false;
|
|
834
|
-
file.status = this.__effectiveI18n.uploading.status.connecting;
|
|
835
|
-
|
|
836
|
-
xhr.upload.onloadstart = () => {
|
|
837
|
-
this.dispatchEvent(
|
|
838
|
-
new CustomEvent('upload-start', {
|
|
839
|
-
detail: { file, xhr },
|
|
840
|
-
}),
|
|
841
|
-
);
|
|
842
|
-
this._renderFileList();
|
|
843
|
-
};
|
|
844
|
-
|
|
845
|
-
// Custom listener could modify the xhr just before sending it
|
|
846
|
-
// preventing default
|
|
847
|
-
const eventDetail = {
|
|
848
|
-
file,
|
|
849
|
-
xhr,
|
|
850
|
-
uploadFormat: this.uploadFormat,
|
|
851
|
-
requestBody,
|
|
852
|
-
};
|
|
853
|
-
|
|
854
|
-
// Expose formData property when using multipart so listeners can modify it
|
|
855
|
-
if (!isRawUpload) {
|
|
856
|
-
eventDetail.formData = requestBody;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const uploadEvt = this.dispatchEvent(
|
|
860
|
-
new CustomEvent('upload-request', {
|
|
861
|
-
detail: eventDetail,
|
|
862
|
-
cancelable: true,
|
|
863
|
-
}),
|
|
864
|
-
);
|
|
865
|
-
if (uploadEvt) {
|
|
866
|
-
xhr.send(requestBody);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/** @private */
|
|
871
|
-
_retryFileUpload(file) {
|
|
872
|
-
const evt = this.dispatchEvent(
|
|
873
|
-
new CustomEvent('upload-retry', {
|
|
874
|
-
detail: { file, xhr: file.xhr },
|
|
875
|
-
cancelable: true,
|
|
876
|
-
}),
|
|
877
|
-
);
|
|
878
|
-
if (evt) {
|
|
879
|
-
this._queueFileUpload(file);
|
|
880
|
-
this._updateFocus(this.files.indexOf(file));
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/** @private */
|
|
885
|
-
_abortFileUpload(file) {
|
|
886
|
-
const evt = this.dispatchEvent(
|
|
887
|
-
new CustomEvent('upload-abort', {
|
|
888
|
-
detail: { file, xhr: file.xhr },
|
|
889
|
-
cancelable: true,
|
|
890
|
-
}),
|
|
891
|
-
);
|
|
892
|
-
if (evt) {
|
|
893
|
-
file.abort = true;
|
|
894
|
-
if (file.xhr) {
|
|
895
|
-
file.xhr.abort();
|
|
896
|
-
}
|
|
897
|
-
this._removeFile(file);
|
|
898
|
-
}
|
|
763
|
+
e.stopPropagation();
|
|
764
|
+
this.$.fileInput.value = '';
|
|
765
|
+
this.$.fileInput.click();
|
|
899
766
|
}
|
|
900
767
|
|
|
901
768
|
/** @private */
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
this._fileList.requestContentUpdate();
|
|
905
|
-
}
|
|
769
|
+
_onFileInputChange(event) {
|
|
770
|
+
this._addFiles(event.target.files);
|
|
906
771
|
}
|
|
907
772
|
|
|
908
773
|
/** @private */
|
|
909
774
|
_addFiles(files) {
|
|
910
|
-
Array.
|
|
775
|
+
Array.from(files).forEach((file) => this._addFile(file));
|
|
911
776
|
}
|
|
912
777
|
|
|
913
778
|
/**
|
|
@@ -917,146 +782,76 @@ export const UploadMixin = (superClass) =>
|
|
|
917
782
|
* @protected
|
|
918
783
|
*/
|
|
919
784
|
_addFile(file) {
|
|
920
|
-
|
|
921
|
-
this.dispatchEvent(
|
|
922
|
-
new CustomEvent('file-reject', {
|
|
923
|
-
detail: { file, error: this.__effectiveI18n.error.tooManyFiles },
|
|
924
|
-
}),
|
|
925
|
-
);
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
if (this.maxFileSize >= 0 && file.size > this.maxFileSize) {
|
|
929
|
-
this.dispatchEvent(
|
|
930
|
-
new CustomEvent('file-reject', {
|
|
931
|
-
detail: { file, error: this.__effectiveI18n.error.fileIsTooBig },
|
|
932
|
-
}),
|
|
933
|
-
);
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
const re = this.__acceptRegexp;
|
|
937
|
-
if (re && !(re.test(file.type) || re.test(file.name))) {
|
|
938
|
-
this.dispatchEvent(
|
|
939
|
-
new CustomEvent('file-reject', {
|
|
940
|
-
detail: { file, error: this.__effectiveI18n.error.incorrectFileType },
|
|
941
|
-
}),
|
|
942
|
-
);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
file.loaded = 0;
|
|
946
|
-
file.held = true;
|
|
947
|
-
file.status = this.__effectiveI18n.uploading.status.held;
|
|
948
|
-
this.files = [file, ...this.files];
|
|
949
|
-
|
|
950
|
-
if (!this.noAuto) {
|
|
951
|
-
this._queueFileUpload(file);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
/** @private */
|
|
956
|
-
_updateFocus(fileIndex) {
|
|
957
|
-
if (this.files.length === 0) {
|
|
958
|
-
this._addButton.focus({ focusVisible: isKeyboardActive() });
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
const lastFileRemoved = fileIndex === this.files.length;
|
|
962
|
-
if (lastFileRemoved) {
|
|
963
|
-
fileIndex -= 1;
|
|
964
|
-
}
|
|
965
|
-
this._fileList.children[fileIndex].firstElementChild.focus({ focusVisible: isKeyboardActive() });
|
|
785
|
+
this._manager.addFiles([file]);
|
|
966
786
|
}
|
|
967
787
|
|
|
968
788
|
/**
|
|
969
789
|
* Remove file from upload list. Called internally if file upload was canceled.
|
|
790
|
+
* Does nothing when called without a file.
|
|
970
791
|
* @param {!UploadFile} file File to remove
|
|
971
792
|
* @protected
|
|
972
793
|
*/
|
|
973
794
|
_removeFile(file) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (fileIndex >= 0) {
|
|
979
|
-
this.files = this.files.filter((i) => i !== file);
|
|
980
|
-
|
|
981
|
-
this.dispatchEvent(
|
|
982
|
-
new CustomEvent('file-remove', {
|
|
983
|
-
detail: { file },
|
|
984
|
-
bubbles: true,
|
|
985
|
-
composed: true,
|
|
986
|
-
}),
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
this._updateFocus(fileIndex);
|
|
795
|
+
// Tolerate a missing file to keep the historical no-op behavior,
|
|
796
|
+
// which e.g. UploadElement.removeFile(index) in Flow relies on
|
|
797
|
+
if (!file) {
|
|
798
|
+
return;
|
|
990
799
|
}
|
|
800
|
+
this._manager.removeFile(file);
|
|
991
801
|
}
|
|
992
802
|
|
|
993
|
-
|
|
994
|
-
_onAddFilesTouchEnd(e) {
|
|
995
|
-
// Cancel the event to avoid the following click event
|
|
996
|
-
e.preventDefault();
|
|
997
|
-
this._onAddFilesClick(e);
|
|
998
|
-
}
|
|
803
|
+
// ============ Accessibility ============
|
|
999
804
|
|
|
1000
805
|
/** @private */
|
|
1001
|
-
|
|
1002
|
-
if (this.
|
|
806
|
+
_updateFocus(fileIndex) {
|
|
807
|
+
if (this.files.length === 0) {
|
|
808
|
+
this._addButton.focus({ focusVisible: isKeyboardActive() });
|
|
1003
809
|
return;
|
|
1004
810
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
this._addFiles(event.target.files);
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/** @private */
|
|
1017
|
-
_onFileStart(event) {
|
|
1018
|
-
this._queueFileUpload(event.detail.file);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/** @private */
|
|
1022
|
-
_onFileRetry(event) {
|
|
1023
|
-
this._retryFileUpload(event.detail.file);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
/** @private */
|
|
1027
|
-
_onFileAbort(event) {
|
|
1028
|
-
this._abortFileUpload(event.detail.file);
|
|
811
|
+
// If the removed file was at the end, focus the new last file
|
|
812
|
+
const lastFileRemoved = fileIndex >= this.files.length;
|
|
813
|
+
if (lastFileRemoved) {
|
|
814
|
+
fileIndex = this.files.length - 1;
|
|
815
|
+
}
|
|
816
|
+
if (this._fileList && this._fileList.children[fileIndex]) {
|
|
817
|
+
this._fileList.children[fileIndex].firstElementChild.focus({ focusVisible: isKeyboardActive() });
|
|
818
|
+
}
|
|
1029
819
|
}
|
|
1030
820
|
|
|
1031
|
-
/**
|
|
1032
|
-
|
|
1033
|
-
|
|
821
|
+
/**
|
|
822
|
+
* Getter/setter for _createXhr to allow tests to mock XHR creation.
|
|
823
|
+
* @private
|
|
824
|
+
*/
|
|
825
|
+
get _createXhr() {
|
|
826
|
+
return this._manager._createXhr;
|
|
1034
827
|
}
|
|
1035
828
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
announce(`${event.detail.file.name}: 0%`, { mode: 'alert' });
|
|
829
|
+
set _createXhr(value) {
|
|
830
|
+
this._manager._createXhr = value;
|
|
1039
831
|
}
|
|
1040
832
|
|
|
1041
|
-
/**
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Triggers the upload of any files that are not completed
|
|
835
|
+
*
|
|
836
|
+
* @param {!UploadFile | !Array<!UploadFile>=} files - Files being uploaded. Defaults to all outstanding files
|
|
837
|
+
*/
|
|
838
|
+
uploadFiles(files = this.files) {
|
|
839
|
+
// Convert to array if single file
|
|
840
|
+
if (files && !Array.isArray(files)) {
|
|
841
|
+
files = [files];
|
|
842
|
+
}
|
|
1045
843
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
844
|
+
files
|
|
845
|
+
.filter((file) => !file.complete)
|
|
846
|
+
.forEach((file) => {
|
|
847
|
+
this.__clearFileError(file);
|
|
848
|
+
this.__trackExternalFile(file);
|
|
849
|
+
});
|
|
1050
850
|
|
|
1051
|
-
|
|
1052
|
-
_dragoverChanged(dragover) {
|
|
1053
|
-
setOrRemoveAttribute(this, 'dragover', dragover);
|
|
851
|
+
this._manager.uploadFiles(files);
|
|
1054
852
|
}
|
|
1055
853
|
|
|
1056
|
-
|
|
1057
|
-
_dragoverValidChanged(dragoverValid) {
|
|
1058
|
-
setOrRemoveAttribute(this, 'dragover-valid', dragoverValid);
|
|
1059
|
-
}
|
|
854
|
+
// ============ Utilities ============
|
|
1060
855
|
|
|
1061
856
|
/** @private */
|
|
1062
857
|
_i18nPlural(value, plural) {
|