diff --git a/src/Dropzone/CHANGELOG.md b/src/Dropzone/CHANGELOG.md index bfe4e024f34..77389fe0f19 100644 --- a/src/Dropzone/CHANGELOG.md +++ b/src/Dropzone/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.20 + +- Enable file replacement via "drag-and-drop" + ## 2.13.2 - Revert "Change JavaScript package to `type: module`" diff --git a/src/Dropzone/assets/dist/controller.d.ts b/src/Dropzone/assets/dist/controller.d.ts index 5c834d44488..6e67b85b5cd 100644 --- a/src/Dropzone/assets/dist/controller.d.ts +++ b/src/Dropzone/assets/dist/controller.d.ts @@ -13,5 +13,7 @@ export default class extends Controller { clear(): void; onInputChange(event: any): void; _populateImagePreview(file: Blob): void; + onDragEnter(): void; + onDragLeave(event: any): void; private dispatchEvent; } diff --git a/src/Dropzone/assets/dist/controller.js b/src/Dropzone/assets/dist/controller.js index 2ff1e46c6c3..ebfb380d12a 100644 --- a/src/Dropzone/assets/dist/controller.js +++ b/src/Dropzone/assets/dist/controller.js @@ -4,16 +4,22 @@ class default_1 extends Controller { initialize() { this.clear = this.clear.bind(this); this.onInputChange = this.onInputChange.bind(this); + this.onDragEnter = this.onDragEnter.bind(this); + this.onDragLeave = this.onDragLeave.bind(this); } connect() { this.clear(); this.previewClearButtonTarget.addEventListener('click', this.clear); this.inputTarget.addEventListener('change', this.onInputChange); + this.element.addEventListener('dragenter', this.onDragEnter); + this.element.addEventListener('dragleave', this.onDragLeave); this.dispatchEvent('connect'); } disconnect() { this.previewClearButtonTarget.removeEventListener('click', this.clear); this.inputTarget.removeEventListener('change', this.onInputChange); + this.element.removeEventListener('dragenter', this.onDragEnter); + this.element.removeEventListener('dragleave', this.onDragLeave); } clear() { this.inputTarget.value = ''; @@ -51,6 +57,19 @@ class default_1 extends Controller { }); reader.readAsDataURL(file); } + onDragEnter() { + this.inputTarget.style.display = 'block'; + this.placeholderTarget.style.display = 'block'; + this.previewTarget.style.display = 'none'; + } + 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'; + } + } dispatchEvent(name, payload = {}) { this.dispatch(name, { detail: payload, prefix: 'dropzone' }); } diff --git a/src/Dropzone/assets/src/controller.ts b/src/Dropzone/assets/src/controller.ts index 51550e43e11..b2533329388 100644 --- a/src/Dropzone/assets/src/controller.ts +++ b/src/Dropzone/assets/src/controller.ts @@ -22,6 +22,8 @@ export default class extends Controller { initialize() { this.clear = this.clear.bind(this); this.onInputChange = this.onInputChange.bind(this); + this.onDragEnter = this.onDragEnter.bind(this); + this.onDragLeave = this.onDragLeave.bind(this); } connect() { @@ -34,12 +36,20 @@ export default class extends Controller { // Listen on input change and display preview this.inputTarget.addEventListener('change', this.onInputChange); + // Add dragenter event listener + this.element.addEventListener('dragenter', this.onDragEnter); + + // Add dragleave event listener + this.element.addEventListener('dragleave', this.onDragLeave); + this.dispatchEvent('connect'); } disconnect() { this.previewClearButtonTarget.removeEventListener('click', this.clear); this.inputTarget.removeEventListener('change', this.onInputChange); + this.element.removeEventListener('dragenter', this.onDragEnter); + this.element.removeEventListener('dragleave', this.onDragLeave); } clear() { @@ -93,6 +103,23 @@ export default class extends Controller { reader.readAsDataURL(file); } + onDragEnter() { + this.inputTarget.style.display = 'block'; + this.placeholderTarget.style.display = 'block'; + this.previewTarget.style.display = 'none'; + } + + onDragLeave(event: any) { + event.preventDefault(); + + // Check if we really leave the main drag area + if (!this.element.contains(event.relatedTarget as Node)) { + this.inputTarget.style.display = 'none'; + this.placeholderTarget.style.display = 'none'; + this.previewTarget.style.display = 'block'; + } + } + private dispatchEvent(name: string, payload: any = {}) { this.dispatch(name, { detail: payload, prefix: 'dropzone' }); } diff --git a/src/Dropzone/assets/test/controller.test.ts b/src/Dropzone/assets/test/controller.test.ts index 7935ebf1b61..84859f0665b 100644 --- a/src/Dropzone/assets/test/controller.test.ts +++ b/src/Dropzone/assets/test/controller.test.ts @@ -130,4 +130,27 @@ describe('DropzoneController', () => { expect(dispatched).not.toBeNull(); expect(dispatched.detail).toStrictEqual(file); }); + + it('on drag', async () => { + startStimulus(); + + // Simulate dragenter event + const dragEnterEvent = new Event('dragenter'); + getByTestId(container, 'container').dispatchEvent(dragEnterEvent); + + // Check that the input and placeholder are visible, and preview hidden + await waitFor(() => expect(getByTestId(container, 'input')).toHaveStyle({ display: 'block' })); + await waitFor(() => expect(getByTestId(container, 'placeholder')).toHaveStyle({ display: 'block' })); + await waitFor(() => expect(getByTestId(container, 'preview')).toHaveStyle({ display: 'none' })); + + // Simulate dragleave event with relatedTarget set to outside the dropzone + const dragLeaveEvent = new Event('dragleave', { bubbles: true }); + Object.defineProperty(dragLeaveEvent, 'relatedTarget', { value: document.body }); + getByTestId(container, 'container').dispatchEvent(dragLeaveEvent); + + // Check that the input and placeholder are hidden, and preview shown + await waitFor(() => expect(getByTestId(container, 'input')).toHaveStyle({ display: 'none' })); + await waitFor(() => expect(getByTestId(container, 'placeholder')).toHaveStyle({ display: 'none' })); + await waitFor(() => expect(getByTestId(container, 'preview')).toHaveStyle({ display: 'block' })); + }); });