@dile/ui 3.15.0 → 3.16.1

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.
@@ -109,4 +109,106 @@ describe('dile-drop-file', () => {
109
109
  expect(eventDetail).toBeTruthy();
110
110
  expect(eventDetail.file).toBeNull();
111
111
  });
112
+
113
+ describe('multiple mode', () => {
114
+ function createFile(name, content = 'content') {
115
+ return new File([content], name, { lastModified: 1 });
116
+ }
117
+
118
+ function dropFiles(el, files) {
119
+ const dataTransfer = new DataTransfer();
120
+ files.forEach(file => dataTransfer.items.add(file));
121
+ const dropZone = el.shadowRoot.querySelector('#dropZone');
122
+ dropZone.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true, cancelable: true }));
123
+ return el.updateComplete;
124
+ }
125
+
126
+ it('renders the file input with the multiple attribute', async () => {
127
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
128
+ expect(el.shadowRoot.querySelector('input[type="file"]').hasAttribute('multiple')).toBe(true);
129
+ });
130
+
131
+ it('does not render the multiple attribute by default', async () => {
132
+ const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
133
+ expect(el.shadowRoot.querySelector('input[type="file"]').hasAttribute('multiple')).toBe(false);
134
+ });
135
+
136
+ it('accepts several dropped files and renders a preview for each one', async () => {
137
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
138
+ let eventDetail = null;
139
+ el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
140
+
141
+ await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
142
+
143
+ expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(2);
144
+ expect(el.value.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
145
+ expect(el.getFiles().length).toBe(2);
146
+ expect(el.shadowRoot.querySelector('#fileInput').files.length).toBe(2);
147
+ expect(eventDetail.files.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
148
+ expect(eventDetail.file.name).toBe('a.txt');
149
+ });
150
+
151
+ it('accumulates files between selections without duplicates', async () => {
152
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
153
+ await dropFiles(el, [createFile('a.txt')]);
154
+ await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
155
+
156
+ expect(el.value.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
157
+ expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(2);
158
+ });
159
+
160
+ it('removes a single file with removeFile', async () => {
161
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
162
+ await dropFiles(el, [createFile('a.txt'), createFile('b.txt'), createFile('c.txt')]);
163
+
164
+ el.removeFile(1);
165
+ await el.updateComplete;
166
+
167
+ expect(el.value.map(file => file.name)).toEqual(['a.txt', 'c.txt']);
168
+ expect(el.shadowRoot.querySelector('#fileInput').files.length).toBe(2);
169
+ });
170
+
171
+ it('removes a file when its preview clear link is clicked', async () => {
172
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
173
+ await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
174
+
175
+ const preview = el.shadowRoot.querySelectorAll('dile-file-preview')[0];
176
+ await preview.updateComplete;
177
+ preview.shadowRoot.querySelector('a').click();
178
+ await el.updateComplete;
179
+
180
+ expect(el.value.map(file => file.name)).toEqual(['b.txt']);
181
+ });
182
+
183
+ it('discards files with invalid extensions and keeps the valid ones', async () => {
184
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
185
+ el.allowedExtensions = ['pdf'];
186
+ await dropFiles(el, [createFile('a.pdf'), createFile('b.exe')]);
187
+
188
+ expect(el.value.map(file => file.name)).toEqual(['a.pdf']);
189
+ expect(el.errored).toBe(true);
190
+ expect(el.shadowRoot.querySelector('.message.errored-msg')).toBeTruthy();
191
+ });
192
+
193
+ it('clears all the files', async () => {
194
+ const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
195
+ await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
196
+
197
+ el.clear();
198
+ await el.updateComplete;
199
+
200
+ expect(el.value).toEqual([]);
201
+ expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(0);
202
+ });
203
+
204
+ it('sets all the files as form value', async () => {
205
+ document.body.innerHTML = '<form><dile-drop-file name="docs" multiple></dile-drop-file></form>';
206
+ const el = document.querySelector('dile-drop-file');
207
+ await el.updateComplete;
208
+ await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
209
+
210
+ const formData = new FormData(document.querySelector('form'));
211
+ expect(formData.getAll('docs').map(file => file.name)).toEqual(['a.txt', 'b.txt']);
212
+ });
213
+ });
112
214
  });
@@ -64,6 +64,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
64
64
  name: { type: String, reflect: true },
65
65
  accept: { type: String },
66
66
  capture: { type: String },
67
+ multiple: { type: Boolean, reflect: true },
68
+ files: { state: true },
67
69
  };
68
70
  }
69
71
 
@@ -83,6 +85,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
83
85
  this.allowedExtensions = [];
84
86
  this.accept = '';
85
87
  this.capture = '';
88
+ this.multiple = false;
89
+ this.files = [];
86
90
  this.internals = this.attachInternals();
87
91
  }
88
92
 
@@ -109,12 +113,23 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
109
113
  ${this.dropLabelTemplate}
110
114
  </div>
111
115
  ${this.messageTemplate}
112
- <input type="file" id="fileInput" @change=${this._handleFileInput} accept=${ifDefined(this.accept || undefined)} capture=${ifDefined(this.capture || undefined)}>
113
- <dile-file-preview
114
- fileName="${this.fileName}"
115
- selectedFileLabel="${this.selectedFileLabel}"
116
- @dile-file-clear=${this.clear}
117
- ></dile-file-preview>
116
+ <input type="file" id="fileInput" ?multiple=${this.multiple} @change=${this._handleFileInput} accept=${ifDefined(this.accept || undefined)} capture=${ifDefined(this.capture || undefined)}>
117
+ ${this.multiple
118
+ ? this.files.map((file, index) => html`
119
+ <dile-file-preview
120
+ fileName="${file.name}"
121
+ selectedFileLabel="${this.selectedFileLabel}"
122
+ @dile-file-clear=${() => this.removeFile(index)}
123
+ ></dile-file-preview>
124
+ `)
125
+ : html`
126
+ <dile-file-preview
127
+ fileName="${this.fileName}"
128
+ selectedFileLabel="${this.selectedFileLabel}"
129
+ @dile-file-clear=${this.clear}
130
+ ></dile-file-preview>
131
+ `
132
+ }
118
133
  `;
119
134
  }
120
135
 
@@ -167,6 +182,10 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
167
182
  }
168
183
 
169
184
  _processFile(files) {
185
+ if (this.multiple) {
186
+ this._processMultipleFiles(files);
187
+ return;
188
+ }
170
189
  this.internals.setFormValue(files.length > 0 ? files[0] : null);
171
190
  if (files.length > 0) {
172
191
  this.fileName = files[0].name;
@@ -183,6 +202,56 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
183
202
  }
184
203
  }
185
204
 
205
+ _processMultipleFiles(files) {
206
+ const incoming = Array.from(files);
207
+ if (incoming.length === 0) {
208
+ // Restore the input files, as a canceled dialog empties it
209
+ this._syncFileInput();
210
+ return;
211
+ }
212
+ const validFiles = incoming.filter(file => this._isValidExtension(file.name));
213
+ const newFiles = validFiles.filter(file => !this.files.some(current => this._isSameFile(current, file)));
214
+ this.files = [...this.files, ...newFiles];
215
+ if (validFiles.length < incoming.length) {
216
+ this.message = `${this.extensionErrorMessage} ${this.allowedExtensions.join(', ')}`;
217
+ this.errored = true;
218
+ } else {
219
+ this.message = this.firstMessage || '';
220
+ this.errored = false;
221
+ }
222
+ this._syncFiles();
223
+ }
224
+
225
+ _isSameFile(a, b) {
226
+ return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
227
+ }
228
+
229
+ _syncFileInput() {
230
+ if (!this.fileInput) return;
231
+ const dataTransfer = new DataTransfer();
232
+ this.files.forEach(file => dataTransfer.items.add(file));
233
+ this.fileInput.files = dataTransfer.files;
234
+ }
235
+
236
+ _syncFiles() {
237
+ this._syncFileInput();
238
+ if (this.files.length > 0) {
239
+ const formData = new FormData();
240
+ this.files.forEach(file => formData.append(this.name || 'files', file));
241
+ this.internals.setFormValue(formData);
242
+ } else {
243
+ this.internals.setFormValue(null);
244
+ }
245
+ this.emmitChange();
246
+ this._emitDropFileChanged(this.files[0] || null);
247
+ }
248
+
249
+ removeFile(index) {
250
+ if (index < 0 || index >= this.files.length) return;
251
+ this.files = this.files.filter((_, i) => i !== index);
252
+ this._syncFiles();
253
+ }
254
+
186
255
  _isValidExtension(fileName) {
187
256
  if(this.allowedExtensions.length === 0) {
188
257
  return true;
@@ -216,7 +285,8 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
216
285
  bubbles: true,
217
286
  composed: true,
218
287
  detail: {
219
- file: file
288
+ file: file,
289
+ files: this.multiple ? [...this.files] : (file ? [file] : []),
220
290
  }
221
291
  }));
222
292
  }
@@ -228,16 +298,23 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
228
298
  clear() {
229
299
  this.fileInput.value = "";
230
300
  this.fileName = "";
301
+ this.files = [];
231
302
  this.internals.setFormValue(null);
232
303
  this._emitDropFileChanged(null);
233
304
  this.emmitChange();
234
305
  }
235
306
 
236
307
  getFiles() {
308
+ if (this.multiple) {
309
+ return [...this.files];
310
+ }
237
311
  return this.fileInput.files;
238
312
  }
239
313
 
240
314
  get value() {
315
+ if (this.multiple) {
316
+ return [...this.files];
317
+ }
241
318
  if(this.fileInput) {
242
319
  return this.fileInput.files[0];
243
320
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dile/ui",
3
- "version": "3.15.0",
3
+ "version": "3.16.1",
4
4
  "description": "UI Core components from dile-components.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -28,5 +28,5 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
- "gitHead": "5ef91e1892e270fade848c64082184fa3fbddb53"
31
+ "gitHead": "bb98f6a98cd4fc1034d9fb05fe35cf6379cda54f"
32
32
  }