Skip to content

Commit 6624bea

Browse files
author
Carlos Hernández
committed
Refactor dropzone controller to support multiple files, improve test coverage, and adjust styles.
1 parent 7c4f5e6 commit 6624bea

File tree

9 files changed

+297
-122
lines changed

9 files changed

+297
-122
lines changed

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,30 @@ import { Controller } from '@hotwired/stimulus';
33
declare class export_default extends Controller {
44
readonly inputTarget: HTMLInputElement;
55
readonly placeholderTarget: HTMLDivElement;
6-
readonly previewTarget: HTMLDivElement;
7-
readonly previewClearButtonTarget: HTMLButtonElement;
8-
readonly previewFilenameTarget: HTMLDivElement;
9-
readonly previewImageTarget: HTMLDivElement;
6+
readonly previewTargets: HTMLDivElement[];
7+
readonly previewContainerTarget: HTMLDivElement;
108
static targets: string[];
9+
files: Map<string, File>;
1110
initialize(): void;
1211
connect(): void;
1312
disconnect(): void;
14-
clear(): void;
15-
onInputChange(event: any): void;
16-
_populateImagePreview(file: Blob): void;
13+
clear(event?: {
14+
target?: HTMLElement;
15+
params?: {
16+
filename?: string;
17+
};
18+
}): void;
19+
onInputChange(): void;
20+
private renderPreview;
21+
private clearPreviewContainer;
22+
private buildPreview;
23+
_populateImagePreview(element: HTMLElement, file: File): void;
1724
onDragEnter(): void;
1825
onDragLeave(event: any): void;
26+
private updateFileInput;
27+
private addFiles;
28+
private isImage;
29+
private get isMultiple();
1930
private dispatchEvent;
2031
}
2132

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// src/controller.ts
22
import { Controller } from "@hotwired/stimulus";
33
var controller_default = class extends Controller {
4+
constructor() {
5+
super(...arguments);
6+
this.files = /* @__PURE__ */ new Map();
7+
}
48
initialize() {
59
this.clear = this.clear.bind(this);
610
this.onInputChange = this.onInputChange.bind(this);
@@ -9,72 +13,125 @@ var controller_default = class extends Controller {
913
}
1014
connect() {
1115
this.clear();
12-
this.previewClearButtonTarget.addEventListener("click", this.clear);
1316
this.inputTarget.addEventListener("change", this.onInputChange);
1417
this.element.addEventListener("dragenter", this.onDragEnter);
1518
this.element.addEventListener("dragleave", this.onDragLeave);
1619
this.dispatchEvent("connect");
1720
}
1821
disconnect() {
19-
this.previewClearButtonTarget.removeEventListener("click", this.clear);
2022
this.inputTarget.removeEventListener("change", this.onInputChange);
2123
this.element.removeEventListener("dragenter", this.onDragEnter);
2224
this.element.removeEventListener("dragleave", this.onDragLeave);
2325
}
24-
clear() {
25-
this.inputTarget.value = "";
26-
this.inputTarget.style.display = "block";
27-
this.placeholderTarget.style.display = "block";
28-
this.previewTarget.style.display = "none";
29-
this.previewImageTarget.style.display = "none";
30-
this.previewImageTarget.style.backgroundImage = "none";
31-
this.previewFilenameTarget.textContent = "";
26+
clear(event) {
27+
if (event?.params) {
28+
const filename = event.params.filename;
29+
if (filename && this.files.has(filename)) {
30+
this.files.delete(filename);
31+
this.updateFileInput();
32+
this.renderPreview();
33+
}
34+
}
35+
if (!this.inputTarget || !this.inputTarget.files || this.inputTarget?.files?.length === 0) {
36+
this.placeholderTarget.style.display = "block";
37+
if (!this.isMultiple) {
38+
this.inputTarget.style.display = "block";
39+
}
40+
}
3241
this.dispatchEvent("clear");
3342
}
34-
onInputChange(event) {
35-
const file = event.target.files[0];
36-
if (typeof file === "undefined") {
43+
onInputChange() {
44+
const files = this.inputTarget.files;
45+
if (!files || files.length <= 0) {
3746
return;
3847
}
39-
this.inputTarget.style.display = "none";
40-
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);
48+
if (!this.isMultiple && this.files.size > 0) {
49+
this.inputTarget.style.display = "none";
4650
}
47-
this.dispatchEvent("change", file);
51+
const selectedFiles = this.isMultiple ? Array.from(files) : Array.from(files).slice(0, 1);
52+
this.addFiles(selectedFiles);
53+
this.updateFileInput();
54+
this.renderPreview();
55+
this.dispatchEvent("change", files);
4856
}
49-
_populateImagePreview(file) {
50-
if (typeof FileReader === "undefined") {
51-
return;
57+
renderPreview() {
58+
this.clearPreviewContainer();
59+
for (const file of this.files.values()) {
60+
const preview = this.buildPreview(file);
61+
if (preview) {
62+
this.previewContainerTarget.appendChild(preview);
63+
}
64+
}
65+
if (this.previewTargets.length > 1) {
66+
this.placeholderTarget.style.display = "none";
67+
if (!this.isMultiple) {
68+
this.inputTarget.style.display = "none";
69+
} else {
70+
this.inputTarget.style.display = "block";
71+
}
72+
}
73+
}
74+
clearPreviewContainer() {
75+
const previews = this.previewTargets;
76+
previews.slice(1).forEach((el) => el.remove());
77+
}
78+
buildPreview(file, element) {
79+
if (!element) {
80+
element = this.previewContainerTarget.firstElementChild?.cloneNode(true);
81+
}
82+
element.style.display = "flex";
83+
const fileName = element.querySelector(".dropzone-preview-filename");
84+
if (fileName) {
85+
fileName.textContent = file.name;
86+
}
87+
const button = element.querySelector(".dropzone-preview-button");
88+
if (button) {
89+
button.setAttribute("data-symfony--ux-dropzone--dropzone-filename-param", file.name);
90+
}
91+
this._populateImagePreview(element, file);
92+
return element;
93+
}
94+
_populateImagePreview(element, file) {
95+
const image = element.querySelector(".dropzone-preview-image");
96+
if (image && this.isImage(file) && typeof FileReader !== "undefined") {
97+
const reader = new FileReader();
98+
reader.addEventListener("load", (event) => {
99+
image.querySelector(".dropzone-preview-image")?.remove();
100+
image.style.backgroundImage = `url('${event.target.result}')`;
101+
image.style.display = "block";
102+
});
103+
reader.readAsDataURL(file);
52104
}
53-
const reader = new FileReader();
54-
reader.addEventListener("load", (event) => {
55-
this.previewImageTarget.style.display = "block";
56-
this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`;
57-
});
58-
reader.readAsDataURL(file);
59105
}
60106
onDragEnter() {
61107
this.inputTarget.style.display = "block";
62-
this.placeholderTarget.style.display = "block";
63-
this.previewTarget.style.display = "none";
64108
}
65109
onDragLeave(event) {
66110
event.preventDefault();
67-
if (!this.element.contains(event.relatedTarget)) {
68-
this.inputTarget.style.display = "none";
69-
this.placeholderTarget.style.display = "none";
70-
this.previewTarget.style.display = "block";
111+
}
112+
updateFileInput() {
113+
const dataTransfer = new DataTransfer();
114+
for (const file of this.files.values()) {
115+
dataTransfer.items.add(file);
116+
}
117+
this.inputTarget.files = dataTransfer.files;
118+
}
119+
addFiles(files) {
120+
for (const file of files) {
121+
this.files.set(file.name, file);
71122
}
72123
}
124+
isImage(file) {
125+
return typeof file.type !== "undefined" && file.type.indexOf("image") !== -1;
126+
}
127+
get isMultiple() {
128+
return this.inputTarget.multiple;
129+
}
73130
dispatchEvent(name, payload = {}) {
74131
this.dispatch(name, { detail: payload, prefix: "dropzone" });
75132
}
76133
};
77-
controller_default.targets = ["input", "placeholder", "preview", "previewClearButton", "previewFilename", "previewImage"];
134+
controller_default.targets = ["input", "placeholder", "preview", "previewClearButton", "previewFilename", "previewImage", "previewContainer"];
78135
export {
79136
controller_default as default
80137
};

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

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Dropzone/assets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@symfony/ux-dropzone",
33
"description": "File input dropzones for Symfony Forms",
44
"license": "MIT",
5-
"version": "2.31.0",
5+
"version": "2.30.0",
66
"keywords": [
77
"symfony-ux"
88
],

0 commit comments

Comments
 (0)