Skip to content

[Dropzone] Enable multiple file uploads #512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
29 changes: 17 additions & 12 deletions src/Dropzone/assets/dist/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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 };
39 changes: 21 additions & 18 deletions src/Dropzone/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion src/Dropzone/assets/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -70,3 +70,7 @@
text-align: center;
color: #999;
}

.dropzone-preview-image-container {
margin-right: 1em;
}
35 changes: 32 additions & 3 deletions src/Dropzone/assets/test/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('DropzoneController', () => {
<input type="file"
style="display: none"
data-dropzone-target="input"
data-testid="input" />
data-testid="input" multiple />

<div class="dropzone-placeholder"
data-dropzone-target="placeholder"
Expand Down Expand Up @@ -105,7 +105,7 @@ describe('DropzoneController', () => {
expect(dispatched).toBe(true);
});

it('file chosen', async () => {
it('single file chosen', async () => {
startStimulus();
await waitFor(() => expect(getByTestId(container, 'input')).toHaveStyle({ display: 'block' }));

Expand All @@ -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]);
});
});
48 changes: 44 additions & 4 deletions src/Dropzone/tests/Form/DropzoneTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
'<form name="form" method="post" enctype="multipart/form-data"><div id="form"><div><label for="form_photo" class="required">Photo</label><div class="dropzone-container" data-controller="mydropzone symfony--ux-dropzone--dropzone">
Expand All @@ -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(
'<form name="form" method="post" enctype="multipart/form-data"><div id="form"><div><label for="form_photo" class="required">Photo</label><div class="dropzone-container" data-controller="mydropzone symfony--ux-dropzone--dropzone">
<input type="file" id="form_photo" name="form[photo][]" required="required" data-controller="" multiple="multiple" class="dropzone-input" data-symfony--ux-dropzone--dropzone-target="input" />

<div class="dropzone-placeholder" data-symfony--ux-dropzone--dropzone-target="placeholder"></div>

<div class="dropzone-preview" data-symfony--ux-dropzone--dropzone-target="preview" style="display: none">
<button class="dropzone-preview-button" type="button"
data-symfony--ux-dropzone--dropzone-target="previewClearButton"></button>

<div class="dropzone-preview-image" style="display: none"
data-symfony--ux-dropzone--dropzone-target="previewImage"></div>

<div data-symfony--ux-dropzone--dropzone-target="previewFilename" class="dropzone-preview-filename"></div>
</div>
</div></div></div></form>
',
str_replace(' >', '>', $rendered)
);
}
}