Skip to content

Commit 8444bbe

Browse files
committed
[Dropzone] Improve display with multiple files
1 parent 61819ae commit 8444bbe

File tree

9 files changed

+194
-115
lines changed

9 files changed

+194
-115
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

+30-14
Original file line numberDiff line numberDiff line change
@@ -25,49 +25,65 @@ 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 = '';
3230
this.dispatchEvent('clear');
3331
}
3432
onInputChange(event) {
35-
const file = event.target.files[0];
36-
if (typeof file === 'undefined') {
33+
const files = event.target.files;
34+
if (files.length === 0) {
35+
this.previewClearButtonTarget.style.display = 'none';
3736
return;
3837
}
3938
this.inputTarget.style.display = 'none';
4039
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);
40+
this.previewTarget.innerHTML = '';
41+
for (const file of files) {
42+
const filePreviewContainer = document.createElement('div');
43+
filePreviewContainer.classList.add('dropzone-preview-file');
44+
const fileNameElement = document.createElement('span');
45+
fileNameElement.textContent = file.name;
46+
filePreviewContainer.appendChild(fileNameElement);
47+
if (file.type) {
48+
const imagePreviewElement = document.createElement('div');
49+
if (file.type.indexOf('image') !== -1) {
50+
imagePreviewElement.classList.add('dropzone-preview-image');
51+
this._populateImagePreview(file, imagePreviewElement);
52+
}
53+
else {
54+
imagePreviewElement.classList.add('dropzone-preview-svg');
55+
}
56+
filePreviewContainer.appendChild(imagePreviewElement);
57+
}
58+
this.previewTarget.appendChild(filePreviewContainer);
59+
this.dispatchEvent('change', file);
4660
}
47-
this.dispatchEvent('change', file);
61+
this.previewTarget.style.display = 'grid';
4862
}
49-
_populateImagePreview(file) {
63+
_populateImagePreview(file, imagePreviewElement) {
5064
if (typeof FileReader === 'undefined') {
5165
return;
5266
}
5367
const reader = new FileReader();
5468
reader.addEventListener('load', (event) => {
55-
this.previewImageTarget.style.display = 'block';
56-
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
69+
imagePreviewElement.style.backgroundImage = `url("${event.target.result}")`;
70+
imagePreviewElement.style.display = 'block';
5771
});
5872
reader.readAsDataURL(file);
5973
}
6074
onDragEnter() {
6175
this.inputTarget.style.display = 'block';
6276
this.placeholderTarget.style.display = 'block';
6377
this.previewTarget.style.display = 'none';
78+
this.element.classList.add('dropzone-on-drag-enter');
6479
}
6580
onDragLeave(event) {
6681
event.preventDefault();
6782
if (!this.element.contains(event.relatedTarget)) {
6883
this.inputTarget.style.display = 'none';
6984
this.placeholderTarget.style.display = 'none';
7085
this.previewTarget.style.display = 'block';
86+
this.element.classList.add('dropzone-on-drag-leave');
7187
}
7288
}
7389
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

+40-16
Original file line numberDiff line numberDiff line change
@@ -56,38 +56,60 @@ 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 = '';
6361

6462
this.dispatchEvent('clear');
6563
}
6664

6765
onInputChange(event: any) {
68-
const file = event.target.files[0];
69-
if (typeof file === 'undefined') {
66+
const files = event.target.files;
67+
if (files.length === 0) {
68+
this.previewClearButtonTarget.style.display = 'none';
7069
return;
7170
}
7271

7372
// Hide the input and placeholder
7473
this.inputTarget.style.display = 'none';
7574
this.placeholderTarget.style.display = 'none';
7675

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

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);
79+
for (const file of files) {
80+
// Create a container for each file preview
81+
const filePreviewContainer = document.createElement('div');
82+
filePreviewContainer.classList.add('dropzone-preview-file');
83+
84+
// Create a filename preview element
85+
const fileNameElement = document.createElement('span');
86+
fileNameElement.textContent = file.name;
87+
filePreviewContainer.appendChild(fileNameElement);
88+
89+
// Create an image preview element if the file is an image, else a default svg file icon
90+
if (file.type) {
91+
const imagePreviewElement = document.createElement('div');
92+
if (file.type.indexOf('image') !== -1) {
93+
imagePreviewElement.classList.add('dropzone-preview-image');
94+
this._populateImagePreview(file, imagePreviewElement);
95+
} else {
96+
imagePreviewElement.classList.add('dropzone-preview-svg');
97+
}
98+
99+
filePreviewContainer.appendChild(imagePreviewElement);
100+
}
101+
102+
// Append the file preview container to the main preview target
103+
this.previewTarget.appendChild(filePreviewContainer);
104+
105+
this.dispatchEvent('change', file);
85106
}
86107

87-
this.dispatchEvent('change', file);
108+
// Show the preview container
109+
this.previewTarget.style.display = 'grid';
88110
}
89111

90-
_populateImagePreview(file: Blob) {
112+
_populateImagePreview(file: Blob, imagePreviewElement: HTMLElement) {
91113
if (typeof FileReader === 'undefined') {
92114
// FileReader API not available, skip
93115
return;
@@ -96,8 +118,8 @@ export default class extends Controller {
96118
const reader = new FileReader();
97119

98120
reader.addEventListener('load', (event: any) => {
99-
this.previewImageTarget.style.display = 'block';
100-
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
121+
imagePreviewElement.style.backgroundImage = `url("${event.target.result}")`;
122+
imagePreviewElement.style.display = 'block';
101123
});
102124

103125
reader.readAsDataURL(file);
@@ -107,6 +129,7 @@ export default class extends Controller {
107129
this.inputTarget.style.display = 'block';
108130
this.placeholderTarget.style.display = 'block';
109131
this.previewTarget.style.display = 'none';
132+
this.element.classList.add('dropzone-on-drag-enter');
110133
}
111134

112135
onDragLeave(event: any) {
@@ -117,6 +140,7 @@ export default class extends Controller {
117140
this.inputTarget.style.display = 'none';
118141
this.placeholderTarget.style.display = 'none';
119142
this.previewTarget.style.display = 'block';
143+
this.element.classList.add('dropzone-on-drag-leave');
120144
}
121145
}
122146

src/Dropzone/assets/src/style.css

+110-52
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,130 @@
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+
--dropzone-file-svg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M10 3v4a1 1 0 0 1-1 1H5m14-4v16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7.914a1 1 0 0 1 .293-.707l3.914-3.914A1 1 0 0 1 9.914 3H18a1 1 0 0 1 1 1Z"/></svg>');
16+
--dropzone-close-svg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 9l-6 6m0-6l6 6m6-3a9 9 0 1 1-18 0a9 9 0 0 1 18 0"/></svg>');
17+
}
18+
119
.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;
20+
position: relative;
21+
display: flex;
22+
flex-wrap: wrap;
23+
align-items: center;
24+
min-height: var(--dropzone-height);
25+
width: var(--dropzone-width);
26+
border: 2px dashed var(--dropzone-border-color);
27+
padding: var(--dropzone-spacing);
28+
border-radius: var(--dropzone-radius);
29+
color: var(--dropzone-text-color);
30+
background-color: var(--dropzone-background);
31+
}
32+
33+
.dropzone-container:has(.dropzone-preview:empty) {
34+
background-color: var(--dropzone-background);
35+
}
36+
37+
.dropzone-container:has(.dropzone-preview:empty) .dropzone-preview-button {
38+
display: none;
39+
}
40+
41+
.dropzone-container:hover,
42+
.dropzone-on-drag-enter {
43+
background-color: var(--dropzone-background-hover);
44+
transition: 0.3s;
845
}
946

1047
.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;
48+
position: absolute;
49+
display: block;
50+
top: 0;
51+
left: 0;
52+
width: 100%;
53+
height: 100%;
54+
opacity: 0;
55+
cursor: pointer;
56+
z-index: 1;
2057
}
2158

2259
.dropzone-preview {
23-
display: flex;
24-
align-items: center;
25-
max-width: 100%;
60+
display: grid;
61+
gap: var(--dropzone-spacing);
62+
grid-template-columns: repeat(auto-fill, var(--dropzone-image-size));
63+
grid-template-rows: auto;
64+
place-items: stretch;
65+
width: 100%;
66+
height: 100%;
67+
}
68+
69+
.dropzone-preview-file {
70+
display: flex;
71+
flex-direction: column-reverse;
72+
align-items: center;
73+
justify-content: start;
74+
word-wrap: anywhere;
75+
}
76+
77+
.dropzone-preview-file:hover {
78+
filter: brightness(110%);
2679
}
2780

2881
.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;
82+
flex-basis: 0;
83+
margin-bottom: var(--dropzone-spacing);
84+
width: var(--dropzone-image-size);
85+
aspect-ratio: 1;
86+
background-size: cover;
87+
background-position: 50% 50%;
88+
background-repeat: no-repeat;
89+
border-radius: var(--dropzone-radius);
90+
box-shadow: 0 0 8px var(--dropzone-background-hover);
3791
}
3892

39-
.dropzone-preview-filename {
40-
word-wrap: anywhere;
93+
.dropzone-preview-svg {
94+
margin-bottom: var(--dropzone-spacing);
95+
width: var(--dropzone-image-size);
96+
aspect-ratio: 1;
97+
background: var(--dropzone-file-svg);
4198
}
4299

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;
100+
.dropzone-preview-file span {
101+
font-weight: 300;
102+
font-size: 0.9em;
60103
}
61104

62-
.dropzone-preview-button::before {
63-
content: '×';
64-
padding: 3px 7px;
65-
cursor: pointer;
105+
.dropzone-preview-button {
106+
position: absolute;
107+
top: var(--dropzone-spacing);
108+
right: var(--dropzone-spacing);
109+
z-index: 1;
110+
border: none;
111+
margin: 0;
112+
padding: var(--dropzone-spacing);
113+
width: 1em;
114+
height: 1em;
115+
background-size: cover;
116+
background-position: 50% 50%;
117+
background-repeat: no-repeat;
118+
background: var(--dropzone-close-svg);
119+
cursor: pointer;
66120
}
67121

68122
.dropzone-placeholder {
69-
flex-grow: 1;
70-
text-align: center;
71-
color: #999;
123+
flex-grow: 1;
124+
text-align: center;
125+
}
126+
127+
.dropzone-on-drag-leave {
128+
background-color: var(--dropzone-background);
129+
transition: 0.3s;
72130
}

0 commit comments

Comments
 (0)