diff --git a/src/Dropzone/assets/dist/controller.js b/src/Dropzone/assets/dist/controller.js index d92d8c90052..95eebe2b985 100644 --- a/src/Dropzone/assets/dist/controller.js +++ b/src/Dropzone/assets/dist/controller.js @@ -15,22 +15,24 @@ 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.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') { @@ -47,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 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/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) + ); + } }