Skip to content

[Dropzone] Improve display with multiple files #2704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Dropzone/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 2.24

- Preview works with muliple files

## 2.20

- Enable file replacement via "drag-and-drop"
Expand Down
2 changes: 1 addition & 1 deletion src/Dropzone/assets/dist/controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class extends Controller {
disconnect(): void;
clear(): void;
onInputChange(event: any): void;
_populateImagePreview(file: Blob): void;
_populateImagePreview(file: Blob, imagePreviewElement: HTMLElement): void;
onDragEnter(): void;
onDragLeave(event: any): void;
private dispatchEvent;
Expand Down
49 changes: 35 additions & 14 deletions src/Dropzone/assets/dist/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,49 +25,70 @@ class default_1 extends Controller {
this.inputTarget.value = '';
this.inputTarget.style.display = 'block';
this.placeholderTarget.style.display = 'block';
this.previewTarget.innerHTML = '';
this.previewTarget.style.display = 'none';
this.previewImageTarget.style.display = 'none';
this.previewImageTarget.style.backgroundImage = 'none';
this.previewFilenameTarget.textContent = '';
this.element.classList.remove('dropzone-on-drag-enter');
this.dispatchEvent('clear');
}
onInputChange(event) {
const file = event.target.files[0];
if (typeof file === 'undefined') {
const files = event.target.files;
if (files.length === 0) {
this.previewClearButtonTarget.style.display = 'none';
return;
}
this.inputTarget.style.display = 'none';
this.placeholderTarget.style.display = 'none';
this.previewFilenameTarget.textContent = file.name;
this.previewTarget.style.display = 'flex';
this.previewImageTarget.style.display = 'none';
if (file.type && file.type.indexOf('image') !== -1) {
this._populateImagePreview(file);
this.previewTarget.innerHTML = '';
for (const file of files) {
const filePreviewContainer = document.createElement('div');
filePreviewContainer.classList.add('dropzone-preview-file');
const fileNameElement = document.createElement('span');
fileNameElement.textContent = file.name;
filePreviewContainer.appendChild(fileNameElement);
if (file.type) {
const imagePreviewElement = document.createElement('div');
if (file.type.indexOf('image') !== -1) {
imagePreviewElement.classList.add('dropzone-preview-image');
this._populateImagePreview(file, imagePreviewElement);
}
else {
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>';
imagePreviewElement.innerHTML = noPreviewSvg;
imagePreviewElement.classList.add('dropzone-no-preview');
}
filePreviewContainer.appendChild(imagePreviewElement);
}
this.previewTarget.appendChild(filePreviewContainer);
this.dispatchEvent('change', file);
}
this.dispatchEvent('change', file);
this.previewTarget.style.display = 'grid';
}
_populateImagePreview(file) {
_populateImagePreview(file, imagePreviewElement) {
if (typeof FileReader === 'undefined') {
return;
}
const reader = new FileReader();
reader.addEventListener('load', (event) => {
this.previewImageTarget.style.display = 'block';
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
imagePreviewElement.style.backgroundImage = `url("${event.target.result}")`;
imagePreviewElement.style.display = 'block';
});
reader.readAsDataURL(file);
}
onDragEnter() {
this.inputTarget.style.display = 'block';
this.placeholderTarget.style.display = 'block';
this.previewTarget.style.display = 'none';
this.element.classList.add('dropzone-on-drag-enter');
this.element.classList.remove('dropzone-on-drag-leave');
}
onDragLeave(event) {
event.preventDefault();
if (!this.element.contains(event.relatedTarget)) {
this.inputTarget.style.display = 'none';
this.placeholderTarget.style.display = 'none';
this.previewTarget.style.display = 'block';
this.element.classList.remove('dropzone-on-drag-enter');
this.element.classList.add('dropzone-on-drag-leave');
}
}
dispatchEvent(name, payload = {}) {
Expand Down
2 changes: 1 addition & 1 deletion src/Dropzone/assets/dist/style.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 48 additions & 17 deletions src/Dropzone/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,48 +56,75 @@ export default class extends Controller {
this.inputTarget.value = '';
this.inputTarget.style.display = 'block';
this.placeholderTarget.style.display = 'block';
this.previewTarget.innerHTML = '';
this.previewTarget.style.display = 'none';
this.previewImageTarget.style.display = 'none';
this.previewImageTarget.style.backgroundImage = 'none';
this.previewFilenameTarget.textContent = '';
this.element.classList.remove('dropzone-on-drag-enter');

this.dispatchEvent('clear');
}

onInputChange(event: any) {
const file = event.target.files[0];
if (typeof file === 'undefined') {
const files = event.target.files;
if (files.length === 0) {
this.previewClearButtonTarget.style.display = 'none';
return;
}

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

// Show the filename in preview
this.previewFilenameTarget.textContent = file.name;
this.previewTarget.style.display = 'flex';
// Clear previous previews
this.previewTarget.innerHTML = '';

// If the file is an image, load it and display it as preview
this.previewImageTarget.style.display = 'none';
if (file.type && file.type.indexOf('image') !== -1) {
this._populateImagePreview(file);
for (const file of files) {
// Create a container for each file preview
const filePreviewContainer = document.createElement('div');
filePreviewContainer.classList.add('dropzone-preview-file');

// Create a filename preview element
const fileNameElement = document.createElement('span');
fileNameElement.textContent = file.name;
filePreviewContainer.appendChild(fileNameElement);

// Create an image preview element if the file is an image, else a default svg file icon
if (file.type) {
const imagePreviewElement = document.createElement('div');

if (file.type.indexOf('image') !== -1) {
imagePreviewElement.classList.add('dropzone-preview-image');
this._populateImagePreview(file, imagePreviewElement);
} else {
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>';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep SVG in HTML :)

That way use can change them easily with their own form theme

imagePreviewElement.innerHTML = noPreviewSvg;

imagePreviewElement.classList.add('dropzone-no-preview');
}

filePreviewContainer.appendChild(imagePreviewElement);
}

// Append the file preview container to the main preview target
this.previewTarget.appendChild(filePreviewContainer);

this.dispatchEvent('change', file);
}

this.dispatchEvent('change', file);
// Show the preview container
this.previewTarget.style.display = 'grid';
}

_populateImagePreview(file: Blob) {
_populateImagePreview(file: Blob, imagePreviewElement: HTMLElement) {
if (typeof FileReader === 'undefined') {
// FileReader API not available, skip
return;
}

const reader = new FileReader();

reader.addEventListener('load', (event: any) => {
this.previewImageTarget.style.display = 'block';
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
imagePreviewElement.style.backgroundImage = `url("${event.target.result}")`;
imagePreviewElement.style.display = 'block';
});

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

onDragLeave(event: any) {
Expand All @@ -117,6 +146,8 @@ export default class extends Controller {
this.inputTarget.style.display = 'none';
this.placeholderTarget.style.display = 'none';
this.previewTarget.style.display = 'block';
this.element.classList.remove('dropzone-on-drag-enter');
this.element.classList.add('dropzone-on-drag-leave');
}
}

Expand Down
153 changes: 101 additions & 52 deletions src/Dropzone/assets/src/style.css
Original file line number Diff line number Diff line change
@@ -1,72 +1,121 @@
:root {
--dropzone-background: white;
--dropzone-background-hover: #dddddd;
--dropzone-border-color: #aaaaaa;
--dropzone-border-color-hover: #666666;
--dropzone-text-color: ##333333;

--dropzone-spacing: 8px;
--dropzone-radius: 8px;

--dropzone-width: cacl(100% - 2 * var(--dropzone-spacing));
--dropzone-height: 120px;
--dropzone-image-size: 100px;
}

.dropzone-container {
position: relative;
display: flex;
min-height: 100px;
border: 2px dashed #bbb;
align-items: center;
padding: 20px 10px;
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
min-height: var(--dropzone-height);
width: var(--dropzone-width);
border: 2px dashed var(--dropzone-border-color);
padding: var(--dropzone-spacing);
border-radius: var(--dropzone-radius);
color: var(--dropzone-text-color);
background-color: var(--dropzone-background);
}

.dropzone-container:has(.dropzone-preview:empty) .dropzone-preview-button {
display: none;
}

.dropzone-container:hover,
.dropzone-on-drag-enter {
background-color: var(--dropzone-background-hover);
transition: 0.3s;
}

.dropzone-input {
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 1;
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 1;
}

.dropzone-preview {
display: flex;
align-items: center;
max-width: 100%;
display: grid;
gap: var(--dropzone-spacing);
grid-template-columns: repeat(auto-fill, var(--dropzone-image-size));
grid-template-rows: auto;
place-items: stretch;
width: 100%;
height: 100%;
}

.dropzone-preview-file {
display: flex;
flex-direction: column-reverse;
align-items: center;
justify-content: start;
word-wrap: anywhere;
}

.dropzone-preview-file:hover {
filter: brightness(110%);
}

.dropzone-preview-image {
flex-basis: 0;
min-width: 50px;
max-width: 50px;
height: 50px;
margin-right: 10px;
background-size: contain;
background-position: 50% 50%;
background-repeat: no-repeat;
margin-bottom: var(--dropzone-spacing);
width: var(--dropzone-image-size);
aspect-ratio: 1;
background-size: cover;
background-position: 50% 50%;
background-repeat: no-repeat;
border-radius: var(--dropzone-radius);
box-shadow: 0 0 8px var(--dropzone-background-hover);
}

.dropzone-preview-filename {
word-wrap: anywhere;
.dropzone-no-preview {
margin-bottom: var(--dropzone-spacing);
height: var(--dropzone-image-size);
}

.dropzone-preview-button {
position: absolute;
top: 0;
right: 0;
z-index: 1;
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
color: inherit;
font: inherit;
line-height: normal;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
.dropzone-no-preview svg {
width: 100%;
}

.dropzone-preview-button::before {
content: '×';
padding: 3px 7px;
cursor: pointer;

.dropzone-preview-file span {
font-weight: 300;
font-size: 0.9em;
}

.dropzone-preview-button {
position: absolute;
top: var(--dropzone-spacing);
right: var(--dropzone-spacing);
z-index: 1;
border: none;
margin: 0;
background: none;
display: grid;
place-items: center;
cursor: pointer;
}

.dropzone-placeholder {
flex-grow: 1;
text-align: center;
color: #999;
flex-grow: 1;
text-align: center;
}

.dropzone-on-drag-leave {
background-color: var(--dropzone-background);
transition: 0.3s;
Comment on lines +118 to +120
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the only reason to add the 2 classes, maybe it's not 100% required, wdyt ? :)

(the same thing can be done with :has selectors)

}
Loading
Loading