@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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
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": "
|
|
31
|
+
"gitHead": "bb98f6a98cd4fc1034d9fb05fe35cf6379cda54f"
|
|
32
32
|
}
|