@vaadin/upload 25.2.7 → 25.3.0-alpha10
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 +637 -204
- package/package.json +11 -11
- package/src/vaadin-upload-button.js +4 -10
- package/src/vaadin-upload-drop-zone.js +1 -0
- 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-file-list.js +1 -0
- package/src/vaadin-upload-file-mixin.js +0 -9
- package/src/vaadin-upload-file.js +8 -5
- package/src/vaadin-upload-helpers.js +143 -0
- package/src/vaadin-upload-icon.js +1 -0
- package/src/vaadin-upload-internal-manager.js +174 -0
- package/src/vaadin-upload-manager.d.ts +1 -6
- package/src/vaadin-upload-manager.js +85 -32
- package/src/vaadin-upload-mixin.d.ts +4 -44
- package/src/vaadin-upload-mixin.js +361 -577
- package/src/vaadin-upload.js +1 -0
- package/web-types.json +6 -16
- package/web-types.lit.json +1 -1
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
import { announce } from '@vaadin/a11y-base/src/announce.js';
|
|
7
7
|
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
|
|
8
8
|
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
|
|
9
|
+
import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
|
|
9
10
|
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
|
|
10
11
|
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
11
|
-
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';
|
|
12
15
|
|
|
13
16
|
export const DEFAULT_I18N = {
|
|
14
17
|
dropFiles: {
|
|
@@ -19,92 +22,12 @@ export const DEFAULT_I18N = {
|
|
|
19
22
|
one: 'Upload File...',
|
|
20
23
|
many: 'Upload Files...',
|
|
21
24
|
},
|
|
22
|
-
error:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
uploading: {
|
|
28
|
-
status: {
|
|
29
|
-
connecting: 'Connecting...',
|
|
30
|
-
stalled: 'Stalled',
|
|
31
|
-
processing: 'Processing File...',
|
|
32
|
-
held: 'Queued',
|
|
33
|
-
},
|
|
34
|
-
remainingTime: {
|
|
35
|
-
prefix: 'remaining time: ',
|
|
36
|
-
unknown: 'unknown remaining time',
|
|
37
|
-
},
|
|
38
|
-
error: {
|
|
39
|
-
serverUnavailable: 'Upload failed, please try again later',
|
|
40
|
-
unexpectedServerError: 'Upload failed due to server error',
|
|
41
|
-
forbidden: 'Upload forbidden',
|
|
42
|
-
fileTooLarge: 'File is too large',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
file: {
|
|
46
|
-
retry: 'Retry',
|
|
47
|
-
start: 'Start',
|
|
48
|
-
remove: 'Remove',
|
|
49
|
-
},
|
|
50
|
-
units: {
|
|
51
|
-
size: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
|
52
|
-
},
|
|
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,
|
|
53
29
|
};
|
|
54
30
|
|
|
55
|
-
class AddButtonController extends SlotController {
|
|
56
|
-
constructor(host) {
|
|
57
|
-
super(host, 'add-button', 'vaadin-button');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Override method inherited from `SlotController`
|
|
62
|
-
* to add listeners to default and custom node.
|
|
63
|
-
*
|
|
64
|
-
* @param {Node} node
|
|
65
|
-
* @protected
|
|
66
|
-
* @override
|
|
67
|
-
*/
|
|
68
|
-
initNode(node) {
|
|
69
|
-
// Needed by Flow counterpart to apply i18n to custom button
|
|
70
|
-
if (node._isDefault) {
|
|
71
|
-
this.defaultNode = node;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
node.addEventListener('touchend', (e) => {
|
|
75
|
-
this.host._onAddFilesTouchEnd(e);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
node.addEventListener('click', (e) => {
|
|
79
|
-
this.host._onAddFilesClick(e);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
this.host._addButton = node;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
class DropLabelController extends SlotController {
|
|
87
|
-
constructor(host) {
|
|
88
|
-
super(host, 'drop-label', 'span');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Override method inherited from `SlotController`
|
|
93
|
-
* to add listeners to default and custom node.
|
|
94
|
-
*
|
|
95
|
-
* @param {Node} node
|
|
96
|
-
* @protected
|
|
97
|
-
* @override
|
|
98
|
-
*/
|
|
99
|
-
initNode(node) {
|
|
100
|
-
// Needed by Flow counterpart to apply i18n to custom label
|
|
101
|
-
if (node._isDefault) {
|
|
102
|
-
this.defaultNode = node;
|
|
103
|
-
}
|
|
104
|
-
this.host._dropLabel = node;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
31
|
export const UploadMixin = (superClass) =>
|
|
109
32
|
class UploadMixin extends I18nMixin(superClass) {
|
|
110
33
|
static get properties() {
|
|
@@ -207,6 +130,7 @@ export const UploadMixin = (superClass) =>
|
|
|
207
130
|
notify: true,
|
|
208
131
|
value: () => [],
|
|
209
132
|
sync: true,
|
|
133
|
+
observer: '__filesChanged',
|
|
210
134
|
},
|
|
211
135
|
|
|
212
136
|
/**
|
|
@@ -343,32 +267,15 @@ export const UploadMixin = (superClass) =>
|
|
|
343
267
|
_fileList: {
|
|
344
268
|
type: Object,
|
|
345
269
|
},
|
|
346
|
-
|
|
347
|
-
/** @private */
|
|
348
|
-
_files: {
|
|
349
|
-
type: Array,
|
|
350
|
-
},
|
|
351
|
-
|
|
352
|
-
/** @private */
|
|
353
|
-
_uploadQueue: {
|
|
354
|
-
type: Array,
|
|
355
|
-
value: () => [],
|
|
356
|
-
},
|
|
357
|
-
|
|
358
|
-
/** @private */
|
|
359
|
-
_activeUploads: {
|
|
360
|
-
type: Number,
|
|
361
|
-
value: 0,
|
|
362
|
-
},
|
|
363
270
|
};
|
|
364
271
|
}
|
|
365
272
|
|
|
366
273
|
static get observers() {
|
|
367
274
|
return [
|
|
275
|
+
'__updateMaxFilesReached(maxFiles, files)',
|
|
368
276
|
'__updateAddButton(_addButton, maxFiles, __effectiveI18n, maxFilesReached, disabled)',
|
|
369
277
|
'__updateDropLabel(_dropLabel, maxFiles, __effectiveI18n)',
|
|
370
278
|
'__updateFileList(_fileList, files, __effectiveI18n, disabled, _theme)',
|
|
371
|
-
'__updateMaxFilesReached(maxFiles, files)',
|
|
372
279
|
];
|
|
373
280
|
}
|
|
374
281
|
|
|
@@ -443,44 +350,104 @@ export const UploadMixin = (superClass) =>
|
|
|
443
350
|
super.i18n = value;
|
|
444
351
|
}
|
|
445
352
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
processedToken = processedToken.replace(/[+.]/gu, '\\$&');
|
|
455
|
-
// Make extension patterns match the end of the file name
|
|
456
|
-
if (processedToken.startsWith('\\.')) {
|
|
457
|
-
processedToken = `.*${processedToken}$`;
|
|
458
|
-
}
|
|
459
|
-
// Handle star (*) wildcards
|
|
460
|
-
return processedToken.replace(/\/\*/gu, '/.*');
|
|
461
|
-
});
|
|
462
|
-
// Create accept regex
|
|
463
|
-
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();
|
|
464
361
|
}
|
|
465
362
|
|
|
466
363
|
/** @protected */
|
|
467
364
|
ready() {
|
|
468
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
|
+
|
|
469
399
|
this.addEventListener('dragover', this._onDragover.bind(this));
|
|
470
400
|
this.addEventListener('dragleave', this._onDragleave.bind(this));
|
|
471
401
|
this.addEventListener('drop', this._onDrop.bind(this));
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.addEventListener('file-start',
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
});
|
|
481
440
|
this.addController(this._addButtonController);
|
|
482
441
|
|
|
483
|
-
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
|
+
});
|
|
484
451
|
this.addController(this._dropLabelController);
|
|
485
452
|
|
|
486
453
|
this.addController(
|
|
@@ -495,66 +462,225 @@ export const UploadMixin = (superClass) =>
|
|
|
495
462
|
}
|
|
496
463
|
|
|
497
464
|
/** @private */
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
465
|
+
__updateMaxFilesReached(maxFiles, files) {
|
|
466
|
+
this._setMaxFilesReached(maxFiles >= 0 && files.length >= maxFiles);
|
|
467
|
+
}
|
|
502
468
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
485
|
}
|
|
510
486
|
|
|
511
487
|
/** @private */
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
500
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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();
|
|
519
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
|
+
}
|
|
520
556
|
|
|
521
|
-
|
|
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
|
+
}
|
|
522
567
|
}
|
|
523
568
|
|
|
524
569
|
/** @private */
|
|
525
|
-
|
|
526
|
-
if (typeof this.
|
|
527
|
-
|
|
570
|
+
__renderFileList() {
|
|
571
|
+
if (this._fileList && typeof this._fileList.requestContentUpdate === 'function') {
|
|
572
|
+
this._fileList.requestContentUpdate();
|
|
528
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
|
+
}
|
|
587
|
+
|
|
588
|
+
/** @private */
|
|
589
|
+
__onManagerFileRemove(event) {
|
|
590
|
+
const { file, fileIndex } = event.detail;
|
|
529
591
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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);
|
|
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();
|
|
533
620
|
}
|
|
621
|
+
}
|
|
534
622
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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);
|
|
541
634
|
}
|
|
542
635
|
|
|
543
636
|
/** @private */
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
: this.__effectiveI18n.uploading.remainingTime.unknown;
|
|
637
|
+
__onManagerUploadRetry(event) {
|
|
638
|
+
this.__redispatchEvent(event);
|
|
639
|
+
if (!event.defaultPrevented) {
|
|
640
|
+
this.__clearFileError(event.detail.file);
|
|
549
641
|
|
|
550
|
-
|
|
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
|
+
}
|
|
551
660
|
}
|
|
552
661
|
|
|
553
662
|
/** @private */
|
|
554
|
-
|
|
555
|
-
|
|
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;
|
|
556
680
|
}
|
|
557
681
|
|
|
682
|
+
// ============ UI updates ============
|
|
683
|
+
|
|
558
684
|
/** @private */
|
|
559
685
|
__updateAddButton(addButton, maxFiles, effectiveI18n, maxFilesReached, disabled) {
|
|
560
686
|
if (addButton) {
|
|
@@ -581,14 +707,22 @@ export const UploadMixin = (superClass) =>
|
|
|
581
707
|
list.items = [...files];
|
|
582
708
|
list.i18n = effectiveI18n;
|
|
583
709
|
list.disabled = disabled;
|
|
584
|
-
|
|
585
|
-
list.setAttribute('theme', this._theme);
|
|
586
|
-
} else {
|
|
587
|
-
list.removeAttribute('theme');
|
|
588
|
-
}
|
|
710
|
+
setOrRemoveAttribute(list, 'theme', this._theme);
|
|
589
711
|
}
|
|
590
712
|
}
|
|
591
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
|
+
|
|
592
726
|
/** @private */
|
|
593
727
|
_onDragover(event) {
|
|
594
728
|
event.preventDefault();
|
|
@@ -618,299 +752,27 @@ export const UploadMixin = (superClass) =>
|
|
|
618
752
|
}
|
|
619
753
|
}
|
|
620
754
|
|
|
621
|
-
|
|
622
|
-
_createXhr() {
|
|
623
|
-
return new XMLHttpRequest();
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/** @private */
|
|
627
|
-
_configureXhr(xhr, file = null, isRawUpload = false) {
|
|
628
|
-
if (typeof this.headers === 'string') {
|
|
629
|
-
try {
|
|
630
|
-
this.headers = JSON.parse(this.headers);
|
|
631
|
-
} catch (_) {
|
|
632
|
-
this.headers = undefined;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
Object.entries(this.headers).forEach(([key, value]) => {
|
|
636
|
-
xhr.setRequestHeader(key, value);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
// Set Content-Type and filename header for raw binary uploads
|
|
640
|
-
if (isRawUpload && file) {
|
|
641
|
-
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
|
|
642
|
-
xhr.setRequestHeader('X-Filename', encodeURIComponent(file.name));
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (this.timeout) {
|
|
646
|
-
xhr.timeout = this.timeout;
|
|
647
|
-
}
|
|
648
|
-
xhr.withCredentials = this.withCredentials;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/** @private */
|
|
652
|
-
_setStatus(file, total, loaded, elapsed) {
|
|
653
|
-
file.elapsed = elapsed;
|
|
654
|
-
file.elapsedStr = this._formatTime(file.elapsed, this._splitTimeByUnits(file.elapsed));
|
|
655
|
-
file.remaining = Math.ceil(elapsed * (total / loaded - 1));
|
|
656
|
-
file.remainingStr = this._formatTime(file.remaining, this._splitTimeByUnits(file.remaining));
|
|
657
|
-
file.speed = ~~(total / elapsed / 1024);
|
|
658
|
-
file.totalStr = this._formatSize(total);
|
|
659
|
-
file.loadedStr = this._formatSize(loaded);
|
|
660
|
-
file.status = this._formatFileProgress(file);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Triggers the upload of any files that are not completed
|
|
665
|
-
*
|
|
666
|
-
* @param {!UploadFile | !Array<!UploadFile>=} files - Files being uploaded. Defaults to all outstanding files
|
|
667
|
-
*/
|
|
668
|
-
uploadFiles(files = this.files) {
|
|
669
|
-
if (files && !Array.isArray(files)) {
|
|
670
|
-
files = [files];
|
|
671
|
-
}
|
|
672
|
-
files.filter((file) => !file.complete).forEach((file) => this._queueFileUpload(file));
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/** @private */
|
|
676
|
-
_queueFileUpload(file) {
|
|
677
|
-
if (file.uploading) {
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
file.held = true;
|
|
682
|
-
file.uploading = file.indeterminate = true;
|
|
683
|
-
file.complete = file.abort = file.error = false;
|
|
684
|
-
file.status = this.__effectiveI18n.uploading.status.held;
|
|
685
|
-
this._renderFileList();
|
|
686
|
-
|
|
687
|
-
this._uploadQueue.push(file);
|
|
688
|
-
this._processUploadQueue();
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Process the upload queue by starting uploads for queued files
|
|
693
|
-
* if there is available capacity.
|
|
694
|
-
*
|
|
695
|
-
* @private
|
|
696
|
-
*/
|
|
697
|
-
_processUploadQueue() {
|
|
698
|
-
// Process as many queued files as we have capacity for
|
|
699
|
-
while (this._uploadQueue.length > 0 && this._activeUploads < this.maxConcurrentUploads) {
|
|
700
|
-
const nextFile = this._uploadQueue.shift();
|
|
701
|
-
if (nextFile) {
|
|
702
|
-
this._uploadFile(nextFile);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
755
|
+
// ============ File input handling ============
|
|
706
756
|
|
|
707
757
|
/** @private */
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
this._activeUploads += 1;
|
|
711
|
-
|
|
712
|
-
const ini = Date.now();
|
|
713
|
-
const xhr = (file.xhr = this._createXhr());
|
|
714
|
-
|
|
715
|
-
let stalledId, last;
|
|
716
|
-
// Onprogress is called always after onreadystatechange
|
|
717
|
-
xhr.upload.onprogress = (e) => {
|
|
718
|
-
clearTimeout(stalledId);
|
|
719
|
-
|
|
720
|
-
last = Date.now();
|
|
721
|
-
const elapsed = (last - ini) / 1000;
|
|
722
|
-
const loaded = e.loaded,
|
|
723
|
-
total = e.total,
|
|
724
|
-
progress = ~~((loaded / total) * 100);
|
|
725
|
-
file.loaded = loaded;
|
|
726
|
-
file.progress = progress;
|
|
727
|
-
file.indeterminate = loaded <= 0 || loaded >= total;
|
|
728
|
-
|
|
729
|
-
if (file.error) {
|
|
730
|
-
file.indeterminate = file.status = undefined;
|
|
731
|
-
} else if (!file.abort) {
|
|
732
|
-
if (progress < 100) {
|
|
733
|
-
this._setStatus(file, total, loaded, elapsed);
|
|
734
|
-
stalledId = setTimeout(() => {
|
|
735
|
-
file.status = this.__effectiveI18n.uploading.status.stalled;
|
|
736
|
-
this._renderFileList();
|
|
737
|
-
}, 2000);
|
|
738
|
-
} else {
|
|
739
|
-
file.loadedStr = file.totalStr;
|
|
740
|
-
file.status = this.__effectiveI18n.uploading.status.processing;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
this._renderFileList();
|
|
745
|
-
this.dispatchEvent(new CustomEvent('upload-progress', { detail: { file, xhr } }));
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
xhr.onabort = () => {
|
|
749
|
-
// Decrement active uploads counter
|
|
750
|
-
this._activeUploads -= 1;
|
|
751
|
-
this._processUploadQueue();
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
// More reliable than xhr.onload
|
|
755
|
-
xhr.onreadystatechange = () => {
|
|
756
|
-
if (xhr.readyState === 4) {
|
|
757
|
-
clearTimeout(stalledId);
|
|
758
|
-
file.indeterminate = file.uploading = false;
|
|
759
|
-
|
|
760
|
-
// Decrement active uploads counter
|
|
761
|
-
this._activeUploads -= 1;
|
|
762
|
-
this._processUploadQueue();
|
|
763
|
-
|
|
764
|
-
if (file.abort) {
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
file.status = '';
|
|
768
|
-
// Custom listener can modify the default behavior either
|
|
769
|
-
// preventing default, changing the xhr, or setting the file error
|
|
770
|
-
const evt = this.dispatchEvent(
|
|
771
|
-
new CustomEvent('upload-response', {
|
|
772
|
-
detail: { file, xhr },
|
|
773
|
-
cancelable: true,
|
|
774
|
-
}),
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
if (!evt) {
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (xhr.status === 0) {
|
|
781
|
-
file.error = this.__effectiveI18n.uploading.error.serverUnavailable;
|
|
782
|
-
} else if (xhr.status >= 500) {
|
|
783
|
-
file.error = this.__effectiveI18n.uploading.error.unexpectedServerError;
|
|
784
|
-
} else if (xhr.status === 413) {
|
|
785
|
-
file.error = this.__effectiveI18n.uploading.error.fileTooLarge;
|
|
786
|
-
} else if (xhr.status >= 400) {
|
|
787
|
-
file.error = this.__effectiveI18n.uploading.error.forbidden;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
file.complete = !file.error;
|
|
791
|
-
this.dispatchEvent(
|
|
792
|
-
new CustomEvent(`upload-${file.error ? 'error' : 'success'}`, {
|
|
793
|
-
detail: { file, xhr },
|
|
794
|
-
}),
|
|
795
|
-
);
|
|
796
|
-
this._renderFileList();
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
// Determine upload format and prepare request body
|
|
801
|
-
const isRawUpload = this.uploadFormat === 'raw';
|
|
802
|
-
|
|
803
|
-
if (!file.uploadTarget) {
|
|
804
|
-
file.uploadTarget = this.target || '';
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Only set formDataName for multipart uploads
|
|
808
|
-
if (!isRawUpload) {
|
|
809
|
-
file.formDataName = this.formDataName;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const evt = this.dispatchEvent(
|
|
813
|
-
new CustomEvent('upload-before', {
|
|
814
|
-
detail: { file, xhr },
|
|
815
|
-
cancelable: true,
|
|
816
|
-
}),
|
|
817
|
-
);
|
|
818
|
-
if (!evt) {
|
|
758
|
+
_onAddFilesClick(e) {
|
|
759
|
+
if (this.maxFilesReached) {
|
|
819
760
|
return;
|
|
820
761
|
}
|
|
821
762
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
requestBody = file;
|
|
826
|
-
} else {
|
|
827
|
-
// Multipart upload - use FormData
|
|
828
|
-
const formData = new FormData();
|
|
829
|
-
formData.append(file.formDataName, file, file.name);
|
|
830
|
-
requestBody = formData;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
xhr.open(this.method, file.uploadTarget, true);
|
|
834
|
-
this._configureXhr(xhr, file, isRawUpload);
|
|
835
|
-
|
|
836
|
-
file.held = false;
|
|
837
|
-
file.status = this.__effectiveI18n.uploading.status.connecting;
|
|
838
|
-
|
|
839
|
-
xhr.upload.onloadstart = () => {
|
|
840
|
-
this.dispatchEvent(
|
|
841
|
-
new CustomEvent('upload-start', {
|
|
842
|
-
detail: { file, xhr },
|
|
843
|
-
}),
|
|
844
|
-
);
|
|
845
|
-
this._renderFileList();
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
// Custom listener could modify the xhr just before sending it
|
|
849
|
-
// preventing default
|
|
850
|
-
const eventDetail = {
|
|
851
|
-
file,
|
|
852
|
-
xhr,
|
|
853
|
-
uploadFormat: this.uploadFormat,
|
|
854
|
-
requestBody,
|
|
855
|
-
};
|
|
856
|
-
|
|
857
|
-
// Expose formData property when using multipart so listeners can modify it
|
|
858
|
-
if (!isRawUpload) {
|
|
859
|
-
eventDetail.formData = requestBody;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const uploadEvt = this.dispatchEvent(
|
|
863
|
-
new CustomEvent('upload-request', {
|
|
864
|
-
detail: eventDetail,
|
|
865
|
-
cancelable: true,
|
|
866
|
-
}),
|
|
867
|
-
);
|
|
868
|
-
if (uploadEvt) {
|
|
869
|
-
xhr.send(requestBody);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
/** @private */
|
|
874
|
-
_retryFileUpload(file) {
|
|
875
|
-
const evt = this.dispatchEvent(
|
|
876
|
-
new CustomEvent('upload-retry', {
|
|
877
|
-
detail: { file, xhr: file.xhr },
|
|
878
|
-
cancelable: true,
|
|
879
|
-
}),
|
|
880
|
-
);
|
|
881
|
-
if (evt) {
|
|
882
|
-
this._queueFileUpload(file);
|
|
883
|
-
this._updateFocus(this.files.indexOf(file));
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/** @private */
|
|
888
|
-
_abortFileUpload(file) {
|
|
889
|
-
const evt = this.dispatchEvent(
|
|
890
|
-
new CustomEvent('upload-abort', {
|
|
891
|
-
detail: { file, xhr: file.xhr },
|
|
892
|
-
cancelable: true,
|
|
893
|
-
}),
|
|
894
|
-
);
|
|
895
|
-
if (evt) {
|
|
896
|
-
file.abort = true;
|
|
897
|
-
if (file.xhr) {
|
|
898
|
-
file.xhr.abort();
|
|
899
|
-
}
|
|
900
|
-
this._removeFile(file);
|
|
901
|
-
}
|
|
763
|
+
e.stopPropagation();
|
|
764
|
+
this.$.fileInput.value = '';
|
|
765
|
+
this.$.fileInput.click();
|
|
902
766
|
}
|
|
903
767
|
|
|
904
768
|
/** @private */
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
this._fileList.requestContentUpdate();
|
|
908
|
-
}
|
|
769
|
+
_onFileInputChange(event) {
|
|
770
|
+
this._addFiles(event.target.files);
|
|
909
771
|
}
|
|
910
772
|
|
|
911
773
|
/** @private */
|
|
912
774
|
_addFiles(files) {
|
|
913
|
-
Array.
|
|
775
|
+
Array.from(files).forEach((file) => this._addFile(file));
|
|
914
776
|
}
|
|
915
777
|
|
|
916
778
|
/**
|
|
@@ -920,154 +782,76 @@ export const UploadMixin = (superClass) =>
|
|
|
920
782
|
* @protected
|
|
921
783
|
*/
|
|
922
784
|
_addFile(file) {
|
|
923
|
-
|
|
924
|
-
this.dispatchEvent(
|
|
925
|
-
new CustomEvent('file-reject', {
|
|
926
|
-
detail: { file, error: this.__effectiveI18n.error.tooManyFiles },
|
|
927
|
-
}),
|
|
928
|
-
);
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
if (this.maxFileSize >= 0 && file.size > this.maxFileSize) {
|
|
932
|
-
this.dispatchEvent(
|
|
933
|
-
new CustomEvent('file-reject', {
|
|
934
|
-
detail: { file, error: this.__effectiveI18n.error.fileIsTooBig },
|
|
935
|
-
}),
|
|
936
|
-
);
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
const re = this.__acceptRegexp;
|
|
940
|
-
if (re && !(re.test(file.type) || re.test(file.name))) {
|
|
941
|
-
this.dispatchEvent(
|
|
942
|
-
new CustomEvent('file-reject', {
|
|
943
|
-
detail: { file, error: this.__effectiveI18n.error.incorrectFileType },
|
|
944
|
-
}),
|
|
945
|
-
);
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
file.loaded = 0;
|
|
949
|
-
file.held = true;
|
|
950
|
-
file.status = this.__effectiveI18n.uploading.status.held;
|
|
951
|
-
this.files = [file, ...this.files];
|
|
952
|
-
|
|
953
|
-
if (!this.noAuto) {
|
|
954
|
-
this._queueFileUpload(file);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/** @private */
|
|
959
|
-
_updateFocus(fileIndex) {
|
|
960
|
-
if (this.files.length === 0) {
|
|
961
|
-
this._addButton.focus({ focusVisible: isKeyboardActive() });
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
const lastFileRemoved = fileIndex === this.files.length;
|
|
965
|
-
if (lastFileRemoved) {
|
|
966
|
-
fileIndex -= 1;
|
|
967
|
-
}
|
|
968
|
-
this._fileList.children[fileIndex].firstElementChild.focus({ focusVisible: isKeyboardActive() });
|
|
785
|
+
this._manager.addFiles([file]);
|
|
969
786
|
}
|
|
970
787
|
|
|
971
788
|
/**
|
|
972
789
|
* Remove file from upload list. Called internally if file upload was canceled.
|
|
790
|
+
* Does nothing when called without a file.
|
|
973
791
|
* @param {!UploadFile} file File to remove
|
|
974
792
|
* @protected
|
|
975
793
|
*/
|
|
976
794
|
_removeFile(file) {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if (fileIndex >= 0) {
|
|
982
|
-
this.files = this.files.filter((i) => i !== file);
|
|
983
|
-
|
|
984
|
-
this.dispatchEvent(
|
|
985
|
-
new CustomEvent('file-remove', {
|
|
986
|
-
detail: { file },
|
|
987
|
-
bubbles: true,
|
|
988
|
-
composed: true,
|
|
989
|
-
}),
|
|
990
|
-
);
|
|
991
|
-
|
|
992
|
-
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;
|
|
993
799
|
}
|
|
800
|
+
this._manager.removeFile(file);
|
|
994
801
|
}
|
|
995
802
|
|
|
996
|
-
|
|
997
|
-
_onAddFilesTouchEnd(e) {
|
|
998
|
-
// Cancel the event to avoid the following click event
|
|
999
|
-
e.preventDefault();
|
|
1000
|
-
this._onAddFilesClick(e);
|
|
1001
|
-
}
|
|
803
|
+
// ============ Accessibility ============
|
|
1002
804
|
|
|
1003
805
|
/** @private */
|
|
1004
|
-
|
|
1005
|
-
if (this.
|
|
806
|
+
_updateFocus(fileIndex) {
|
|
807
|
+
if (this.files.length === 0) {
|
|
808
|
+
this._addButton.focus({ focusVisible: isKeyboardActive() });
|
|
1006
809
|
return;
|
|
1007
810
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
this._addFiles(event.target.files);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
/** @private */
|
|
1020
|
-
_onFileStart(event) {
|
|
1021
|
-
this._queueFileUpload(event.detail.file);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/** @private */
|
|
1025
|
-
_onFileRetry(event) {
|
|
1026
|
-
this._retryFileUpload(event.detail.file);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
/** @private */
|
|
1030
|
-
_onFileAbort(event) {
|
|
1031
|
-
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
|
+
}
|
|
1032
819
|
}
|
|
1033
820
|
|
|
1034
|
-
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
821
|
+
/**
|
|
822
|
+
* Getter/setter for _createXhr to allow tests to mock XHR creation.
|
|
823
|
+
* @private
|
|
824
|
+
*/
|
|
825
|
+
get _createXhr() {
|
|
826
|
+
return this._manager._createXhr;
|
|
1037
827
|
}
|
|
1038
828
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
announce(`${event.detail.file.name}: 0%`, { mode: 'alert' });
|
|
829
|
+
set _createXhr(value) {
|
|
830
|
+
this._manager._createXhr = value;
|
|
1042
831
|
}
|
|
1043
832
|
|
|
1044
|
-
/**
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
+
}
|
|
1048
843
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
844
|
+
files
|
|
845
|
+
.filter((file) => !file.complete)
|
|
846
|
+
.forEach((file) => {
|
|
847
|
+
this.__clearFileError(file);
|
|
848
|
+
this.__trackExternalFile(file);
|
|
849
|
+
});
|
|
1053
850
|
|
|
1054
|
-
|
|
1055
|
-
_dragoverChanged(dragover) {
|
|
1056
|
-
if (dragover) {
|
|
1057
|
-
this.setAttribute('dragover', dragover);
|
|
1058
|
-
} else {
|
|
1059
|
-
this.removeAttribute('dragover');
|
|
1060
|
-
}
|
|
851
|
+
this._manager.uploadFiles(files);
|
|
1061
852
|
}
|
|
1062
853
|
|
|
1063
|
-
|
|
1064
|
-
_dragoverValidChanged(dragoverValid) {
|
|
1065
|
-
if (dragoverValid) {
|
|
1066
|
-
this.setAttribute('dragover-valid', dragoverValid);
|
|
1067
|
-
} else {
|
|
1068
|
-
this.removeAttribute('dragover-valid');
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
854
|
+
// ============ Utilities ============
|
|
1071
855
|
|
|
1072
856
|
/** @private */
|
|
1073
857
|
_i18nPlural(value, plural) {
|