@vaadin/upload 24.0.0-alpha1 → 24.0.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.
@@ -1,18 +1,74 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2016 - 2022 Vaadin Ltd.
3
+ * Copyright (c) 2016 - 2023 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import '@polymer/polymer/lib/elements/dom-repeat.js';
7
7
  import '@vaadin/button/src/vaadin-button.js';
8
+ import './vaadin-upload-icon.js';
8
9
  import './vaadin-upload-icons.js';
9
- import './vaadin-upload-file.js';
10
+ import './vaadin-upload-file-list.js';
10
11
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
11
12
  import { announce } from '@vaadin/component-base/src/a11y-announcer.js';
12
13
  import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
14
+ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
13
15
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
16
+ import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
14
17
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
15
18
 
19
+ class AddButtonController extends SlotController {
20
+ constructor(host) {
21
+ super(host, 'add-button', 'vaadin-button');
22
+ }
23
+
24
+ /**
25
+ * Override method inherited from `SlotController`
26
+ * to add listeners to default and custom node.
27
+ *
28
+ * @param {Node} node
29
+ * @protected
30
+ * @override
31
+ */
32
+ initNode(node) {
33
+ // Needed by Flow counterpart to apply i18n to custom button
34
+ if (node._isDefault) {
35
+ this.defaultNode = node;
36
+ }
37
+
38
+ node.addEventListener('touchend', (e) => {
39
+ this.host._onAddFilesTouchEnd(e);
40
+ });
41
+
42
+ node.addEventListener('click', (e) => {
43
+ this.host._onAddFilesClick(e);
44
+ });
45
+
46
+ this.host._addButton = node;
47
+ }
48
+ }
49
+
50
+ class DropLabelController extends SlotController {
51
+ constructor(host) {
52
+ super(host, 'drop-label', 'span');
53
+ }
54
+
55
+ /**
56
+ * Override method inherited from `SlotController`
57
+ * to add listeners to default and custom node.
58
+ *
59
+ * @param {Node} node
60
+ * @protected
61
+ * @override
62
+ */
63
+ initNode(node) {
64
+ // Needed by Flow counterpart to apply i18n to custom label
65
+ if (node._isDefault) {
66
+ this.defaultNode = node;
67
+ }
68
+ this.host._dropLabel = node;
69
+ }
70
+ }
71
+
16
72
  /**
17
73
  * `<vaadin-upload>` is a Web Component for uploading multiple files with drag and drop support.
18
74
  *
@@ -26,13 +82,10 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
26
82
  *
27
83
  * The following shadow DOM parts are available for styling:
28
84
  *
29
- * Part name | Description
30
- * ---|---
31
- * `primary-buttons` | Upload container
32
- * `upload-button` | Upload button
33
- * `drop-label` | Label for drop indicator
34
- * `drop-label-icon` | Icon for drop indicator
35
- * `file-list` | File list container
85
+ * Part name | Description
86
+ * -------------------|-------------------------------------
87
+ * `primary-buttons` | Upload container
88
+ * `drop-label` | Element wrapping drop label and icon
36
89
  *
37
90
  * The following state attributes are available for styling:
38
91
  *
@@ -59,10 +112,11 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
59
112
  * @fires {CustomEvent} upload-abort - Fired when upload abort is requested.
60
113
  *
61
114
  * @extends HTMLElement
115
+ * @mixes ControllerMixin
62
116
  * @mixes ThemableMixin
63
117
  * @mixes ElementMixin
64
118
  */
65
- class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
119
+ class Upload extends ElementMixin(ThemableMixin(ControllerMixin(PolymerElement))) {
66
120
  static get template() {
67
121
  return html`
68
122
  <style>
@@ -79,38 +133,16 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
79
133
  [hidden] {
80
134
  display: none !important;
81
135
  }
82
-
83
- [part='file-list'] {
84
- padding: 0;
85
- margin: 0;
86
- list-style-type: none;
87
- }
88
136
  </style>
89
137
 
90
138
  <div part="primary-buttons">
91
- <div id="addFiles" on-touchend="_onAddFilesTouchEnd" on-click="_onAddFilesClick">
92
- <slot name="add-button">
93
- <vaadin-button part="upload-button" id="addButton" disabled="[[maxFilesReached]]">
94
- [[_i18nPlural(maxFiles, i18n.addFiles, i18n.addFiles.*)]]
95
- </vaadin-button>
96
- </slot>
97
- </div>
139
+ <slot name="add-button"></slot>
98
140
  <div part="drop-label" hidden$="[[nodrop]]" id="dropLabelContainer" aria-hidden="true">
99
- <slot name="drop-label-icon">
100
- <div part="drop-label-icon"></div>
101
- </slot>
102
- <slot name="drop-label" id="dropLabel"> [[_i18nPlural(maxFiles, i18n.dropFiles, i18n.dropFiles.*)]]</slot>
141
+ <slot name="drop-label-icon"></slot>
142
+ <slot name="drop-label"></slot>
103
143
  </div>
104
144
  </div>
105
- <slot name="file-list">
106
- <ul id="fileList" part="file-list">
107
- <template is="dom-repeat" items="[[files]]" as="file">
108
- <li>
109
- <vaadin-upload-file file="[[file]]" i18n="[[i18n]]"></vaadin-upload-file>
110
- </li>
111
- </template>
112
- </ul>
113
- </slot>
145
+ <slot name="file-list"></slot>
114
146
  <slot></slot>
115
147
  <input
116
148
  type="file"
@@ -429,9 +461,37 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
429
461
  };
430
462
  },
431
463
  },
464
+
465
+ /** @private */
466
+ _addButton: {
467
+ type: Object,
468
+ },
469
+
470
+ /** @private */
471
+ _dropLabel: {
472
+ type: Object,
473
+ },
474
+
475
+ /** @private */
476
+ _fileList: {
477
+ type: Object,
478
+ },
479
+
480
+ /** @private */
481
+ _files: {
482
+ type: Array,
483
+ },
432
484
  };
433
485
  }
434
486
 
487
+ static get observers() {
488
+ return [
489
+ '__updateAddButton(_addButton, maxFiles, i18n, maxFilesReached)',
490
+ '__updateDropLabel(_dropLabel, maxFiles, i18n)',
491
+ '__updateFileList(_fileList, files, i18n)',
492
+ ];
493
+ }
494
+
435
495
  /** @protected */
436
496
  ready() {
437
497
  super.ready();
@@ -440,12 +500,27 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
440
500
  this.addEventListener('drop', this._onDrop.bind(this));
441
501
  this.addEventListener('file-retry', this._onFileRetry.bind(this));
442
502
  this.addEventListener('file-abort', this._onFileAbort.bind(this));
443
- this.addEventListener('file-remove', this._onFileRemove.bind(this));
444
503
  this.addEventListener('file-start', this._onFileStart.bind(this));
445
504
  this.addEventListener('file-reject', this._onFileReject.bind(this));
446
505
  this.addEventListener('upload-start', this._onUploadStart.bind(this));
447
506
  this.addEventListener('upload-success', this._onUploadSuccess.bind(this));
448
507
  this.addEventListener('upload-error', this._onUploadError.bind(this));
508
+
509
+ this._addButtonController = new AddButtonController(this);
510
+ this.addController(this._addButtonController);
511
+
512
+ this._dropLabelController = new DropLabelController(this);
513
+ this.addController(this._dropLabelController);
514
+
515
+ this.addController(
516
+ new SlotController(this, 'file-list', 'vaadin-upload-file-list', {
517
+ initializer: (list) => {
518
+ this._fileList = list;
519
+ },
520
+ }),
521
+ );
522
+
523
+ this.addController(new SlotController(this, 'drop-label-icon', 'vaadin-upload-icon'));
449
524
  }
450
525
 
451
526
  /** @private */
@@ -509,6 +584,34 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
509
584
  return maxFiles >= 0 && numFiles >= maxFiles;
510
585
  }
511
586
 
587
+ /** @private */
588
+ __updateAddButton(addButton, maxFiles, i18n, maxFilesReached) {
589
+ if (addButton) {
590
+ addButton.disabled = maxFilesReached;
591
+
592
+ // Only update text content for the default button element
593
+ if (addButton === this._addButtonController.defaultNode) {
594
+ addButton.textContent = this._i18nPlural(maxFiles, i18n.addFiles);
595
+ }
596
+ }
597
+ }
598
+
599
+ /** @private */
600
+ __updateDropLabel(dropLabel, maxFiles, i18n) {
601
+ // Only update text content for the default label element
602
+ if (dropLabel && dropLabel === this._dropLabelController.defaultNode) {
603
+ dropLabel.textContent = this._i18nPlural(maxFiles, i18n.dropFiles);
604
+ }
605
+ }
606
+
607
+ /** @private */
608
+ __updateFileList(list, files, i18n) {
609
+ if (list) {
610
+ list.items = [...files];
611
+ list.i18n = i18n;
612
+ }
613
+ }
614
+
512
615
  /** @private */
513
616
  _onDragover(event) {
514
617
  event.preventDefault();
@@ -576,11 +679,10 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
576
679
  *
577
680
  * @param {!UploadFile | !Array<!UploadFile>=} files - Files being uploaded. Defaults to all outstanding files
578
681
  */
579
- uploadFiles(files) {
682
+ uploadFiles(files = this.files) {
580
683
  if (files && !Array.isArray(files)) {
581
684
  files = [files];
582
685
  }
583
- files = files || this.files;
584
686
  files = files.filter((file) => !file.complete);
585
687
  Array.prototype.forEach.call(files, this._uploadFile.bind(this));
586
688
  }
@@ -615,7 +717,7 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
615
717
  this._setStatus(file, total, loaded, elapsed);
616
718
  stalledId = setTimeout(() => {
617
719
  file.status = this.i18n.uploading.status.stalled;
618
- this._notifyFileChanges(file);
720
+ this._renderFileList();
619
721
  }, 2000);
620
722
  } else {
621
723
  file.loadedStr = file.totalStr;
@@ -623,7 +725,7 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
623
725
  }
624
726
  }
625
727
 
626
- this._notifyFileChanges(file);
728
+ this._renderFileList();
627
729
  this.dispatchEvent(new CustomEvent('upload-progress', { detail: { file, xhr } }));
628
730
  };
629
731
 
@@ -633,7 +735,6 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
633
735
  clearTimeout(stalledId);
634
736
  file.indeterminate = file.uploading = false;
635
737
  if (file.abort) {
636
- this._notifyFileChanges(file);
637
738
  return;
638
739
  }
639
740
  file.status = '';
@@ -663,7 +764,7 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
663
764
  detail: { file, xhr },
664
765
  }),
665
766
  );
666
- this._notifyFileChanges(file);
767
+ this._renderFileList();
667
768
  }
668
769
  };
669
770
 
@@ -697,7 +798,7 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
697
798
  detail: { file, xhr },
698
799
  }),
699
800
  );
700
- this._notifyFileChanges(file);
801
+ this._renderFileList();
701
802
  };
702
803
 
703
804
  // Custom listener could modify the xhr just before sending it
@@ -739,16 +840,15 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
739
840
  if (file.xhr) {
740
841
  file.xhr.abort();
741
842
  }
742
- this._notifyFileChanges(file);
843
+ this._removeFile(file);
743
844
  }
744
845
  }
745
846
 
746
847
  /** @private */
747
- _notifyFileChanges(file) {
748
- const p = `files.${this.files.indexOf(file)}.`;
749
- Object.keys(file).forEach((i) => {
750
- this.notifyPath(p + i, file[i]);
751
- });
848
+ _renderFileList() {
849
+ if (this._fileList) {
850
+ this._fileList.requestContentUpdate();
851
+ }
752
852
  }
753
853
 
754
854
  /** @private */
@@ -779,11 +879,11 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
779
879
  );
780
880
  return;
781
881
  }
782
- const fileExt = file.name.match(/\.[^.]*$|$/)[0];
882
+ const fileExt = file.name.match(/\.[^.]*$|$/u)[0];
783
883
  // Escape regex operators common to mime types
784
- const escapedAccept = this.accept.replace(/[+.]/g, '\\$&');
884
+ const escapedAccept = this.accept.replace(/[+.]/gu, '\\$&');
785
885
  // Create accept regex that can match comma separated patterns, star (*) wildcards
786
- const re = new RegExp(`^(${escapedAccept.replace(/[, ]+/g, '|').replace(/\/\*/g, '/.*')})$`, 'i');
886
+ const re = new RegExp(`^(${escapedAccept.replace(/[, ]+/gu, '|').replace(/\/\*/gu, '/.*')})$`, 'iu');
787
887
  if (this.accept && !(re.test(file.type) || re.test(fileExt))) {
788
888
  this.dispatchEvent(
789
889
  new CustomEvent('file-reject', {
@@ -810,6 +910,14 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
810
910
  _removeFile(file) {
811
911
  if (this.files.indexOf(file) > -1) {
812
912
  this.files = this.files.filter((i) => i !== file);
913
+
914
+ this.dispatchEvent(
915
+ new CustomEvent('file-remove', {
916
+ detail: { file },
917
+ bubbles: true,
918
+ composed: true,
919
+ }),
920
+ );
813
921
  }
814
922
  }
815
923
 
@@ -851,11 +959,6 @@ class Upload extends ElementMixin(ThemableMixin(PolymerElement)) {
851
959
  this._abortFileUpload(event.detail.file);
852
960
  }
853
961
 
854
- /** @private */
855
- _onFileRemove(event) {
856
- this._removeFile(event.detail.file);
857
- }
858
-
859
962
  /** @private */
860
963
  _onFileReject(event) {
861
964
  announce(`${event.detail.file.name}: ${event.detail.file.error}`, { mode: 'alert' });
@@ -24,11 +24,6 @@ registerStyles(
24
24
  transition: background-color 0.6s, border-color 0.6s;
25
25
  }
26
26
 
27
- [part='primary-buttons'] > * {
28
- display: inline-block;
29
- white-space: nowrap;
30
- }
31
-
32
27
  [part='drop-label'] {
33
28
  display: inline-block;
34
29
  white-space: normal;
@@ -50,24 +45,32 @@ registerStyles(
50
45
  :host([max-files-reached]) [part='drop-label'] {
51
46
  color: var(--lumo-disabled-text-color);
52
47
  }
48
+ `,
49
+ { moduleId: 'lumo-upload' },
50
+ );
53
51
 
54
- [part='drop-label-icon'] {
55
- display: inline-block;
56
- }
57
-
58
- [part='drop-label-icon']::before {
52
+ registerStyles(
53
+ 'vaadin-upload-icon',
54
+ css`
55
+ :host::before {
59
56
  content: var(--lumo-icons-upload);
60
57
  font-family: lumo-icons;
61
58
  font-size: var(--lumo-icon-size-m);
62
59
  line-height: 1;
63
60
  vertical-align: -0.25em;
64
61
  }
62
+ `,
63
+ { moduleId: 'lumo-upload-icon' },
64
+ );
65
65
 
66
- [part='file-list'] > *:not(:first-child) > * {
66
+ registerStyles(
67
+ 'vaadin-upload-file-list',
68
+ css`
69
+ ::slotted(li:not(:first-of-type)) {
67
70
  border-top: 1px solid var(--lumo-contrast-10pct);
68
71
  }
69
72
  `,
70
- { moduleId: 'lumo-upload' },
73
+ { moduleId: 'lumo-upload-file-list' },
71
74
  );
72
75
 
73
76
  const uploadFile = css`
@@ -172,16 +175,11 @@ const uploadFile = css`
172
175
  color: var(--lumo-error-text-color);
173
176
  }
174
177
 
175
- [part='progress'] {
178
+ ::slotted([slot='progress']) {
176
179
  width: auto;
177
180
  margin-left: calc(var(--lumo-icon-size-m) + var(--lumo-space-xs));
178
181
  margin-right: calc(var(--lumo-icon-size-m) + var(--lumo-space-xs));
179
182
  }
180
-
181
- [part='progress'][complete],
182
- [part='progress'][error] {
183
- display: none;
184
- }
185
183
  `;
186
184
 
187
185
  registerStyles('vaadin-upload-file', [fieldButton, uploadFile], { moduleId: 'lumo-upload-file' });
@@ -23,14 +23,7 @@ registerStyles(
23
23
  align-items: baseline;
24
24
  }
25
25
 
26
- /* TODO(jouni): unsupported selector (not sure why there's #addFiles element wrapping the upload button) */
27
- [part='primary-buttons'] > * {
28
- display: block;
29
- flex-grow: 1;
30
- }
31
-
32
- [part='upload-button'] {
33
- display: block;
26
+ ::slotted([slot='add-button']) {
34
27
  margin: 0 -8px;
35
28
  }
36
29
 
@@ -56,18 +49,6 @@ registerStyles(
56
49
  color: var(--material-disabled-text-color);
57
50
  }
58
51
 
59
- [part='drop-label-icon'] {
60
- display: inline-block;
61
- margin-right: 8px;
62
- }
63
-
64
- [part='drop-label-icon']::before {
65
- content: var(--material-icons-upload);
66
- font-family: material-icons;
67
- font-size: var(--material-icon-font-size);
68
- line-height: 1;
69
- }
70
-
71
52
  /* Ripple */
72
53
 
73
54
  :host::before {
@@ -96,6 +77,23 @@ registerStyles(
96
77
  { moduleId: 'material-upload' },
97
78
  );
98
79
 
80
+ registerStyles(
81
+ 'vaadin-upload-icon',
82
+ css`
83
+ :host {
84
+ margin-right: 8px;
85
+ }
86
+
87
+ :host::before {
88
+ content: var(--material-icons-upload);
89
+ font-family: material-icons;
90
+ font-size: var(--material-icon-font-size);
91
+ line-height: 1;
92
+ }
93
+ `,
94
+ { moduleId: 'material-upload-icon' },
95
+ );
96
+
99
97
  registerStyles(
100
98
  'vaadin-upload-file',
101
99
  css`
@@ -237,15 +235,10 @@ registerStyles(
237
235
  color: var(--material-error-text-color);
238
236
  }
239
237
 
240
- [part='progress'] {
238
+ ::slotted([slot='progress']) {
241
239
  width: auto;
242
240
  margin-left: 28px;
243
241
  }
244
-
245
- [part='progress'][complete],
246
- [part='progress'][error] {
247
- display: none;
248
- }
249
242
  `,
250
243
  { moduleId: 'material-upload-file' },
251
244
  );