From 26fb0cc93ed0707584f71186553360d4e40974bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20St=C3=B6hr?= Date: Mon, 24 Oct 2022 13:26:23 +0200 Subject: [PATCH 1/4] feat(dropzone): enable multiple file uploads --- src/Dropzone/assets/dist/controller.js | 27 ++++++----- src/Dropzone/assets/src/controller.ts | 39 ++++++++-------- src/Dropzone/assets/src/style.css | 6 ++- src/Dropzone/assets/test/controller.test.ts | 35 ++++++++++++-- src/Dropzone/src/Form/DropzoneType.php | 1 + src/Dropzone/templates/form_theme.html.twig | 1 + src/Dropzone/tests/Form/DropzoneTypeTest.php | 48 ++++++++++++++++++-- 7 files changed, 119 insertions(+), 38 deletions(-) diff --git a/src/Dropzone/assets/dist/controller.js b/src/Dropzone/assets/dist/controller.js index d92d8c90052..563e5231c64 100644 --- a/src/Dropzone/assets/dist/controller.js +++ b/src/Dropzone/assets/dist/controller.js @@ -15,22 +15,25 @@ class default_1 extends Controller { this.previewImageTarget.style.display = 'none'; this.previewImageTarget.style.backgroundImage = 'none'; this.previewFilenameTarget.textContent = ''; + document.querySelectorAll('.dropzone-preview-image-container').forEach((e) => e.remove()); this.dispatchEvent('clear'); } onInputChange(event) { - const file = event.target.files[0]; - if (typeof file === 'undefined') { - 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); + for (const fileItem in event.target.files) { + const file = event.target.files[fileItem]; + if (typeof file === 'undefined') { + 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.dispatchEvent('change', file); + this.dispatchEvent('change', event.target.files); } _populateImagePreview(file) { if (typeof FileReader === 'undefined') { diff --git a/src/Dropzone/assets/src/controller.ts b/src/Dropzone/assets/src/controller.ts index f27702f3a27..683e5f07a5f 100644 --- a/src/Dropzone/assets/src/controller.ts +++ b/src/Dropzone/assets/src/controller.ts @@ -42,31 +42,34 @@ export default class extends Controller { this.previewImageTarget.style.display = 'none'; this.previewImageTarget.style.backgroundImage = 'none'; this.previewFilenameTarget.textContent = ''; + document.querySelectorAll('.dropzone-preview-image-container').forEach((e) => e.remove()); this.dispatchEvent('clear'); } onInputChange(event: any) { - const file = event.target.files[0]; - if (typeof file === 'undefined') { - 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'; - - // 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 fileItem in event.target.files) { + const file = event.target.files[fileItem]; + if (typeof file === 'undefined') { + 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'; + + // 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); + } } - this.dispatchEvent('change', file); + this.dispatchEvent('change', event.target.files); } _populateImagePreview(file: Blob) { diff --git a/src/Dropzone/assets/src/style.css b/src/Dropzone/assets/src/style.css index 4cd21ac6b8e..b1d6e3cd937 100644 --- a/src/Dropzone/assets/src/style.css +++ b/src/Dropzone/assets/src/style.css @@ -26,11 +26,11 @@ } .dropzone-preview-image { + margin: auto; 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; @@ -70,3 +70,7 @@ text-align: center; color: #999; } + +.dropzone-preview-image-container { + margin-right: 1em; +} diff --git a/src/Dropzone/assets/test/controller.test.ts b/src/Dropzone/assets/test/controller.test.ts index 0821b9f48e8..164c1afbcf0 100644 --- a/src/Dropzone/assets/test/controller.test.ts +++ b/src/Dropzone/assets/test/controller.test.ts @@ -39,7 +39,7 @@ describe('DropzoneController', () => { + data-testid="input" multiple />
{ expect(dispatched).toBe(true); }); - it('file chosen', async () => { + it('single file chosen', async () => { startStimulus(); await waitFor(() => expect(getByTestId(container, 'input')).toHaveStyle({ display: 'block' })); @@ -126,6 +126,35 @@ describe('DropzoneController', () => { // The event should have been dispatched expect(dispatched).not.toBeNull(); - expect(dispatched.detail).toStrictEqual(file); + expect(dispatched.detail[0]).toStrictEqual(file); + }); + + it('multiple files chosen', async () => { + startStimulus(); + await waitFor(() => expect(getByTestId(container, 'input')).toHaveStyle({ display: 'block' })); + + // Attach a listener to ensure the event is dispatched + let dispatched = null; + getByTestId(container, 'container').addEventListener('dropzone:change', (event) => (dispatched = event)); + + // Select the file + const input = getByTestId(container, 'input'); + const files = [ + new File(['hello'], 'hello.png', { type: 'image/png' }), + new File(['again'], 'again.png', { type: 'image/png' }), + ] + + user.upload(input, files); + expect(input.files[0]).toStrictEqual(files[0]); + expect(input.files[1]).toStrictEqual(files[1]); + + // The dropzone should be in preview mode + await waitFor(() => expect(getByTestId(container, 'input')).toHaveStyle({ display: 'none' })); + await waitFor(() => expect(getByTestId(container, 'placeholder')).toHaveStyle({ display: 'none' })); + + // The event should have been dispatched + expect(dispatched).not.toBeNull(); + expect(dispatched.detail[0]).toStrictEqual(files[0]); + expect(dispatched.detail[1]).toStrictEqual(files[1]); }); }); diff --git a/src/Dropzone/src/Form/DropzoneType.php b/src/Dropzone/src/Form/DropzoneType.php index d0e73271c61..0a0f2f55499 100644 --- a/src/Dropzone/src/Form/DropzoneType.php +++ b/src/Dropzone/src/Form/DropzoneType.php @@ -28,6 +28,7 @@ public function configureOptions(OptionsResolver $resolver) 'attr' => [ 'placeholder' => 'Drag and drop or browse', ], + 'multiple' => false, ]); } diff --git a/src/Dropzone/templates/form_theme.html.twig b/src/Dropzone/templates/form_theme.html.twig index fb9812623fd..d11b5e6788e 100644 --- a/src/Dropzone/templates/form_theme.html.twig +++ b/src/Dropzone/templates/form_theme.html.twig @@ -2,6 +2,7 @@ {%- set dataController = (attr['data-controller']|default('') ~ ' symfony--ux-dropzone--dropzone')|trim -%} {%- set attr = attr|merge({ 'data-controller': '', class: (attr.class|default('') ~ ' dropzone-input')|trim}) -%} +
diff --git a/src/Dropzone/tests/Form/DropzoneTypeTest.php b/src/Dropzone/tests/Form/DropzoneTypeTest.php index cbd447776ab..44c24c25102 100644 --- a/src/Dropzone/tests/Form/DropzoneTypeTest.php +++ b/src/Dropzone/tests/Form/DropzoneTypeTest.php @@ -12,6 +12,7 @@ namespace Symfony\UX\Dropzone\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\UX\Dropzone\Form\DropzoneType; use Symfony\UX\Dropzone\Tests\Kernel\TwigAppKernel; @@ -24,18 +25,27 @@ */ class DropzoneTypeTest extends TestCase { - public function testRenderForm() + /** + * @var ContainerInterface + */ + private $container; + + protected function setUp(): void { $kernel = new TwigAppKernel('test', true); $kernel->boot(); - $container = $kernel->getContainer()->get('test.service_container'); - $form = $container->get(FormFactoryInterface::class)->createBuilder() + $this->container = $kernel->getContainer()->get('test.service_container'); + } + + public function testRenderForm() + { + $form = $this->container->get(FormFactoryInterface::class)->createBuilder() ->add('photo', DropzoneType::class, ['attr' => ['data-controller' => 'mydropzone']]) ->getForm() ; - $rendered = $container->get(Environment::class)->render('dropzone_form.html.twig', ['form' => $form->createView()]); + $rendered = $this->container->get(Environment::class)->render('dropzone_form.html.twig', ['form' => $form->createView()]); $this->assertSame( '
@@ -57,4 +67,34 @@ public function testRenderForm() str_replace(' >', '>', $rendered) ); } + + public function testRenderFormWithMultiFileUploads(): void + { + $form = $this->container->get(FormFactoryInterface::class)->createBuilder() + ->add('photo', DropzoneType::class, ['attr' => ['data-controller' => 'mydropzone'], 'multiple' => true]) + ->getForm() + ; + + $rendered = $this->container->get(Environment::class)->render('dropzone_form.html.twig', ['form' => $form->createView()]); + + $this->assertSame( + '
+ + +
+ + +
+', + str_replace(' >', '>', $rendered) + ); + } } From 3e471743bd5b10746a0002e52e03abc38bb88f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20St=C3=B6hr?= Date: Mon, 28 Nov 2022 10:01:46 +0100 Subject: [PATCH 2/4] fixup! feat(dropzone): enable multiple file uploads --- src/Dropzone/assets/dist/controller.js | 4 +++- src/Dropzone/assets/src/controller.ts | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Dropzone/assets/dist/controller.js b/src/Dropzone/assets/dist/controller.js index 563e5231c64..95eebe2b985 100644 --- a/src/Dropzone/assets/dist/controller.js +++ b/src/Dropzone/assets/dist/controller.js @@ -24,7 +24,6 @@ class default_1 extends Controller { if (typeof file === 'undefined') { return; } - this.inputTarget.style.display = 'none'; this.placeholderTarget.style.display = 'none'; this.previewFilenameTarget.textContent = file.name; this.previewTarget.style.display = 'flex'; @@ -50,6 +49,9 @@ class default_1 extends Controller { this.dispatch(name, { detail: payload, prefix: 'dropzone' }); } } +default_1.values = { + numberOfFiles: Number +}; default_1.targets = ['input', 'placeholder', 'preview', 'previewClearButton', 'previewFilename', 'previewImage']; export { default_1 as default }; diff --git a/src/Dropzone/assets/src/controller.ts b/src/Dropzone/assets/src/controller.ts index 683e5f07a5f..afe1d1b2540 100644 --- a/src/Dropzone/assets/src/controller.ts +++ b/src/Dropzone/assets/src/controller.ts @@ -21,6 +21,12 @@ export default class extends Controller { static targets = ['input', 'placeholder', 'preview', 'previewClearButton', 'previewFilename', 'previewImage']; + static values = { + numberOfFiles: Number + } + + readonly numberOfFilesValue: number; + connect() { // Reset when connecting to work with Turbolinks this.clear(); @@ -55,7 +61,7 @@ export default class extends Controller { } // Hide the input and placeholder - this.inputTarget.style.display = 'none'; + //this.inputTarget.style.display = 'none'; // hide only, when max number is reached this.placeholderTarget.style.display = 'none'; // Show the filename in preview From 651de6d6fae470b2d21dd309b0f21aa330329ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20St=C3=B6hr?= Date: Tue, 7 Feb 2023 20:32:30 +0100 Subject: [PATCH 3/4] fixup! feat(dropzone): enable multiple file uploads --- src/Dropzone/assets/src/controller.ts | 6 ------ src/Dropzone/src/Form/DropzoneType.php | 1 - src/Dropzone/templates/form_theme.html.twig | 1 - 3 files changed, 8 deletions(-) diff --git a/src/Dropzone/assets/src/controller.ts b/src/Dropzone/assets/src/controller.ts index afe1d1b2540..81ee5d41dbc 100644 --- a/src/Dropzone/assets/src/controller.ts +++ b/src/Dropzone/assets/src/controller.ts @@ -21,12 +21,6 @@ export default class extends Controller { static targets = ['input', 'placeholder', 'preview', 'previewClearButton', 'previewFilename', 'previewImage']; - static values = { - numberOfFiles: Number - } - - readonly numberOfFilesValue: number; - connect() { // Reset when connecting to work with Turbolinks this.clear(); diff --git a/src/Dropzone/src/Form/DropzoneType.php b/src/Dropzone/src/Form/DropzoneType.php index 0a0f2f55499..d0e73271c61 100644 --- a/src/Dropzone/src/Form/DropzoneType.php +++ b/src/Dropzone/src/Form/DropzoneType.php @@ -28,7 +28,6 @@ public function configureOptions(OptionsResolver $resolver) 'attr' => [ 'placeholder' => 'Drag and drop or browse', ], - 'multiple' => false, ]); } diff --git a/src/Dropzone/templates/form_theme.html.twig b/src/Dropzone/templates/form_theme.html.twig index d11b5e6788e..fb9812623fd 100644 --- a/src/Dropzone/templates/form_theme.html.twig +++ b/src/Dropzone/templates/form_theme.html.twig @@ -2,7 +2,6 @@ {%- set dataController = (attr['data-controller']|default('') ~ ' symfony--ux-dropzone--dropzone')|trim -%} {%- set attr = attr|merge({ 'data-controller': '', class: (attr.class|default('') ~ ' dropzone-input')|trim}) -%} -
From c568012bfcff8f0ccd16544ac4826c5c89469868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20St=C3=B6hr?= Date: Tue, 7 Feb 2023 20:45:15 +0100 Subject: [PATCH 4/4] fixup! feat(dropzone): enable multiple file uploads --- src/Dropzone/assets/src/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dropzone/assets/src/controller.ts b/src/Dropzone/assets/src/controller.ts index 81ee5d41dbc..683e5f07a5f 100644 --- a/src/Dropzone/assets/src/controller.ts +++ b/src/Dropzone/assets/src/controller.ts @@ -55,7 +55,7 @@ export default class extends Controller { } // Hide the input and placeholder - //this.inputTarget.style.display = 'none'; // hide only, when max number is reached + this.inputTarget.style.display = 'none'; this.placeholderTarget.style.display = 'none'; // Show the filename in preview