Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/Dropzone/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Add a `dropzone:pre-add` event to transform, replace or reject the selected files before they are added, and a `data-state="processing"` attribute on the container while it is pending
- Add a `data-state` attribute on the container: `empty`, `filled`, `dragging` or `processing`
- Show and hide the parts of the Dropzone with the `hidden` attribute instead of inline styles, see [UPGRADE.md](UPGRADE.md)
- Add a `dropzone_preview_item_template` form theme block and a `previewItemTemplate` target to render the preview of each file in `multiple` mode from a `<template>`

## 3.5.0

Expand Down
7 changes: 7 additions & 0 deletions src/Dropzone/assets/dist/controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export default class export_default extends Controller {
readonly previewImageTarget: HTMLDivElement;
readonly previewListTarget: HTMLUListElement;
readonly hasPreviewListTarget: boolean;
readonly previewItemTemplateTarget: HTMLTemplateElement;
readonly hasPreviewItemTemplateTarget: boolean;
readonly multipleValue: boolean;
readonly removeLabelValue: string;
static targets: string[];
Expand All @@ -19,6 +21,7 @@ export default class export_default extends Controller {
};
};
private dataTransfer;
private previewItem;
private processingCount;
private filled;
private dragging;
Expand All @@ -43,6 +46,10 @@ export default class export_default extends Controller {
private containsFile;
private renderList;
private buildListItem;
private buildListItemFromTemplate;
private isImage;
private readAsDataUrl;
private bindRemove;
private updateState;
private dispatchEvent;
}
55 changes: 46 additions & 9 deletions src/Dropzone/assets/dist/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ var controller_default = class extends Controller {
"previewClearButton",
"previewFilename",
"previewImage",
"previewList"
"previewList",
"previewItemTemplate"
];
static values = {
multiple: Boolean,
Expand All @@ -17,6 +18,7 @@ var controller_default = class extends Controller {
}
};
dataTransfer;
previewItem = null;
processingCount = 0;
filled = false;
dragging = false;
Expand Down Expand Up @@ -89,13 +91,10 @@ var controller_default = class extends Controller {
});
}
_populateImagePreview(target, file) {
if (typeof FileReader === "undefined") return;
const reader = new FileReader();
reader.addEventListener("load", (event) => {
this.readAsDataUrl(file, (url) => {
target.hidden = false;
target.style.backgroundImage = `url("${event.target.result}")`;
target.style.backgroundImage = `url("${url}")`;
});
reader.readAsDataURL(file);
}
onDragEnter() {
this.dragging = true;
Expand All @@ -115,6 +114,12 @@ var controller_default = class extends Controller {
});
}
connectMultiple() {
this.previewItem = null;
if (this.hasPreviewItemTemplateTarget) {
const elements = this.previewItemTemplateTarget.content.children;
if (elements.length !== 1) throw new Error("The \"previewItemTemplate\" target must contain one root element.");
this.previewItem = elements[0];
}
this.dataTransfer = new DataTransfer();
for (const file of Array.from(this.inputTarget.files ?? [])) this.dataTransfer.items.add(file);
this.inputTarget.addEventListener("change", this.onMultipleChange);
Expand Down Expand Up @@ -198,23 +203,55 @@ var controller_default = class extends Controller {
this.previewListTarget.replaceChildren(...items);
}
buildListItem(file, index) {
if (this.previewItem) return this.buildListItemFromTemplate(this.previewItem, file, index);
const item = document.createElement("li");
item.className = "dropzone-preview-list-item";
const image = document.createElement("div");
image.className = "dropzone-preview-image";
image.hidden = true;
if (file.type && file.type.indexOf("image") !== -1) this._populateImagePreview(image, file);
if (this.isImage(file)) this._populateImagePreview(image, file);
const filename = document.createElement("span");
filename.className = "dropzone-preview-filename";
filename.textContent = file.name;
const remove = document.createElement("button");
remove.type = "button";
remove.className = "dropzone-preview-list-remove";
remove.setAttribute("aria-label", `${this.removeLabelValue} ${file.name}`);
remove.addEventListener("click", () => this.removeFileAt(index, file));
this.bindRemove(remove, file, index);
item.append(image, filename, remove);
return item;
}
buildListItemFromTemplate(template, file, index) {
const item = template.cloneNode(true);
const slots = (name) => {
const selector = `[data-dropzone-slot="${name}"]`;
return [...item.matches(selector) ? [item] : [], ...Array.from(item.querySelectorAll(selector))];
};
const images = slots("image");
for (const image of images) image.hidden = true;
if (images.length > 0 && this.isImage(file)) this.readAsDataUrl(file, (url) => {
for (const image of images) {
if (image instanceof HTMLImageElement) image.src = url;
else image.style.backgroundImage = `url("${url}")`;
image.hidden = false;
}
});
for (const filename of slots("filename")) filename.textContent = file.name;
for (const remove of slots("remove")) this.bindRemove(remove, file, index);
return item;
}
isImage(file) {
return !!file.type && file.type.indexOf("image") !== -1;
}
readAsDataUrl(file, onLoad) {
if (typeof FileReader === "undefined") return;
const reader = new FileReader();
reader.addEventListener("load", () => onLoad(reader.result));
reader.readAsDataURL(file);
}
bindRemove(element, file, index) {
element.setAttribute("aria-label", `${this.removeLabelValue} ${file.name}`);
element.addEventListener("click", () => this.removeFileAt(index, file));
}
updateState() {
const filled = this.multipleValue ? this.dataTransfer.files.length > 0 : this.filled;
let state = filled ? "filled" : "empty";
Expand Down
95 changes: 80 additions & 15 deletions src/Dropzone/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export default class extends Controller {
declare readonly previewImageTarget: HTMLDivElement;
declare readonly previewListTarget: HTMLUListElement;
declare readonly hasPreviewListTarget: boolean;
declare readonly previewItemTemplateTarget: HTMLTemplateElement;
declare readonly hasPreviewItemTemplateTarget: boolean;

declare readonly multipleValue: boolean;
declare readonly removeLabelValue: string;
Expand All @@ -30,6 +32,7 @@ export default class extends Controller {
'previewFilename',
'previewImage',
'previewList',
'previewItemTemplate',
];

static values = {
Expand All @@ -38,6 +41,7 @@ export default class extends Controller {
};

private dataTransfer!: DataTransfer;
private previewItem: Element | null = null;
private processingCount = 0;
private filled = false;
private dragging = false;
Expand Down Expand Up @@ -143,19 +147,10 @@ export default class extends Controller {
}

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

const reader = new FileReader();

reader.addEventListener('load', (event: any) => {
this.readAsDataUrl(file, (url) => {
target.hidden = false;
target.style.backgroundImage = `url("${event.target.result}")`;
target.style.backgroundImage = `url("${url}")`;
});

reader.readAsDataURL(file);
}

onDragEnter() {
Expand Down Expand Up @@ -185,6 +180,16 @@ export default class extends Controller {
// --- Multiple-file handling ------------------------------------------------

private connectMultiple() {
this.previewItem = null;
if (this.hasPreviewItemTemplateTarget) {
// Checked before any file is added, so an invalid template cannot leave the input and the list apart
const elements = this.previewItemTemplateTarget.content.children;
if (elements.length !== 1) {
throw new Error('The "previewItemTemplate" target must contain one root element.');
}
this.previewItem = elements[0];
}

this.dataTransfer = new DataTransfer();

// Reseed from the input in case the controller reconnects with files already selected
Expand Down Expand Up @@ -326,14 +331,18 @@ export default class extends Controller {
this.previewListTarget.replaceChildren(...items);
}

private buildListItem(file: File, index: number): HTMLLIElement {
private buildListItem(file: File, index: number): Element {
if (this.previewItem) {
return this.buildListItemFromTemplate(this.previewItem, file, index);
}

const item = document.createElement('li');
item.className = 'dropzone-preview-list-item';

const image = document.createElement('div');
image.className = 'dropzone-preview-image';
image.hidden = true;
if (file.type && file.type.indexOf('image') !== -1) {
if (this.isImage(file)) {
this._populateImagePreview(image, file);
}

Expand All @@ -345,14 +354,70 @@ export default class extends Controller {
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'dropzone-preview-list-remove';
remove.setAttribute('aria-label', `${this.removeLabelValue} ${file.name}`);
remove.addEventListener('click', () => this.removeFileAt(index, file));
this.bindRemove(remove, file, index);

item.append(image, filename, remove);

return item;
}

private buildListItemFromTemplate(template: Element, file: File, index: number): Element {
const item = template.cloneNode(true) as Element;
const slots = (name: string): Element[] => {
const selector = `[data-dropzone-slot="${name}"]`;

return [...(item.matches(selector) ? [item] : []), ...Array.from(item.querySelectorAll(selector))];
};

const images = slots('image') as HTMLElement[];
for (const image of images) {
image.hidden = true;
}
if (images.length > 0 && this.isImage(file)) {
this.readAsDataUrl(file, (url) => {
for (const image of images) {
if (image instanceof HTMLImageElement) {
image.src = url;
} else {
image.style.backgroundImage = `url("${url}")`;
}
image.hidden = false;
}
});
}

for (const filename of slots('filename')) {
// Always use textContent: file names are user input and must never be treated as HTML
filename.textContent = file.name;
}

for (const remove of slots('remove')) {
this.bindRemove(remove, file, index);
}

return item;
}

private isImage(file: File): boolean {
return !!file.type && file.type.indexOf('image') !== -1;
}

private readAsDataUrl(file: Blob, onLoad: (url: string) => void) {
if (typeof FileReader === 'undefined') {
// FileReader API not available, skip
return;
}

const reader = new FileReader();
reader.addEventListener('load', () => onLoad(reader.result as string));
reader.readAsDataURL(file);
}

private bindRemove(element: Element, file: File, index: number) {
element.setAttribute('aria-label', `${this.removeLabelValue} ${file.name}`);
element.addEventListener('click', () => this.removeFileAt(index, file));
}

private updateState() {
const filled = this.multipleValue ? this.dataTransfer.files.length > 0 : this.filled;

Expand Down
Loading
Loading