Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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 };
45 changes: 27 additions & 18 deletions src/Dropzone/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export default class extends Controller {

static targets = ['input', 'placeholder', 'preview', 'previewClearButton', 'previewFilename', 'previewImage'];

static values = {
numberOfFiles: Number
}

readonly numberOfFilesValue: number;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like this is used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already removed.


connect() {
// Reset when connecting to work with Turbolinks
this.clear();
Expand All @@ -42,31 +48,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'; // hide only, when max number is reached
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]);
});
});
1 change: 1 addition & 0 deletions src/Dropzone/src/Form/DropzoneType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function configureOptions(OptionsResolver $resolver)
'attr' => [
'placeholder' => 'Drag and drop or browse',
],
'multiple' => false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that needed? It's already the default of the parent type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has been removed.

]);
}

Expand Down
1 change: 1 addition & 0 deletions src/Dropzone/templates/form_theme.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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}) -%}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

<div class="dropzone-container" data-controller="{{ dataController }}">
<input type="file" {{ block('widget_attributes') }} data-symfony--ux-dropzone--dropzone-target="input" />

Expand Down
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)
);
}
}