Skip to content

Commit 35c9e51

Browse files
committed
[Dropzone] Improve display with multiple files
1 parent 61819ae commit 35c9e51

File tree

9 files changed

+217
-133
lines changed

9 files changed

+217
-133
lines changed

src/Dropzone/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.24
4+
5+
- Preview works with muliple files
6+
37
## 2.20
48

59
- Enable file replacement via "drag-and-drop"

src/Dropzone/assets/dist/controller.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default class extends Controller {
1212
disconnect(): void;
1313
clear(): void;
1414
onInputChange(event: any): void;
15-
_populateImagePreview(file: Blob): void;
15+
_populateImagePreview(file: Blob, imagePreviewElement: HTMLElement): void;
1616
onDragEnter(): void;
1717
onDragLeave(event: any): void;
1818
private dispatchEvent;

src/Dropzone/assets/dist/controller.js

+35-14
Original file line numberDiff line numberDiff line change
@@ -25,49 +25,70 @@ class default_1 extends Controller {
2525
this.inputTarget.value = '';
2626
this.inputTarget.style.display = 'block';
2727
this.placeholderTarget.style.display = 'block';
28+
this.previewTarget.innerHTML = '';
2829
this.previewTarget.style.display = 'none';
29-
this.previewImageTarget.style.display = 'none';
30-
this.previewImageTarget.style.backgroundImage = 'none';
31-
this.previewFilenameTarget.textContent = '';
30+
this.element.classList.remove('dropzone-on-drag-enter');
3231
this.dispatchEvent('clear');
3332
}
3433
onInputChange(event) {
35-
const file = event.target.files[0];
36-
if (typeof file === 'undefined') {
34+
const files = event.target.files;
35+
if (files.length === 0) {
36+
this.previewClearButtonTarget.style.display = 'none';
3737
return;
3838
}
3939
this.inputTarget.style.display = 'none';
4040
this.placeholderTarget.style.display = 'none';
41-
this.previewFilenameTarget.textContent = file.name;
42-
this.previewTarget.style.display = 'flex';
43-
this.previewImageTarget.style.display = 'none';
44-
if (file.type && file.type.indexOf('image') !== -1) {
45-
this._populateImagePreview(file);
41+
this.previewTarget.innerHTML = '';
42+
for (const file of files) {
43+
const filePreviewContainer = document.createElement('div');
44+
filePreviewContainer.classList.add('dropzone-preview-file');
45+
const fileNameElement = document.createElement('span');
46+
fileNameElement.textContent = file.name;
47+
filePreviewContainer.appendChild(fileNameElement);
48+
if (file.type) {
49+
const imagePreviewElement = document.createElement('div');
50+
if (file.type.indexOf('image') !== -1) {
51+
imagePreviewElement.classList.add('dropzone-preview-image');
52+
this._populateImagePreview(file, imagePreviewElement);
53+
}
54+
else {
55+
const noPreviewSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M14 11a3 3 0 0 1-3-3V4H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8zm-2-3a2 2 0 0 0 2 2h3.59L12 4.41zM7 3h5l7 7v9a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3"/></svg>';
56+
imagePreviewElement.innerHTML = noPreviewSvg;
57+
imagePreviewElement.classList.add('dropzone-no-preview');
58+
}
59+
filePreviewContainer.appendChild(imagePreviewElement);
60+
}
61+
this.previewTarget.appendChild(filePreviewContainer);
62+
this.dispatchEvent('change', file);
4663
}
47-
this.dispatchEvent('change', file);
64+
this.previewTarget.style.display = 'grid';
4865
}
49-
_populateImagePreview(file) {
66+
_populateImagePreview(file, imagePreviewElement) {
5067
if (typeof FileReader === 'undefined') {
5168
return;
5269
}
5370
const reader = new FileReader();
5471
reader.addEventListener('load', (event) => {
55-
this.previewImageTarget.style.display = 'block';
56-
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
72+
imagePreviewElement.style.backgroundImage = `url("${event.target.result}")`;
73+
imagePreviewElement.style.display = 'block';
5774
});
5875
reader.readAsDataURL(file);
5976
}
6077
onDragEnter() {
6178
this.inputTarget.style.display = 'block';
6279
this.placeholderTarget.style.display = 'block';
6380
this.previewTarget.style.display = 'none';
81+
this.element.classList.add('dropzone-on-drag-enter');
82+
this.element.classList.remove('dropzone-on-drag-leave');
6483
}
6584
onDragLeave(event) {
6685
event.preventDefault();
6786
if (!this.element.contains(event.relatedTarget)) {
6887
this.inputTarget.style.display = 'none';
6988
this.placeholderTarget.style.display = 'none';
7089
this.previewTarget.style.display = 'block';
90+
this.element.classList.remove('dropzone-on-drag-enter');
91+
this.element.classList.add('dropzone-on-drag-leave');
7192
}
7293
}
7394
dispatchEvent(name, payload = {}) {

src/Dropzone/assets/dist/style.min.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Dropzone/assets/src/controller.ts

+48-17
Original file line numberDiff line numberDiff line change
@@ -56,48 +56,75 @@ export default class extends Controller {
5656
this.inputTarget.value = '';
5757
this.inputTarget.style.display = 'block';
5858
this.placeholderTarget.style.display = 'block';
59+
this.previewTarget.innerHTML = '';
5960
this.previewTarget.style.display = 'none';
60-
this.previewImageTarget.style.display = 'none';
61-
this.previewImageTarget.style.backgroundImage = 'none';
62-
this.previewFilenameTarget.textContent = '';
61+
this.element.classList.remove('dropzone-on-drag-enter');
6362

6463
this.dispatchEvent('clear');
6564
}
6665

6766
onInputChange(event: any) {
68-
const file = event.target.files[0];
69-
if (typeof file === 'undefined') {
67+
const files = event.target.files;
68+
if (files.length === 0) {
69+
this.previewClearButtonTarget.style.display = 'none';
7070
return;
7171
}
7272

7373
// Hide the input and placeholder
7474
this.inputTarget.style.display = 'none';
7575
this.placeholderTarget.style.display = 'none';
7676

77-
// Show the filename in preview
78-
this.previewFilenameTarget.textContent = file.name;
79-
this.previewTarget.style.display = 'flex';
77+
// Clear previous previews
78+
this.previewTarget.innerHTML = '';
8079

81-
// If the file is an image, load it and display it as preview
82-
this.previewImageTarget.style.display = 'none';
83-
if (file.type && file.type.indexOf('image') !== -1) {
84-
this._populateImagePreview(file);
80+
for (const file of files) {
81+
// Create a container for each file preview
82+
const filePreviewContainer = document.createElement('div');
83+
filePreviewContainer.classList.add('dropzone-preview-file');
84+
85+
// Create a filename preview element
86+
const fileNameElement = document.createElement('span');
87+
fileNameElement.textContent = file.name;
88+
filePreviewContainer.appendChild(fileNameElement);
89+
90+
// Create an image preview element if the file is an image, else a default svg file icon
91+
if (file.type) {
92+
const imagePreviewElement = document.createElement('div');
93+
94+
if (file.type.indexOf('image') !== -1) {
95+
imagePreviewElement.classList.add('dropzone-preview-image');
96+
this._populateImagePreview(file, imagePreviewElement);
97+
} else {
98+
const noPreviewSvg =
99+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M14 11a3 3 0 0 1-3-3V4H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8zm-2-3a2 2 0 0 0 2 2h3.59L12 4.41zM7 3h5l7 7v9a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3"/></svg>';
100+
imagePreviewElement.innerHTML = noPreviewSvg;
101+
102+
imagePreviewElement.classList.add('dropzone-no-preview');
103+
}
104+
105+
filePreviewContainer.appendChild(imagePreviewElement);
106+
}
107+
108+
// Append the file preview container to the main preview target
109+
this.previewTarget.appendChild(filePreviewContainer);
110+
111+
this.dispatchEvent('change', file);
85112
}
86113

87-
this.dispatchEvent('change', file);
114+
// Show the preview container
115+
this.previewTarget.style.display = 'grid';
88116
}
89117

90-
_populateImagePreview(file: Blob) {
118+
_populateImagePreview(file: Blob, imagePreviewElement: HTMLElement) {
91119
if (typeof FileReader === 'undefined') {
92120
// FileReader API not available, skip
93121
return;
94122
}
95123

96124
const reader = new FileReader();
97-
98125
reader.addEventListener('load', (event: any) => {
99-
this.previewImageTarget.style.display = 'block';
100-
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
126+
imagePreviewElement.style.backgroundImage = `url("${event.target.result}")`;
127+
imagePreviewElement.style.display = 'block';
101128
});
102129

103130
reader.readAsDataURL(file);
@@ -107,6 +134,8 @@ export default class extends Controller {
107134
this.inputTarget.style.display = 'block';
108135
this.placeholderTarget.style.display = 'block';
109136
this.previewTarget.style.display = 'none';
137+
this.element.classList.add('dropzone-on-drag-enter');
138+
this.element.classList.remove('dropzone-on-drag-leave');
110139
}
111140

112141
onDragLeave(event: any) {
@@ -117,6 +146,8 @@ export default class extends Controller {
117146
this.inputTarget.style.display = 'none';
118147
this.placeholderTarget.style.display = 'none';
119148
this.previewTarget.style.display = 'block';
149+
this.element.classList.remove('dropzone-on-drag-enter');
150+
this.element.classList.add('dropzone-on-drag-leave');
120151
}
121152
}
122153

src/Dropzone/assets/src/style.css

+101-52
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,121 @@
1+
:root {
2+
--dropzone-background: white;
3+
--dropzone-background-hover: #dddddd;
4+
--dropzone-border-color: #aaaaaa;
5+
--dropzone-border-color-hover: #666666;
6+
--dropzone-text-color: ##333333;
7+
8+
--dropzone-spacing: 8px;
9+
--dropzone-radius: 8px;
10+
11+
--dropzone-width: cacl(100% - 2 * var(--dropzone-spacing));
12+
--dropzone-height: 120px;
13+
--dropzone-image-size: 100px;
14+
}
15+
116
.dropzone-container {
2-
position: relative;
3-
display: flex;
4-
min-height: 100px;
5-
border: 2px dashed #bbb;
6-
align-items: center;
7-
padding: 20px 10px;
17+
position: relative;
18+
display: flex;
19+
flex-wrap: wrap;
20+
align-items: center;
21+
min-height: var(--dropzone-height);
22+
width: var(--dropzone-width);
23+
border: 2px dashed var(--dropzone-border-color);
24+
padding: var(--dropzone-spacing);
25+
border-radius: var(--dropzone-radius);
26+
color: var(--dropzone-text-color);
27+
background-color: var(--dropzone-background);
28+
}
29+
30+
.dropzone-container:has(.dropzone-preview:empty) .dropzone-preview-button {
31+
display: none;
32+
}
33+
34+
.dropzone-container:hover,
35+
.dropzone-on-drag-enter {
36+
background-color: var(--dropzone-background-hover);
37+
transition: 0.3s;
838
}
939

1040
.dropzone-input {
11-
position: absolute;
12-
display: block;
13-
top: 0;
14-
left: 0;
15-
width: 100%;
16-
height: 100%;
17-
opacity: 0;
18-
cursor: pointer;
19-
z-index: 1;
41+
position: absolute;
42+
display: block;
43+
top: 0;
44+
left: 0;
45+
width: 100%;
46+
height: 100%;
47+
opacity: 0;
48+
cursor: pointer;
49+
z-index: 1;
2050
}
2151

2252
.dropzone-preview {
23-
display: flex;
24-
align-items: center;
25-
max-width: 100%;
53+
display: grid;
54+
gap: var(--dropzone-spacing);
55+
grid-template-columns: repeat(auto-fill, var(--dropzone-image-size));
56+
grid-template-rows: auto;
57+
place-items: stretch;
58+
width: 100%;
59+
height: 100%;
60+
}
61+
62+
.dropzone-preview-file {
63+
display: flex;
64+
flex-direction: column-reverse;
65+
align-items: center;
66+
justify-content: start;
67+
word-wrap: anywhere;
68+
}
69+
70+
.dropzone-preview-file:hover {
71+
filter: brightness(110%);
2672
}
2773

2874
.dropzone-preview-image {
29-
flex-basis: 0;
30-
min-width: 50px;
31-
max-width: 50px;
32-
height: 50px;
33-
margin-right: 10px;
34-
background-size: contain;
35-
background-position: 50% 50%;
36-
background-repeat: no-repeat;
75+
margin-bottom: var(--dropzone-spacing);
76+
width: var(--dropzone-image-size);
77+
aspect-ratio: 1;
78+
background-size: cover;
79+
background-position: 50% 50%;
80+
background-repeat: no-repeat;
81+
border-radius: var(--dropzone-radius);
82+
box-shadow: 0 0 8px var(--dropzone-background-hover);
3783
}
3884

39-
.dropzone-preview-filename {
40-
word-wrap: anywhere;
85+
.dropzone-no-preview {
86+
margin-bottom: var(--dropzone-spacing);
87+
height: var(--dropzone-image-size);
4188
}
4289

43-
.dropzone-preview-button {
44-
position: absolute;
45-
top: 0;
46-
right: 0;
47-
z-index: 1;
48-
border: none;
49-
margin: 0;
50-
padding: 0;
51-
width: auto;
52-
overflow: visible;
53-
background: transparent;
54-
color: inherit;
55-
font: inherit;
56-
line-height: normal;
57-
-webkit-font-smoothing: inherit;
58-
-moz-osx-font-smoothing: inherit;
59-
-webkit-appearance: none;
90+
.dropzone-no-preview svg {
91+
width: 100%;
6092
}
6193

62-
.dropzone-preview-button::before {
63-
content: '×';
64-
padding: 3px 7px;
65-
cursor: pointer;
94+
95+
.dropzone-preview-file span {
96+
font-weight: 300;
97+
font-size: 0.9em;
98+
}
99+
100+
.dropzone-preview-button {
101+
position: absolute;
102+
top: var(--dropzone-spacing);
103+
right: var(--dropzone-spacing);
104+
z-index: 1;
105+
border: none;
106+
margin: 0;
107+
background: none;
108+
display: grid;
109+
place-items: center;
110+
cursor: pointer;
66111
}
67112

68113
.dropzone-placeholder {
69-
flex-grow: 1;
70-
text-align: center;
71-
color: #999;
114+
flex-grow: 1;
115+
text-align: center;
116+
}
117+
118+
.dropzone-on-drag-leave {
119+
background-color: var(--dropzone-background);
120+
transition: 0.3s;
72121
}

0 commit comments

Comments
 (0)