Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/Cropperjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Support for `stimulus` version 2 was removed and support for `@hotwired/stimulus`
version 3 was added. See the [@symfony/stimulus-bridge CHANGELOG](https://github.com/symfony/stimulus-bridge/blob/main/CHANGELOG.md#300)
for more details.
- The individual Cropper.js options in `CropperType` were moved under
a single `cropper_options` option.
- Support added for Symfony 6

## 1.3
Expand Down
132 changes: 3 additions & 129 deletions src/Cropperjs/Form/CropperType.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,38 +34,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
'error_bubbling' => true,
'attr' => [
'data-controller' => trim(($options['attr']['data-controller'] ?? '').' symfony--ux-cropperjs--cropper'),
'data-public-url' => $options['public_url'],
'data-view-mode' => $options['view_mode'],
'data-drag-mode' => $options['drag_mode'],
'data-aspect-ratio' => $options['aspect_ratio'] ?: false,
'data-initial-aspect-ratio' => $options['initial_aspect_ratio'] ?: false,
'data-responsive' => $options['responsive'],
'data-restore' => $options['restore'],
'data-check-cross-origin' => $options['check_cross_origin'],
'data-check-orientation' => $options['check_orientation'],
'data-modal' => $options['modal'],
'data-guides' => $options['guides'],
'data-center' => $options['center'],
'data-highlight' => $options['highlight'],
'data-background' => $options['background'],
'data-auto-crop' => $options['auto_crop'],
'data-auto-crop-area' => $options['auto_crop_area'],
'data-movable' => $options['movable'],
'data-rotatable' => $options['rotatable'],
'data-scalable' => $options['scalable'],
'data-zoomable' => $options['zoomable'],
'data-zoom-on-touch' => $options['zoom_on_touch'],
'data-zoom-on-wheel' => $options['zoom_on_wheel'],
'data-wheel-zoom-ratio' => $options['wheel_zoom_ratio'],
'data-crop-box-movable' => $options['crop_box_movable'],
'data-crop-box-resizable' => $options['crop_box_resizable'],
'data-toggle-drag-mode-on-dblclick' => $options['toggle_drag_mode_on_dblclick'],
'data-min-container-width' => $options['min_container_width'],
'data-min-container-height' => $options['min_container_height'],
'data-min-canvas-width' => $options['min_canvas_width'],
'data-min-canvas-height' => $options['min_canvas_height'],
'data-min-crop-box-width' => $options['min_crop_box_width'],
'data-min-crop-box-height' => $options['min_crop_box_height'],
'data-symfony--ux-cropperjs--cropper-public-url-value' => $options['public_url'],
'data-symfony--ux-cropperjs--cropper-options-value' => json_encode($options['cropper_options']),
],
])
;
Expand All @@ -85,107 +55,11 @@ public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('public_url');
$resolver->setAllowedTypes('public_url', 'string');

$resolver->setDefined([
'view_mode',
'drag_mode',
'aspect_ratio',
'initial_aspect_ratio',
'responsive',
'restore',
'check_cross_origin',
'check_orientation',
'modal',
'guides',
'center',
'highlight',
'background',
'auto_crop',
'auto_crop_area',
'movable',
'rotatable',
'scalable',
'zoomable',
'zoom_on_touch',
'zoom_on_wheel',
'wheel_zoom_ratio',
'crop_box_movable',
'crop_box_resizable',
'toggle_drag_mode_on_dblclick',
'min_container_width',
'min_container_height',
'min_canvas_width',
'min_canvas_height',
'min_crop_box_width',
'min_crop_box_height',
]);

$resolver->setAllowedTypes('view_mode', ['int']);
$resolver->setAllowedTypes('drag_mode', ['string']);
$resolver->setAllowedTypes('aspect_ratio', ['double', 'null']);
$resolver->setAllowedTypes('initial_aspect_ratio', ['double', 'null']);
$resolver->setAllowedTypes('responsive', ['bool']);
$resolver->setAllowedTypes('restore', ['bool']);
$resolver->setAllowedTypes('check_cross_origin', ['bool']);
$resolver->setAllowedTypes('check_orientation', ['bool']);
$resolver->setAllowedTypes('modal', ['bool']);
$resolver->setAllowedTypes('guides', ['bool']);
$resolver->setAllowedTypes('center', ['bool']);
$resolver->setAllowedTypes('highlight', ['bool']);
$resolver->setAllowedTypes('background', ['bool']);
$resolver->setAllowedTypes('auto_crop', ['bool']);
$resolver->setAllowedTypes('auto_crop_area', ['float']);
$resolver->setAllowedTypes('movable', ['bool']);
$resolver->setAllowedTypes('rotatable', ['bool']);
$resolver->setAllowedTypes('scalable', ['bool']);
$resolver->setAllowedTypes('zoomable', ['bool']);
$resolver->setAllowedTypes('zoom_on_touch', ['bool']);
$resolver->setAllowedTypes('zoom_on_wheel', ['bool']);
$resolver->setAllowedTypes('wheel_zoom_ratio', ['float']);
$resolver->setAllowedTypes('crop_box_movable', ['bool']);
$resolver->setAllowedTypes('crop_box_resizable', ['bool']);
$resolver->setAllowedTypes('toggle_drag_mode_on_dblclick', ['bool']);
$resolver->setAllowedTypes('min_container_width', ['int']);
$resolver->setAllowedTypes('min_container_height', ['int']);
$resolver->setAllowedTypes('min_canvas_width', ['int']);
$resolver->setAllowedTypes('min_canvas_height', ['int']);
$resolver->setAllowedTypes('min_crop_box_width', ['int']);
$resolver->setAllowedTypes('min_crop_box_height', ['int']);
$resolver->setDefault('cropper_options', []);

$resolver->setDefaults([
'label' => false,
'data_class' => Crop::class,
'view_mode' => 0,
'drag_mode' => 'crop',
'initial_aspect_ratio' => null,
'aspect_ratio' => null,
'responsive' => true,
'restore' => true,
'check_cross_origin' => true,
'check_orientation' => true,
'modal' => true,
'guides' => true,
'center' => true,
'highlight' => true,
'background' => true,
'auto_crop' => true,
'auto_crop_area' => 0.8,
'movable' => false,
'rotatable' => true,
'scalable' => false,
'zoomable' => false,
'zoom_on_touch' => true,
'zoom_on_wheel' => true,
'wheel_zoom_ratio' => 0.1,
'crop_box_movable' => true,
'crop_box_resizable' => true,
'toggle_drag_mode_on_dblclick' => true,
'min_container_width' => 200,
'min_container_height' => 100,
'min_canvas_width' => 0,
'min_canvas_height' => 0,
'min_crop_box_width' => 0,
'min_crop_box_height' => 0,
]);
}

Expand Down
60 changes: 15 additions & 45 deletions src/Cropperjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ class HomeController extends AbstractController
$form = $this->createFormBuilder(['crop' => $crop])
->add('crop', CropperType::class, [
'public_url' => '/public/url/to/the/image.jpg',
'aspect_ratio' => 2000 / 1500,
'cropper_options' => [
'aspectRatio' => 2000 / 1500,
],
])
->getForm()
;
Expand All @@ -68,49 +70,7 @@ class HomeController extends AbstractController
}
```

You can pass the following options to the `CropperType` field:

```php
$form = $this->createFormBuilder(['crop' => $crop])
->add('crop', CropperType::class, [
'public_url' => '/public/url/to/the/image.jpg',
'view_mode' => 1,
'drag_mode' => 'move',
'initial_aspect_ratio' => 2000 / 1800,
'aspect_ratio' => 2000 / 1800,
'responsive' => true,
'restore' => true,
'check_cross_origin' => true,
'check_orientation' => true,
'modal' => true,
'guides' => true,
'center' => true,
'highlight' => true,
'background' => true,
'auto_crop' => true,
'auto_crop_area' => 0.1,
'movable' => true,
'rotatable' => true,
'scalable' => true,
'zoomable' => true,
'zoom_on_touch' => true,
'zoom_on_wheel' => true,
'wheel_zoom_ratio' => 0.2,
'crop_box_movable' => true,
'crop_box_resizable' => true,
'toggle_drag_mode_on_dblclick' => true,
'min_container_width' => 200,
'min_container_height' => 100,
'min_canvas_width' => 0,
'min_canvas_height' => 0,
'min_crop_box_width' => 0,
'min_crop_box_height' => 0,
])
->getForm()
;
```

These options are associated to [the Cropper.js options](https://github.com/fengyuanchen/cropperjs/blob/master/README.md#options).
These `cropper_options` can be any [the Cropper.js option](https://github.com/fengyuanchen/cropperjs/blob/main/README.md#options).

Once created in PHP, a crop form is a normal form, meaning you can display it using Twig
as you would normally with any form:
Expand All @@ -130,14 +90,22 @@ import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
connect() {
this.element.addEventListener('cropperjs:pre-connect', this._onPreConnect);
this.element.addEventListener('cropperjs:connect', this._onConnect);
}

disconnect() {
// You should always remove listeners when the controller is disconnected to avoid side effects
this.element.removeEventListener('cropperjs:pre-connect', this._onConnect);
this.element.removeEventListener('cropperjs:connect', this._onConnect);
}

_onPreConnect(event) {
// The cropper has not yet been created and options can be modified
console.log(event.detail.options);
console.log(event.detail.img);
}

_onConnect(event) {
// The cropper was just created and you can access details from the event
console.log(event.detail.cropper);
Expand All @@ -158,7 +126,9 @@ Then in your form, add your controller as an HTML attribute:
$form = $this->createFormBuilder(['crop' => $crop])
->add('crop', CropperType::class, [
'public_url' => '/public/url/to/the/image.jpg',
'aspect_ratio' => 2000 / 1800,
'cropper_options' => [
'aspectRatio' => 2000 / 1800,
],
'attr' => ['data-controller' => 'mycropper'],
])
->getForm()
Expand Down
63 changes: 18 additions & 45 deletions src/Cropperjs/Resources/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,68 +11,41 @@

import { Controller } from '@hotwired/stimulus';
import Cropper from 'cropperjs';
import CropEvent = Cropper.CropEvent;

export default class CropperController extends Controller {
static values = {
publicUrl: String,
options: Object,
};

export default class extends Controller {
connect() {
// Create image view
const img = document.createElement('img');
img.classList.add('cropperjs-image');
img.src = this.element.getAttribute('data-public-url');

const parent = this.element.parentNode;
parent.appendChild(img);

// Build the cropper
const options = {
viewMode: parseInt(this.element.getAttribute('data-view-mode')),
dragMode: this.element.getAttribute('data-drag-mode'),
responsive: this.element.hasAttribute('data-responsive'),
restore: this.element.hasAttribute('data-restore'),
checkCrossOrigin: this.element.hasAttribute('data-check-cross-origin'),
checkOrientation: this.element.hasAttribute('data-check-orientation'),
modal: this.element.hasAttribute('data-modal'),
guides: this.element.hasAttribute('data-guides'),
center: this.element.hasAttribute('data-center'),
highlight: this.element.hasAttribute('data-highlight'),
background: this.element.hasAttribute('data-background'),
autoCrop: this.element.hasAttribute('data-auto-crop'),
autoCropArea: parseFloat(this.element.getAttribute('data-auto-crop-area')),
movable: this.element.hasAttribute('data-movable'),
rotatable: this.element.hasAttribute('data-rotatable'),
scalable: this.element.hasAttribute('data-scalable'),
zoomable: this.element.hasAttribute('data-zoomable'),
zoomOnTouch: this.element.hasAttribute('data-zoom-on-touch'),
zoomOnWheel: this.element.hasAttribute('data-zoom-on-wheel'),
wheelZoomRatio: parseFloat(this.element.getAttribute('data-wheel-zoom-ratio')),
cropBoxMovable: this.element.hasAttribute('data-crop-box-movable'),
cropBoxResizable: this.element.hasAttribute('data-crop-box-resizable'),
toggleDragModeOnDblclick: this.element.hasAttribute('data-toggle-drag-mode-on-dblclick'),
minContainerWidth: parseInt(this.element.getAttribute('data-min-container-width')),
minContainerHeight: parseInt(this.element.getAttribute('data-min-container-height')),
minCanvasWidth: parseInt(this.element.getAttribute('data-min-canvas-width')),
minCanvasHeight: parseInt(this.element.getAttribute('data-min-canvas-height')),
minCropBoxWidth: parseInt(this.element.getAttribute('data-min-crop-box-width')),
minCropBoxHeight: parseInt(this.element.getAttribute('data-min-crop-box-height')),
};
img.src = (this as any).publicUrlValue;

if (this.element.getAttribute('data-aspect-ratio')) {
options.aspectRatio = parseFloat(this.element.getAttribute('data-aspect-ratio'));
const parent = (this.element as HTMLInputElement).parentNode;
if (!parent) {
throw new Error('Missing parent node for Cropperjs');
}

if (this.element.getAttribute('data-initial-aspect-ratio')) {
options.initialAspectRatio = parseFloat(this.element.getAttribute('data-initial-aspect-ratio'));
}
parent.appendChild(img);

const options = this.optionsValue;
this._dispatchEvent('cropperjs:pre-connect', { options, img });

// Build the cropper
const cropper = new Cropper(img, options);

img.addEventListener('crop', (event) => {
this.element.value = JSON.stringify(event.detail);
(this.element as HTMLInputElement).value = JSON.stringify((event as CropEvent).detail);
});

this._dispatchEvent('cropperjs:connect', { cropper, options, img });
}

_dispatchEvent(name, payload = null, canBubble = false, cancelable = false) {
_dispatchEvent(name: string, payload: any = null, canBubble = false, cancelable = false) {
const userEvent = document.createEvent('CustomEvent');
userEvent.initCustomEvent(name, canBubble, cancelable, payload);

Expand Down
Loading